mirror of
https://github.com/pinnacle-comp/pinnacle.git
synced 2025-01-30 20:34:49 +01:00
Expose keyboard focus stack to API
This commit is contained in:
parent
4750d7ce26
commit
dbccfa9c76
11 changed files with 333 additions and 14 deletions
|
@ -892,6 +892,7 @@ end
|
|||
---@field scale number?
|
||||
---@field transform Transform?
|
||||
---@field serial integer?
|
||||
---@field keyboard_focus_stack WindowHandle[]
|
||||
|
||||
---Get all properties of this output.
|
||||
---
|
||||
|
@ -900,12 +901,18 @@ function OutputHandle:props()
|
|||
local response = client.unary_request(output_service.GetProperties, { output_name = self.name })
|
||||
|
||||
---@diagnostic disable-next-line: invisible
|
||||
local handles = require("pinnacle.tag").handle.new_from_table(response.tag_ids or {})
|
||||
local tag_handles = require("pinnacle.tag").handle.new_from_table(response.tag_ids or {})
|
||||
|
||||
response.tags = handles
|
||||
---@diagnostic disable-next-line: invisible
|
||||
local keyboard_focus_stack_handles = require("pinnacle.window").handle.new_from_table(
|
||||
response.keyboard_focus_stack_window_ids or {}
|
||||
)
|
||||
|
||||
response.tags = tag_handles
|
||||
response.tag_ids = nil
|
||||
response.modes = response.modes or {}
|
||||
response.transform = transform_code_to_name[response.transform]
|
||||
response.keyboard_focus_stack = keyboard_focus_stack_handles
|
||||
|
||||
return response
|
||||
end
|
||||
|
@ -1060,6 +1067,53 @@ function OutputHandle:serial()
|
|||
return self:props().serial
|
||||
end
|
||||
|
||||
---Get this output's keyboard focus stack.
|
||||
---
|
||||
---This includes *all* windows on the output, even those on inactive tags.
|
||||
---If you only want visible windows, use `keyboard_focus_stack_visible` instead.
|
||||
---
|
||||
---Shorthand for `handle:props().keyboard_focus_stack`.
|
||||
---
|
||||
---@return WindowHandle[]
|
||||
---
|
||||
---@see OutputHandle.keyboard_focus_stack_visible
|
||||
function OutputHandle:keyboard_focus_stack()
|
||||
return self:props().keyboard_focus_stack
|
||||
end
|
||||
|
||||
---Get this output's keyboard focus stack.
|
||||
---
|
||||
---This only includes windows on active tags.
|
||||
---If you want all windows on the output, use `keyboard_focus_stack` instead.
|
||||
---
|
||||
---@return WindowHandle[]
|
||||
---
|
||||
---@see OutputHandle.keyboard_focus_stack
|
||||
function OutputHandle:keyboard_focus_stack_visible()
|
||||
local stack = self:props().keyboard_focus_stack
|
||||
|
||||
---@type (fun(): boolean)[]
|
||||
local batch = {}
|
||||
for i, win in ipairs(stack) do
|
||||
batch[i] = function()
|
||||
return win:is_on_active_tag()
|
||||
end
|
||||
end
|
||||
|
||||
local on_active_tags = require("pinnacle.util").batch(batch)
|
||||
|
||||
---@type WindowHandle[]
|
||||
local keyboard_focus_stack_visible = {}
|
||||
|
||||
for i, is_active in ipairs(on_active_tags) do
|
||||
if is_active then
|
||||
table.insert(keyboard_focus_stack_visible, stack[i])
|
||||
end
|
||||
end
|
||||
|
||||
return keyboard_focus_stack_visible
|
||||
end
|
||||
|
||||
---@nodoc
|
||||
---Create a new `OutputHandle` from its raw name.
|
||||
---@param output_name string
|
||||
|
|
|
@ -610,6 +610,32 @@ function WindowHandle:raise()
|
|||
client.unary_request(window_service.Raise, { window_id = self.id })
|
||||
end
|
||||
|
||||
---Returns whether or not this window is on an active tag.
|
||||
---
|
||||
---@return boolean
|
||||
function WindowHandle:is_on_active_tag()
|
||||
local tags = self:tags() or {}
|
||||
|
||||
---@type (fun(): boolean)[]
|
||||
local batch = {}
|
||||
|
||||
for i, tg in ipairs(tags) do
|
||||
batch[i] = function()
|
||||
return tg:active() or false
|
||||
end
|
||||
end
|
||||
|
||||
local actives = require("pinnacle.util").batch(batch)
|
||||
|
||||
for _, active in ipairs(actives) do
|
||||
if active then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
---@class WindowProperties
|
||||
---@field geometry { x: integer?, y: integer?, width: integer?, height: integer? }? The location and size of the window
|
||||
---@field class string? The window's class
|
||||
|
|
|
@ -94,6 +94,8 @@ message GetPropertiesResponse {
|
|||
//
|
||||
// The EDID serial number of this output, if it exists.
|
||||
optional uint32 serial = 16;
|
||||
// Window ids of the keyboard focus stack for this output.
|
||||
repeated uint32 keyboard_focus_stack_window_ids = 17;
|
||||
}
|
||||
|
||||
service OutputService {
|
||||
|
|
|
@ -26,6 +26,7 @@ use crate::{
|
|||
signal::{OutputSignal, SignalHandle},
|
||||
tag::{Tag, TagHandle},
|
||||
util::Batch,
|
||||
window::WindowHandle,
|
||||
ApiModules,
|
||||
};
|
||||
|
||||
|
@ -537,7 +538,7 @@ impl OutputId {
|
|||
/// Returns whether `output` is identified by this `OutputId`.
|
||||
pub fn matches(&self, output: &OutputHandle) -> bool {
|
||||
match self {
|
||||
OutputId::Name(name) => name == output.name(),
|
||||
OutputId::Name(name) => *name == output.name(),
|
||||
OutputId::Serial(serial) => Some(serial.get()) == output.serial(),
|
||||
}
|
||||
}
|
||||
|
@ -943,6 +944,11 @@ impl OutputHandle {
|
|||
scale: response.scale,
|
||||
transform: response.transform.and_then(|tf| tf.try_into().ok()),
|
||||
serial: response.serial,
|
||||
keyboard_focus_stack: response
|
||||
.keyboard_focus_stack_window_ids
|
||||
.into_iter()
|
||||
.map(|id| self.api.window.new_handle(id))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1142,9 +1148,37 @@ impl OutputHandle {
|
|||
self.props_async().await.serial
|
||||
}
|
||||
|
||||
/// Get this output's keyboard focus stack.
|
||||
///
|
||||
/// This will return the focus stack containing *all* windows on this output.
|
||||
/// If you only want windows on active tags, see
|
||||
/// [`OutputHandle::keyboard_focus_stack_visible`].
|
||||
///
|
||||
/// Shorthand for `self.props().keyboard_focus_stack`
|
||||
pub fn keyboard_focus_stack(&self) -> Vec<WindowHandle> {
|
||||
self.props().keyboard_focus_stack
|
||||
}
|
||||
|
||||
/// The async version of [`OutputHandle::keyboard_focus_stack`].
|
||||
pub async fn keyboard_focus_stack_async(&self) -> Vec<WindowHandle> {
|
||||
self.props_async().await.keyboard_focus_stack
|
||||
}
|
||||
|
||||
/// Get this output's keyboard focus stack with only visible windows.
|
||||
///
|
||||
/// If you only want a focus stack containing all windows on this output, see
|
||||
/// [`OutputHandle::keyboard_focus_stack`].
|
||||
pub fn keyboard_focus_stack_visible(&self) -> Vec<WindowHandle> {
|
||||
let keyboard_focus_stack = self.props().keyboard_focus_stack;
|
||||
|
||||
keyboard_focus_stack
|
||||
.batch_filter(|win| win.is_on_active_tag_async().boxed(), |is_on| *is_on)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get this output's unique name (the name of its connector).
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
pub fn name(&self) -> String {
|
||||
self.name.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1204,4 +1238,6 @@ pub struct OutputProperties {
|
|||
pub transform: Option<Transform>,
|
||||
/// This output's EDID serial number.
|
||||
pub serial: Option<u32>,
|
||||
/// This output's window keyboard focus stack.
|
||||
pub keyboard_focus_stack: Vec<WindowHandle>,
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@ impl Process {
|
|||
///
|
||||
/// ```
|
||||
/// process.spawn(["alacritty"]);
|
||||
/// process.spawn(["bash", "-c", "swaybg -i ~/path_to_wallpaper"]);
|
||||
/// process.spawn(["bash", "-c", "swaybg -i /path/to/wallpaper"]);
|
||||
/// ```
|
||||
pub fn spawn(&self, args: impl IntoIterator<Item = impl Into<String>>) {
|
||||
self.spawn_inner(args, false, None);
|
||||
|
@ -133,10 +133,11 @@ impl Process {
|
|||
has_callback: Some(callbacks.is_some()),
|
||||
};
|
||||
|
||||
let mut stream = block_on_tokio(client.spawn(request)).unwrap().into_inner();
|
||||
|
||||
self.fut_sender
|
||||
.send(
|
||||
async move {
|
||||
let mut stream = client.spawn(request).await.unwrap().into_inner();
|
||||
let Some(mut callbacks) = callbacks else { return };
|
||||
while let Some(Ok(response)) = stream.next().await {
|
||||
if let Some(line) = response.stdout {
|
||||
|
|
|
@ -466,6 +466,11 @@ impl TagHandle {
|
|||
pub async fn windows_async(&self) -> Vec<WindowHandle> {
|
||||
self.props_async().await.windows
|
||||
}
|
||||
|
||||
/// Get this tag's raw compositor id.
|
||||
pub fn id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
/// Properties of a tag.
|
||||
|
|
|
@ -125,7 +125,7 @@ impl Geometry {
|
|||
}
|
||||
}
|
||||
|
||||
/// Batch a set of requests that will be sent ot the compositor all at once.
|
||||
/// Batch a set of requests that will be sent to the compositor all at once.
|
||||
///
|
||||
/// # Rationale
|
||||
///
|
||||
|
@ -237,8 +237,8 @@ macro_rules! batch_boxed_async {
|
|||
|
||||
/// Methods for batch sending API requests to the compositor.
|
||||
pub trait Batch<I> {
|
||||
/// [`batch_map`][Batch::batch_map]s then finds the object for which `f` with the results
|
||||
/// returns `true`.
|
||||
/// [`batch_map`][Batch::batch_map]s then finds the object for which `find` with the results
|
||||
/// of awaiting `map_to_future(item)` returns `true`.
|
||||
fn batch_find<M, F, FutOp>(self, map_to_future: M, find: F) -> Option<I>
|
||||
where
|
||||
Self: Sized,
|
||||
|
@ -250,6 +250,14 @@ pub trait Batch<I> {
|
|||
where
|
||||
Self: Sized,
|
||||
F: for<'a> FnMut(&'a I) -> Pin<Box<dyn Future<Output = FutOp> + 'a>>;
|
||||
|
||||
/// [`batch_map`][Batch::batch_map]s then filters for objects for which `predicate` with the
|
||||
/// results of awaiting `map_to_future(item)` returns `true`.
|
||||
fn batch_filter<M, F, FutOp>(self, map_to_future: M, predicate: F) -> impl Iterator<Item = I>
|
||||
where
|
||||
Self: Sized,
|
||||
M: for<'a> FnMut(&'a I) -> Pin<Box<dyn Future<Output = FutOp> + 'a>>,
|
||||
F: FnMut(&FutOp) -> bool;
|
||||
}
|
||||
|
||||
impl<T: IntoIterator<Item = I>, I> Batch<I> for T {
|
||||
|
@ -281,4 +289,27 @@ impl<T: IntoIterator<Item = I>, I> Batch<I> for T {
|
|||
let futures = items.iter().map(map);
|
||||
crate::util::batch(futures).into_iter()
|
||||
}
|
||||
|
||||
fn batch_filter<M, F, FutOp>(
|
||||
self,
|
||||
map_to_future: M,
|
||||
mut predicate: F,
|
||||
) -> impl Iterator<Item = I>
|
||||
where
|
||||
Self: Sized,
|
||||
M: for<'a> FnMut(&'a I) -> Pin<Box<dyn Future<Output = FutOp> + 'a>>,
|
||||
F: FnMut(&FutOp) -> bool,
|
||||
{
|
||||
let items = self.into_iter().collect::<Vec<_>>();
|
||||
let futures = items.iter().map(map_to_future);
|
||||
let results = crate::util::batch(futures);
|
||||
|
||||
assert_eq!(items.len(), results.len());
|
||||
|
||||
items
|
||||
.into_iter()
|
||||
.zip(results)
|
||||
.filter(move |(_, fut_op)| predicate(fut_op))
|
||||
.map(|(item, _)| item)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -680,4 +680,27 @@ impl WindowHandle {
|
|||
pub async fn tags_async(&self) -> Vec<TagHandle> {
|
||||
self.props_async().await.tags
|
||||
}
|
||||
|
||||
/// Returns whether this window is on an active tag.
|
||||
pub fn is_on_active_tag(&self) -> bool {
|
||||
self.tags()
|
||||
.batch_find(
|
||||
|tag| tag.active_async().boxed(),
|
||||
|active| active.unwrap_or_default(),
|
||||
)
|
||||
.is_some()
|
||||
}
|
||||
|
||||
/// The async version of [`WindowHandle::is_on_active_tag`].
|
||||
pub async fn is_on_active_tag_async(&self) -> bool {
|
||||
let tags = self.tags_async().await;
|
||||
crate::util::batch_async(tags.iter().map(|tag| tag.active_async()))
|
||||
.await
|
||||
.contains(&Some(true))
|
||||
}
|
||||
|
||||
/// Get this window's raw compositor id.
|
||||
pub fn id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
|
15
src/api.rs
15
src/api.rs
|
@ -1240,6 +1240,20 @@ impl output_service_server::OutputService for OutputService {
|
|||
output.with_state(|state| state.serial.map(|serial| serial.get()))
|
||||
});
|
||||
|
||||
let keyboard_focus_stack_window_ids = output
|
||||
.as_ref()
|
||||
.map(|output| {
|
||||
output.with_state(|state| {
|
||||
state
|
||||
.focus_stack
|
||||
.stack
|
||||
.iter()
|
||||
.map(|win| win.with_state(|state| state.id.0))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
output::v0alpha1::GetPropertiesResponse {
|
||||
make,
|
||||
model,
|
||||
|
@ -1257,6 +1271,7 @@ impl output_service_server::OutputService for OutputService {
|
|||
scale,
|
||||
transform,
|
||||
serial,
|
||||
keyboard_focus_stack_window_ids,
|
||||
}
|
||||
})
|
||||
.await
|
||||
|
|
|
@ -505,6 +505,65 @@ mod output {
|
|||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
// FIXME: split this into keyboard_focus_stack and keyboard_focus_stack_visible tests
|
||||
#[tokio::main]
|
||||
#[self::test]
|
||||
async fn keyboard_focus_stack() -> anyhow::Result<()> {
|
||||
test_api(|_sender| {
|
||||
run_lua! { |Pinnacle|
|
||||
Pinnacle.output.setup({
|
||||
["*"] = { tags = { "1", "2", "3" } },
|
||||
})
|
||||
}
|
||||
|
||||
sleep_secs(1);
|
||||
|
||||
run_lua! { |Pinnacle|
|
||||
Pinnacle.process.spawn("foot")
|
||||
Pinnacle.process.spawn("foot")
|
||||
Pinnacle.process.spawn("foot")
|
||||
}
|
||||
|
||||
sleep_secs(1);
|
||||
|
||||
run_lua! { |Pinnacle|
|
||||
Pinnacle.tag.get("2"):switch_to()
|
||||
|
||||
Pinnacle.process.spawn("foot")
|
||||
Pinnacle.process.spawn("foot")
|
||||
}
|
||||
|
||||
sleep_secs(1);
|
||||
|
||||
run_lua! { |Pinnacle|
|
||||
Pinnacle.tag.get("1"):switch_to()
|
||||
|
||||
local focus_stack = Pinnacle.output.get_focused():keyboard_focus_stack()
|
||||
assert(#focus_stack == 5, "focus stack len != 5")
|
||||
assert(focus_stack[1].id == 0, "focus stack at 1 id != 0")
|
||||
assert(focus_stack[2].id == 1, "focus stack at 2 id != 1")
|
||||
assert(focus_stack[3].id == 2, "focus stack at 3 id != 2")
|
||||
assert(focus_stack[4].id == 3, "focus stack at 4 id != 3")
|
||||
assert(focus_stack[5].id == 4, "focus stack at 5 id != 4")
|
||||
|
||||
local focus_stack = Pinnacle.output.get_focused():keyboard_focus_stack_visible()
|
||||
assert(#focus_stack == 3, "focus stack visible len != 3")
|
||||
assert(focus_stack[1].id == 0)
|
||||
assert(focus_stack[2].id == 1)
|
||||
assert(focus_stack[3].id == 2)
|
||||
|
||||
Pinnacle.tag.get("2"):switch_to()
|
||||
|
||||
local focus_stack = Pinnacle.output.get_focused():keyboard_focus_stack_visible()
|
||||
assert(#focus_stack == 2)
|
||||
assert(focus_stack[1].id == 3)
|
||||
assert(focus_stack[2].id == 4)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
|
|
|
@ -2,6 +2,7 @@ mod common;
|
|||
|
||||
use std::thread::JoinHandle;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use pinnacle::backend::dummy::DUMMY_OUTPUT_NAME;
|
||||
use pinnacle_api::ApiModules;
|
||||
use test_log::test;
|
||||
|
@ -16,12 +17,12 @@ async fn run_rust_inner(run: impl FnOnce(ApiModules) + Send + 'static) {
|
|||
run(api.clone());
|
||||
}
|
||||
|
||||
fn run_rust(run: impl FnOnce(ApiModules) + Send + 'static) {
|
||||
fn run_rust(run: impl FnOnce(ApiModules) + Send + 'static) -> anyhow::Result<()> {
|
||||
std::thread::spawn(|| {
|
||||
run_rust_inner(run);
|
||||
})
|
||||
.join()
|
||||
.unwrap();
|
||||
.map_err(|_| anyhow!("rust oneshot api calls failed"))
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
|
@ -330,7 +331,7 @@ mod output {
|
|||
.get_focused()
|
||||
.unwrap()
|
||||
.set_transform(Transform::Flipped270);
|
||||
});
|
||||
})?;
|
||||
|
||||
sleep_secs(1);
|
||||
|
||||
|
@ -347,7 +348,7 @@ mod output {
|
|||
.get_focused()
|
||||
.unwrap()
|
||||
.set_transform(Transform::_180);
|
||||
});
|
||||
})?;
|
||||
|
||||
sleep_secs(1);
|
||||
|
||||
|
@ -359,5 +360,71 @@ mod output {
|
|||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
// FIXME: split this into keyboard_focus_stack and keyboard_focus_stack_visible tests
|
||||
#[tokio::main]
|
||||
#[self::test]
|
||||
async fn keyboard_focus_stack() -> anyhow::Result<()> {
|
||||
test_api(|_sender| {
|
||||
setup_rust(|api| {
|
||||
api.output.setup([
|
||||
OutputSetup::new_with_matcher(|_| true).with_tags(["1", "2", "3"])
|
||||
]);
|
||||
});
|
||||
|
||||
sleep_secs(1);
|
||||
|
||||
run_rust(|api| {
|
||||
api.process.spawn(["foot"]);
|
||||
api.process.spawn(["foot"]);
|
||||
api.process.spawn(["foot"]);
|
||||
})?;
|
||||
|
||||
sleep_secs(1);
|
||||
|
||||
run_rust(|api| {
|
||||
api.tag.get("2").unwrap().switch_to();
|
||||
api.process.spawn(["foot"]);
|
||||
api.process.spawn(["foot"]);
|
||||
})?;
|
||||
|
||||
sleep_secs(1);
|
||||
|
||||
run_rust(|api| {
|
||||
api.tag.get("1").unwrap().switch_to();
|
||||
|
||||
let focus_stack = api.output.get_focused().unwrap().keyboard_focus_stack();
|
||||
assert!(dbg!(focus_stack.len()) == 5);
|
||||
assert!(focus_stack[0].id() == 0);
|
||||
assert!(focus_stack[1].id() == 1);
|
||||
assert!(focus_stack[2].id() == 2);
|
||||
assert!(focus_stack[3].id() == 3);
|
||||
assert!(focus_stack[4].id() == 4);
|
||||
|
||||
let focus_stack = api
|
||||
.output
|
||||
.get_focused()
|
||||
.unwrap()
|
||||
.keyboard_focus_stack_visible();
|
||||
assert!(focus_stack.len() == 3);
|
||||
assert!(focus_stack[0].id() == 0);
|
||||
assert!(focus_stack[1].id() == 1);
|
||||
assert!(focus_stack[2].id() == 2);
|
||||
|
||||
api.tag.get("2").unwrap().switch_to();
|
||||
|
||||
let focus_stack = api
|
||||
.output
|
||||
.get_focused()
|
||||
.unwrap()
|
||||
.keyboard_focus_stack_visible();
|
||||
assert!(focus_stack.len() == 2);
|
||||
assert!(focus_stack[0].id() == 3);
|
||||
assert!(focus_stack[1].id() == 4);
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue