diff --git a/api/rust/examples/example_config.rs b/api/rust/examples/example_config.rs index 2fcd05f..26b117c 100644 --- a/api/rust/examples/example_config.rs +++ b/api/rust/examples/example_config.rs @@ -2,7 +2,10 @@ use pinnacle_api::{Modifier, MouseButton, MouseEdge}; fn main() { pinnacle_api::setup(|pinnacle| { - pinnacle.process.spawn(vec!["alacritty"]).unwrap(); + pinnacle.output.connect_for_all(move |output| { + pinnacle.tag.add(&output, &["1", "2", "3", "4", "5"]); + pinnacle.tag.get("1", Some(&output)).unwrap().toggle(); + }); pinnacle.input.keybind(&[Modifier::Ctrl], 'a', move || { pinnacle.process.spawn(vec!["alacritty"]).unwrap(); diff --git a/api/rust/src/lib.rs b/api/rust/src/lib.rs index 95b42d6..74111a8 100644 --- a/api/rust/src/lib.rs +++ b/api/rust/src/lib.rs @@ -2,6 +2,7 @@ mod input; mod msg; mod output; mod process; +mod tag; mod window; use input::Input; @@ -9,6 +10,7 @@ pub use input::MouseButton; pub use msg::Modifier; pub use msg::MouseEdge; use output::Output; +use tag::Tag; use window::Window; pub use xkbcommon::xkb::keysyms; pub use xkbcommon::xkb::Keysym; @@ -49,6 +51,7 @@ pub fn setup(config_func: impl FnOnce(Pinnacle)) -> anyhow::Result<()> { input: Input, window: Window, output: Output, + tag: Tag, }; config_func(pinnacle); @@ -174,4 +177,5 @@ pub struct Pinnacle { pub window: Window, pub input: Input, pub output: Output, + pub tag: Tag, } diff --git a/api/rust/src/output.rs b/api/rust/src/output.rs index 5d688f1..cae737d 100644 --- a/api/rust/src/output.rs +++ b/api/rust/src/output.rs @@ -1,6 +1,8 @@ use crate::{ - msg::{OutputName, Request, RequestResponse}, - request, + msg::{Args, CallbackId, Msg, OutputName, Request, RequestResponse}, + request, send_msg, + tag::TagHandle, + CALLBACK_VEC, }; /// Output management. @@ -46,6 +48,27 @@ impl Output { .map(|s| OutputHandle(OutputName(s))) .find(|op| op.properties().focused == Some(true)) } + + pub fn connect_for_all(&self, mut func: F) + where + F: FnMut(OutputHandle) + Send + 'static, + { + let args_callback = move |args: Option| { + if let Some(Args::ConnectForAllOutputs { output_name }) = args { + func(OutputHandle(OutputName(output_name))); + } + }; + + let mut callback_vec = CALLBACK_VEC.lock().unwrap(); + let len = callback_vec.len(); + callback_vec.push(Box::new(args_callback)); + + let msg = Msg::ConnectForAllOutputs { + callback_id: CallbackId(len as u32), + }; + + send_msg(msg).unwrap(); + } } /// An output handle. @@ -53,31 +76,33 @@ impl Output { /// This is a handle to one of your monitors. /// It serves to make it easier to deal with them, defining methods for getting properties and /// helpers for things like positioning multiple monitors. -pub struct OutputHandle(OutputName); +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct OutputHandle(pub OutputName); /// Properties of an output. pub struct OutputProperties { /// The make. - make: Option, + pub make: Option, /// The model. /// /// This is something like `27GL850` or whatever gibberish monitor manufacturers name their /// displays. - model: Option, + pub model: Option, /// The location of the output in the global space. - loc: Option<(i32, i32)>, + pub loc: Option<(i32, i32)>, /// The resolution of the output in pixels, where `res.0` is the width and `res.1` is the /// height. - res: Option<(i32, i32)>, + pub res: Option<(i32, i32)>, /// The refresh rate of the output in millihertz. /// /// For example, 60Hz is returned as 60000. - refresh_rate: Option, + pub refresh_rate: Option, /// The physical size of the output in millimeters. - physical_size: Option<(i32, i32)>, + pub physical_size: Option<(i32, i32)>, /// Whether or not the output is focused. - focused: Option, - // TODO: tags + pub focused: Option, + /// The tags on this output. + pub tags: Vec, } impl OutputHandle { @@ -108,6 +133,11 @@ impl OutputHandle { refresh_rate, physical_size, focused, + tags: tag_ids + .unwrap_or(vec![]) + .into_iter() + .map(TagHandle) + .collect(), } } } diff --git a/api/rust/src/tag.rs b/api/rust/src/tag.rs new file mode 100644 index 0000000..89f1ce9 --- /dev/null +++ b/api/rust/src/tag.rs @@ -0,0 +1,162 @@ +use std::collections::HashMap; + +use crate::{ + msg::{Layout, Msg, OutputName, Request, RequestResponse, TagId}, + output::{Output, OutputHandle}, + request, send_msg, +}; + +pub struct Tag; + +impl Tag { + /// Get a tag by its name and output. If `output` is `None`, the currently focused output will + /// be used instead. + /// + /// If multiple tags have the same name, this returns the first one. + pub fn get(&self, name: &str, output: Option<&OutputHandle>) -> Option { + self.get_all() + .filter(|tag| { + tag.properties() + .output + .is_some_and(|op| Some(&op) == output) + }) + .find(|tag| tag.properties().name.is_some_and(|s| s == name)) + } + + /// Get all tags. + pub fn get_all(&self) -> impl Iterator { + let RequestResponse::Tags { tag_ids } = request(Request::GetTags) else { + unreachable!() + }; + + tag_ids.into_iter().map(TagHandle) + } + + // TODO: return taghandles here + /// Add tags with the names from `names` to `output`. + pub fn add(&self, output: &OutputHandle, names: &[&str]) { + let msg = Msg::AddTags { + output_name: output.0.clone(), + tag_names: names.iter().map(|s| s.to_string()).collect(), + }; + + send_msg(msg).unwrap(); + } + + pub fn layout_cycler(&self, layouts: &[Layout]) -> LayoutCycler { + let mut indices = HashMap::::new(); + let layouts = layouts.to_vec(); + let len = layouts.len(); + let cycle = move |cycle: Cycle, output: Option<&OutputHandle>| { + let Some(output) = output.cloned().or_else(|| Output.get_focused()) else { + return; + }; + + let Some(tag) = output + .properties() + .tags + .into_iter() + .find(|tag| tag.properties().active == Some(true)) + else { + return; + }; + + let index = indices.entry(tag.0).or_insert(0); + + match cycle { + Cycle::Forward => { + if *index + 1 >= len { + *index = 0; + } else { + *index += 1; + } + } + Cycle::Backward => { + if index.wrapping_sub(1) == usize::MAX { + *index = len - 1; + } else { + *index -= 1; + } + } + } + + tag.set_layout(layouts[*index]); + }; + + LayoutCycler { + cycle: Box::new(cycle), + } + } +} + +/// Which direction to cycle layouts. +#[derive(Debug, Clone, Copy)] +enum Cycle { + /// Cycle layouts forward. + Forward, + /// Cycle layouts backward. + Backward, +} + +/// A layout cycler that keeps track of tags and their layouts and provides methods to cycle +/// layouts on them. +#[allow(clippy::type_complexity)] +pub struct LayoutCycler { + cycle: Box)>, +} + +impl LayoutCycler { + pub fn next(&mut self, output: Option<&OutputHandle>) { + (self.cycle)(Cycle::Forward, output); + } + + pub fn prev(&mut self, output: Option<&OutputHandle>) { + (self.cycle)(Cycle::Backward, output); + } +} + +pub struct TagHandle(pub TagId); + +pub struct TagProperties { + active: Option, + name: Option, + output: Option, +} + +impl TagHandle { + pub fn properties(&self) -> TagProperties { + let RequestResponse::TagProps { + active, + name, + output_name, + } = request(Request::GetTagProps { tag_id: self.0 }) + else { + unreachable!() + }; + + TagProperties { + active, + name, + output: output_name.map(|name| OutputHandle(OutputName(name))), + } + } + + pub fn toggle(&self) { + let msg = Msg::ToggleTag { tag_id: self.0 }; + send_msg(msg).unwrap(); + } + + pub fn switch_to(&self) { + let msg = Msg::SwitchToTag { tag_id: self.0 }; + send_msg(msg).unwrap(); + } + + pub fn set_layout(&self, layout: Layout) { + let msg = Msg::SetLayout { + tag_id: self.0, + layout, + }; + + send_msg(msg).unwrap() + } +}