Expose keyboard focus stack to API

This commit is contained in:
Ottatop 2024-05-15 21:24:20 -05:00
parent 4750d7ce26
commit dbccfa9c76
11 changed files with 333 additions and 14 deletions

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

@ -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>,
}

View file

@ -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 {

View file

@ -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.

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -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

View file

@ -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]

View file

@ -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(())
})
}
}
}