From 1cdeb59a38ece86d54432b9164ec39e483c54c04 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Sun, 21 Jan 2024 23:45:09 -0600 Subject: [PATCH] Nuke the old Rust API form orbit --- api/{rust_grpc => rust}/.gitignore | 0 api/rust/Cargo.toml | 29 +- .../examples/default_config/main.rs | 0 .../examples/default_config/metaconfig.toml | 0 api/rust/examples/example_config.rs | 210 ------ api/rust/metaconfig.toml | 52 -- .../pinnacle-api-macros/Cargo.toml | 0 .../pinnacle-api-macros/src/lib.rs | 0 api/rust/src/input.rs | 506 +++++++++---- api/rust/src/input/libinput.rs | 118 ++- api/rust/src/lib.rs | 384 +++++----- api/rust/src/msg.rs | 290 ------- api/rust/src/output.rs | 714 ++++++++++++------ api/{rust_grpc => rust}/src/pinnacle.rs | 0 api/rust/src/process.rs | 284 ++++--- api/rust/src/tag.rs | 658 +++++++++++----- api/{rust_grpc => rust}/src/util.rs | 0 api/rust/src/window.rs | 680 ++++++++++++----- api/rust/src/window/rules.rs | 556 +++++++++++--- api/rust_grpc/Cargo.toml | 25 - api/rust_grpc/src/input.rs | 383 ---------- api/rust_grpc/src/input/libinput.rs | 85 --- api/rust_grpc/src/lib.rs | 202 ----- api/rust_grpc/src/output.rs | 515 ------------- api/rust_grpc/src/process.rs | 178 ----- api/rust_grpc/src/tag.rs | 528 ------------- api/rust_grpc/src/window.rs | 530 ------------- api/rust_grpc/src/window/rules.rs | 521 ------------- 28 files changed, 2698 insertions(+), 4750 deletions(-) rename api/{rust_grpc => rust}/.gitignore (100%) rename api/{rust_grpc => rust}/examples/default_config/main.rs (100%) rename api/{rust_grpc => rust}/examples/default_config/metaconfig.toml (100%) delete mode 100644 api/rust/examples/example_config.rs delete mode 100644 api/rust/metaconfig.toml rename api/{rust_grpc => rust}/pinnacle-api-macros/Cargo.toml (100%) rename api/{rust_grpc => rust}/pinnacle-api-macros/src/lib.rs (100%) delete mode 100644 api/rust/src/msg.rs rename api/{rust_grpc => rust}/src/pinnacle.rs (100%) rename api/{rust_grpc => rust}/src/util.rs (100%) delete mode 100644 api/rust_grpc/Cargo.toml delete mode 100644 api/rust_grpc/src/input.rs delete mode 100644 api/rust_grpc/src/input/libinput.rs delete mode 100644 api/rust_grpc/src/lib.rs delete mode 100644 api/rust_grpc/src/output.rs delete mode 100644 api/rust_grpc/src/process.rs delete mode 100644 api/rust_grpc/src/tag.rs delete mode 100644 api/rust_grpc/src/window.rs delete mode 100644 api/rust_grpc/src/window/rules.rs diff --git a/api/rust_grpc/.gitignore b/api/rust/.gitignore similarity index 100% rename from api/rust_grpc/.gitignore rename to api/rust/.gitignore diff --git a/api/rust/Cargo.toml b/api/rust/Cargo.toml index bf259ba..0d0caf2 100644 --- a/api/rust/Cargo.toml +++ b/api/rust/Cargo.toml @@ -1,14 +1,25 @@ [package] -name = "pinnacle_api" -version = "0.0.1" +name = "pinnacle-api" +version = "0.0.2" edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +authors = ["Ottatop "] +description = "The Rust implementation of the Pinnacle compositor's configuration API" +license = "MPL-2.0" +repository = "https://github.com/pinnacle-comp/pinnacle" +keywords = ["compositor", "pinnacle", "api", "config"] +categories = ["api-bindings", "config"] [dependencies] -serde = { version = "1.0.188", features = ["derive"] } -rmp = { version = "0.8.12" } -rmp-serde = { version = "1.1.2" } -anyhow = { version = "1.0.75", features = ["backtrace"] } -lazy_static = "1.4.0" +pinnacle-api-defs = { path = "../../pinnacle-api-defs" } +pinnacle-api-macros = { path = "./pinnacle-api-macros" } +tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread", "net"] } +async-net = "2.0.0" +async-compat = "0.2.3" +tonic = "0.10.2" +tower = { version = "0.4.13", features = ["util"] } +futures = "0.3.30" +num_enum = "0.7.2" xkbcommon = "0.7.0" + +[workspace] +members = ["pinnacle-api-macros"] diff --git a/api/rust_grpc/examples/default_config/main.rs b/api/rust/examples/default_config/main.rs similarity index 100% rename from api/rust_grpc/examples/default_config/main.rs rename to api/rust/examples/default_config/main.rs diff --git a/api/rust_grpc/examples/default_config/metaconfig.toml b/api/rust/examples/default_config/metaconfig.toml similarity index 100% rename from api/rust_grpc/examples/default_config/metaconfig.toml rename to api/rust/examples/default_config/metaconfig.toml diff --git a/api/rust/examples/example_config.rs b/api/rust/examples/example_config.rs deleted file mode 100644 index ea5b8cb..0000000 --- a/api/rust/examples/example_config.rs +++ /dev/null @@ -1,210 +0,0 @@ -// You should glob import these to prevent your config from getting cluttered. -use pinnacle_api::prelude::*; -use pinnacle_api::*; - -fn main() { - // Connect to the Pinnacle server. - // This needs to be called before you start calling any config functions. - pinnacle_api::connect().unwrap(); - - let mod_key = Modifier::Ctrl; // This is set to Ctrl to not conflict with your WM/DE keybinds. - - let terminal = "alacritty"; - - process::set_env("MOZ_ENABLE_WAYLAND", "1"); - - // You must create a callback_vec to hold your callbacks. - // Rust is not Lua, so it takes a bit more work to get captures working. - // - // Anything that requires a callback will also require a mut reference to this struct. - // - // Additionally, all callbacks also take in `&mut CallbackVec`. - // This allows you to call functions that need callbacks within other callbacks. - let mut callback_vec = CallbackVec::new(); - - // Keybinds ------------------------------------------------------ - - input::mousebind( - &[mod_key], - MouseButton::Left, - MouseEdge::Press, - move |_| { - window::begin_move(MouseButton::Left); - }, - &mut callback_vec, - ); - - input::mousebind( - &[mod_key], - MouseButton::Right, - MouseEdge::Press, - move |_| { - window::begin_resize(MouseButton::Right); - }, - &mut callback_vec, - ); - - input::keybind( - &[mod_key, Modifier::Alt], - 'q', - |_| pinnacle_api::quit(), - &mut callback_vec, - ); - - input::keybind( - &[mod_key, Modifier::Alt], - 'c', - move |_| { - if let Some(window) = window::get_focused() { - window.close(); - } - }, - &mut callback_vec, - ); - - input::keybind( - &[mod_key], - xkbcommon::xkb::keysyms::KEY_Return, - move |_| { - process::spawn(vec![terminal]).unwrap(); - }, - &mut callback_vec, - ); - - input::keybind( - &[mod_key, Modifier::Alt], - xkbcommon::xkb::keysyms::KEY_space, - move |_| { - if let Some(window) = window::get_focused() { - window.toggle_floating(); - } - }, - &mut callback_vec, - ); - - input::keybind( - &[mod_key], - 'f', - move |_| { - if let Some(window) = window::get_focused() { - window.toggle_fullscreen(); - } - }, - &mut callback_vec, - ); - - input::keybind( - &[mod_key], - 'm', - move |_| { - if let Some(window) = window::get_focused() { - window.toggle_maximized(); - } - }, - &mut callback_vec, - ); - - // Output stuff ------------------------------------------------------- - - let tags = ["1", "2", "3", "4", "5"]; - - output::connect_for_all( - move |output, _| { - tag::add(&output, tags.as_slice()); - tag::get("1", Some(&output)).unwrap().toggle(); - }, - &mut callback_vec, - ); - - // Layouts ----------------------------------------------------------- - - // Create a `LayoutCycler` to cycle your layouts. - let mut layout_cycler = tag::layout_cycler(&[ - Layout::MasterStack, - Layout::Dwindle, - Layout::Spiral, - Layout::CornerTopLeft, - Layout::CornerTopRight, - Layout::CornerBottomLeft, - Layout::CornerBottomRight, - ]); - - // Cycle forward. - input::keybind( - &[mod_key], - xkbcommon::xkb::keysyms::KEY_space, - move |_| { - (layout_cycler.next)(None); - }, - &mut callback_vec, - ); - - // Cycle backward. - input::keybind( - &[mod_key, Modifier::Shift], - xkbcommon::xkb::keysyms::KEY_space, - move |_| { - (layout_cycler.prev)(None); - }, - &mut callback_vec, - ); - - // Keybinds for tags ------------------------------------------ - - for tag_name in tags.iter().map(|t| t.to_string()) { - // mod_key + 1-5 to switch to tag - let t = tag_name.clone(); - let num = tag_name.chars().next().unwrap(); - input::keybind( - &[mod_key], - num, - move |_| { - tag::get(&t, None).unwrap().switch_to(); - }, - &mut callback_vec, - ); - - // mod_key + Shift + 1-5 to toggle tag - let t = tag_name.clone(); - input::keybind( - &[mod_key, Modifier::Shift], - num, - move |_| { - tag::get(&t, None).unwrap().toggle(); - }, - &mut callback_vec, - ); - - // mod_key + Alt + 1-5 to move focused window to tag - let t = tag_name.clone(); - input::keybind( - &[mod_key, Modifier::Alt], - num, - move |_| { - if let Some(window) = window::get_focused() { - window.move_to_tag(&tag::get(&t, None).unwrap()); - } - }, - &mut callback_vec, - ); - - // mod_key + Shift + Alt + 1-5 to toggle tag on focused window - let t = tag_name.clone(); - input::keybind( - &[mod_key, Modifier::Shift, Modifier::Alt], - num, - move |_| { - if let Some(window) = window::get_focused() { - window.toggle_tag(&tag::get(&t, None).unwrap()); - } - }, - &mut callback_vec, - ); - } - - // At the very end of your config, you will need to start listening to Pinnacle in order for - // your callbacks to be correctly called. - // - // This will not return unless an error occurs. - pinnacle_api::listen(callback_vec); -} diff --git a/api/rust/metaconfig.toml b/api/rust/metaconfig.toml deleted file mode 100644 index aa70e7d..0000000 --- a/api/rust/metaconfig.toml +++ /dev/null @@ -1,52 +0,0 @@ -# This metaconfig.toml file dictates what config Pinnacle will run. -# -# When running Pinnacle, the compositor will look in the following directories for a metaconfig.toml file, -# in order from top to bottom: -# $PINNACLE_CONFIG_DIR -# $XDG_CONFIG_HOME/pinnacle/ -# ~/.config/pinnacle/ -# -# When Pinnacle finds a metaconfig.toml file, it will execute the command provided to `command`. -# For now, the only thing that should be here is `lua` with a path to the main config file. -# In the future, there will be a Rust API that can be run using `cargo run`. -# -# Because configuration is done using an external process, if it ever crashes, you lose all of your keybinds. -# In order prevent you from getting stuck in the compositor, you must define keybinds to reload your config -# and kill Pinnacle. -# -# More details on each setting can be found below. - -# The command Pinnacle will run on startup and when you reload your config. -# Paths are relative to the directory the metaconfig.toml file is in. -# This must be an array. -command = ["cargo", "run", "--example", "example_config"] - -### Keybinds ### -# Each keybind takes in a table with two fields: `modifiers` and `key`. -# - `modifiers` can be one of "Ctrl", "Alt", "Shift", or "Super". -# - `key` can be a string of any lowercase letter, number, -# "numN" where N is a number for numpad keys, or "esc"/"escape". -# Support for any xkbcommon key is planned for a future update. - -# The keybind that will reload your config. -reload_keybind = { modifiers = ["Ctrl", "Alt"], key = "r" } -# The keybind that will kill Pinnacle. -kill_keybind = { modifiers = ["Ctrl", "Alt", "Shift"], key = "escape" } - -### Socket directory ### -# Pinnacle will open a Unix socket at `$XDG_RUNTIME_DIR` by default, falling back to `/tmp` if it doesn't exist. -# If you want/need to change this, use the `socket_dir` setting set to the directory of your choosing. -# -# socket_dir = "/your/dir/here/" - -### Environment Variables ### -# You may need to specify to Lua where Pinnacle's Lua API library is. -# This is currently done using the `envs` table, with keys as the name of the environment variable and -# the value as the variable value. This supports $var expansion, and paths are relative to this metaconfig.toml file. -# -# Pinnacle will run your config with the additional PINNACLE_DIR environment variable. -# -# Here, LUA_PATH and LUA_CPATH are used to tell Lua the path to the library. -[envs] -# LUA_PATH = "$PINNACLE_LIB_DIR/lua/?.lua;$PINNACLE_LIB_DIR/lua/?/init.lua;$PINNACLE_LIB_DIR/lua/lib/?.lua;$PINNACLE_LIB_DIR/lua/lib/?/init.lua;$LUA_PATH" -# LUA_CPATH = "$PINNACLE_LIB_DIR/lua/lib/?.so;$LUA_CPATH" diff --git a/api/rust_grpc/pinnacle-api-macros/Cargo.toml b/api/rust/pinnacle-api-macros/Cargo.toml similarity index 100% rename from api/rust_grpc/pinnacle-api-macros/Cargo.toml rename to api/rust/pinnacle-api-macros/Cargo.toml diff --git a/api/rust_grpc/pinnacle-api-macros/src/lib.rs b/api/rust/pinnacle-api-macros/src/lib.rs similarity index 100% rename from api/rust_grpc/pinnacle-api-macros/src/lib.rs rename to api/rust/pinnacle-api-macros/src/lib.rs diff --git a/api/rust/src/input.rs b/api/rust/src/input.rs index e80fba1..5127c0a 100644 --- a/api/rust/src/input.rs +++ b/api/rust/src/input.rs @@ -1,165 +1,383 @@ //! Input management. +//! +//! This module provides [`Input`], a struct that gives you several different +//! methods for setting key- and mousebinds, changing xkeyboard settings, and more. +//! View the struct's documentation for more information. + +use futures::{ + channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture, FutureExt, StreamExt, +}; +use num_enum::TryFromPrimitive; +use pinnacle_api_defs::pinnacle::input::{ + self, + v0alpha1::{ + input_service_client::InputServiceClient, + set_libinput_setting_request::{CalibrationMatrix, Setting}, + SetKeybindRequest, SetLibinputSettingRequest, SetMousebindRequest, SetRepeatRateRequest, + SetXkbConfigRequest, + }, +}; +use tonic::transport::Channel; +use xkbcommon::xkb::Keysym; + +use self::libinput::LibinputSetting; pub mod libinput; -use xkbcommon::xkb::Keysym; - -use crate::{ - msg::{Args, CallbackId, KeyIntOrString, Msg}, - send_msg, CallbackVec, -}; - -/// Set a keybind. -/// -/// This function takes in four parameters: -/// - `modifiers`: A slice of the modifiers you want held for the keybind to trigger. -/// - `key`: The key that needs to be pressed. This takes `impl Into` and can -/// take the following three types: -/// - [`char`]: A character of the key you want. This can be `a`, `~`, `@`, and so on. -/// - [`u32`]: The key in numeric form. You can use the keys defined in [`xkbcommon::xkb::keysyms`] for this. -/// - [`Keysym`]: The key in `Keysym` form, from [xkbcommon::xkb::Keysym]. -/// - `action`: What you want to run. -/// - `callback_vec`: Your [`CallbackVec`] to insert `action` into. -/// -/// `action` takes in a `&mut `[`CallbackVec`] for use in the closure. -pub fn keybind<'a, F>( - modifiers: &[Modifier], - key: impl Into, - mut action: F, - callback_vec: &mut CallbackVec<'a>, -) where - F: FnMut(&mut CallbackVec) + 'a, -{ - let args_callback = move |_: Option, callback_vec: &mut CallbackVec<'_>| { - action(callback_vec); - }; - - let len = callback_vec.callbacks.len(); - callback_vec.callbacks.push(Box::new(args_callback)); - - let key = key.into(); - - let msg = Msg::SetKeybind { - key, - modifiers: modifiers.to_vec(), - callback_id: CallbackId(len as u32), - }; - - send_msg(msg).unwrap(); -} - -/// Set a mousebind. If called with an already existing mousebind, it gets replaced. -/// -/// The mousebind can happen either on button press or release, so you must -/// specify which edge you desire. -/// -/// `action` takes in a `&mut `[`CallbackVec`] for use in the closure. -pub fn mousebind<'a, F>( - modifiers: &[Modifier], - button: MouseButton, - edge: MouseEdge, - mut action: F, - callback_vec: &mut CallbackVec<'a>, -) where - F: FnMut(&mut CallbackVec) + 'a, -{ - let args_callback = move |_: Option, callback_vec: &mut CallbackVec<'_>| { - action(callback_vec); - }; - - let len = callback_vec.callbacks.len(); - callback_vec.callbacks.push(Box::new(args_callback)); - - let msg = Msg::SetMousebind { - modifiers: modifiers.to_vec(), - button: button as u32, - edge, - callback_id: CallbackId(len as u32), - }; - - send_msg(msg).unwrap(); -} - -/// Set the xkbconfig for your keyboard. -/// -/// Parameters set to `None` will be set to their default values. -/// -/// Read `xkeyboard-config(7)` for more information. -pub fn set_xkb_config( - rules: Option<&str>, - model: Option<&str>, - layout: Option<&str>, - variant: Option<&str>, - options: Option<&str>, -) { - let msg = Msg::SetXkbConfig { - rules: rules.map(|s| s.to_string()), - variant: variant.map(|s| s.to_string()), - layout: layout.map(|s| s.to_string()), - model: model.map(|s| s.to_string()), - options: options.map(|s| s.to_string()), - }; - - send_msg(msg).unwrap(); -} - /// A mouse button. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub enum MouseButton { - /// The left mouse button. + /// The left mouse button Left = 0x110, - /// The right mouse button. - Right, - /// The middle mouse button, pressed usually by clicking the scroll wheel. - Middle, - /// - Side, - /// - Extra, - /// - Forward, - /// - Back, + /// The right mouse button + Right = 0x111, + /// The middle mouse button + Middle = 0x112, + /// The side mouse button + Side = 0x113, + /// The extra mouse button + Extra = 0x114, + /// The forward mouse button + Forward = 0x115, + /// The backward mouse button + Back = 0x116, } -/// The edge on which you want things to trigger. -#[derive(Debug, Hash, serde::Serialize, serde::Deserialize, Clone, Copy, PartialEq, Eq)] +/// Keyboard modifiers. +#[repr(i32)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, TryFromPrimitive)] +pub enum Mod { + /// The shift key + Shift = 1, + /// The ctrl key + Ctrl, + /// The alt key + Alt, + /// The super key, aka meta, win, mod4 + Super, +} + +/// Press or release. +#[repr(i32)] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, TryFromPrimitive)] pub enum MouseEdge { - /// Actions will be triggered on button press. - Press, - /// Actions will be triggered on button release. + /// Perform actions on button press + Press = 1, + /// Perform actions on button release Release, } -impl From for KeyIntOrString { - fn from(value: char) -> Self { - Self::String(value.to_string()) - } +/// A struct that lets you define xkeyboard config options. +/// +/// See `xkeyboard-config(7)` for more information. +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] +pub struct XkbConfig { + /// Files of rules to be used for keyboard mapping composition + pub rules: Option<&'static str>, + /// Name of the model of your keyboard type + pub model: Option<&'static str>, + /// Layout(s) you intend to use + pub layout: Option<&'static str>, + /// Variant(s) of the layout you intend to use + pub variant: Option<&'static str>, + /// Extra xkb configuration options + pub options: Option<&'static str>, } -impl From for KeyIntOrString { - fn from(value: u32) -> Self { - Self::Int(value) - } +/// The `Input` struct. +/// +/// This struct contains methods that allow you to set key- and mousebinds, +/// change xkeyboard and libinput settings, and change the keyboard's repeat rate. +#[derive(Debug, Clone)] +pub struct Input { + channel: Channel, + fut_sender: UnboundedSender>, } -impl From for KeyIntOrString { - fn from(value: Keysym) -> Self { - Self::Int(value.raw()) +impl Input { + pub(crate) fn new( + channel: Channel, + fut_sender: UnboundedSender>, + ) -> Self { + Self { + channel, + fut_sender, + } } -} -/// A modifier key. -#[derive(Debug, PartialEq, Eq, Copy, Clone, serde::Serialize, serde::Deserialize)] -pub enum Modifier { - /// The shift key. - Shift, - /// The control key. - Ctrl, - /// The alt key. - Alt, - /// The super key. + fn create_input_client(&self) -> InputServiceClient { + InputServiceClient::new(self.channel.clone()) + } + + /// Set a keybind. /// - /// This is also known as the Windows key, meta, or Mod4 for those coming from Xorg. - Super, + /// If called with an already set keybind, it gets replaced. + /// + /// You must supply: + /// - `mods`: A list of [`Mod`]s. These must be held down for the keybind to trigger. + /// - `key`: The key that needs to be pressed. This can be anything that implements the [Key] trait: + /// - `char` + /// - `&str` and `String`: This is any name from + /// [xkbcommon-keysyms.h](https://xkbcommon.org/doc/current/xkbcommon-keysyms_8h.html) + /// without the `XKB_KEY_` prefix. + /// - `u32`: The numerical key code from the website above. + /// - A [`keysym`][Keysym] from the [`xkbcommon`] re-export. + /// - `action`: A closure that will be run when the keybind is triggered. + /// - Currently, any captures must be both `Send` and `'static`. If you want to mutate + /// something, consider using channels or [`Box::leak`]. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::input::Mod; + /// + /// // Set `Super + Shift + c` to close the focused window + /// input.keybind([Mod::Super, Mod::Shift], 'c', || { + /// if let Some(win) = window.get_focused() { + /// win.close(); + /// } + /// }); + /// + /// // With a string key + /// input.keybind([], "BackSpace", || { /* ... */ }); + /// + /// // With a numeric key + /// input.keybind([], 65, || { /* ... */ }); // 65 = 'A' + /// + /// // With a `Keysym` + /// input.keybind([], pinnacle_api::xkbcommon::xkb::Keysym::Return, || { /* ... */ }); + /// ``` + pub fn keybind( + &self, + mods: impl IntoIterator, + key: impl Key + Send + 'static, + mut action: impl FnMut() + Send + 'static, + ) { + let mut client = self.create_input_client(); + + let modifiers = mods.into_iter().map(|modif| modif as i32).collect(); + + self.fut_sender + .unbounded_send( + async move { + let mut stream = client + .set_keybind(SetKeybindRequest { + modifiers, + key: Some(input::v0alpha1::set_keybind_request::Key::RawCode( + key.into_keysym().raw(), + )), + }) + .await + .unwrap() + .into_inner(); + + while let Some(Ok(_response)) = stream.next().await { + action(); + } + } + .boxed(), + ) + .unwrap(); + } + + /// Set a mousebind. + /// + /// If called with an already set mousebind, it gets replaced. + /// + /// You must supply: + /// - `mods`: A list of [`Mod`]s. These must be held down for the keybind to trigger. + /// - `button`: A [`MouseButton`]. + /// - `edge`: A [`MouseEdge`]. This allows you to trigger the bind on either mouse press or release. + /// - `action`: A closure that will be run when the mousebind is triggered. + /// - Currently, any captures must be both `Send` and `'static`. If you want to mutate + /// something, consider using channels or [`Box::leak`]. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::input::{Mod, MouseButton, MouseEdge}; + /// + /// // Set `Super + left click` to start moving a window + /// input.mousebind([Mod::Super], MouseButton::Left, MouseEdge::Press, || { + /// window.begin_move(MouseButton::Press); + /// }); + /// ``` + pub fn mousebind( + &self, + mods: impl IntoIterator, + button: MouseButton, + edge: MouseEdge, + mut action: impl FnMut() + 'static + Send, + ) { + let mut client = self.create_input_client(); + + let modifiers = mods.into_iter().map(|modif| modif as i32).collect(); + + self.fut_sender + .unbounded_send( + async move { + let mut stream = client + .set_mousebind(SetMousebindRequest { + modifiers, + button: Some(button as u32), + edge: Some(edge as i32), + }) + .await + .unwrap() + .into_inner(); + + while let Some(Ok(_response)) = stream.next().await { + action(); + } + } + .boxed(), + ) + .unwrap(); + } + + /// Set the xkeyboard config. + /// + /// This allows you to set several xkeyboard options like `layout` and `rules`. + /// + /// See `xkeyboard-config(7)` for more information. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::input::XkbConfig; + /// + /// input.set_xkb_config(Xkbconfig { + /// layout: Some("us,fr,ge"), + /// options: Some("ctrl:swapcaps,caps:shift"), + /// ..Default::default() + /// }); + /// ``` + pub fn set_xkb_config(&self, xkb_config: XkbConfig) { + let mut client = self.create_input_client(); + + block_on(client.set_xkb_config(SetXkbConfigRequest { + rules: xkb_config.rules.map(String::from), + variant: xkb_config.variant.map(String::from), + layout: xkb_config.layout.map(String::from), + model: xkb_config.model.map(String::from), + options: xkb_config.options.map(String::from), + })) + .unwrap(); + } + + /// Set the keyboard's repeat rate. + /// + /// This allows you to set the time between holding down a key and it repeating + /// as well as the time between each repeat. + /// + /// Units are in milliseconds. + /// + /// # Examples + /// + /// ``` + /// // Set keyboard to repeat after holding down for half a second, + /// // and repeat once every 25ms (40 times a second) + /// input.set_repeat_rate(25, 500); + /// ``` + pub fn set_repeat_rate(&self, rate: i32, delay: i32) { + let mut client = self.create_input_client(); + + block_on(client.set_repeat_rate(SetRepeatRateRequest { + rate: Some(rate), + delay: Some(delay), + })) + .unwrap(); + } + + /// Set a libinput setting. + /// + /// From [freedesktop.org](https://www.freedesktop.org/wiki/Software/libinput/): + /// > libinput is a library to handle input devices in Wayland compositors + /// + /// As such, this method allows you to set various settings related to input devices. + /// This includes things like pointer acceleration and natural scrolling. + /// + /// See [`LibinputSetting`] for all the settings you can change. + /// + /// Note: currently Pinnacle applies anything set here to *every* device, regardless of what it + /// actually is. This will be fixed in the future. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::input::libinput::*; + /// + /// // Set pointer acceleration to flat + /// input.set_libinput_setting(LibinputSetting::AccelProfile(AccelProfile::Flat)); + /// + /// // Enable natural scrolling (reverses scroll direction; usually used with trackpads) + /// input.set_libinput_setting(LibinputSetting::NaturalScroll(true)); + /// ``` + pub fn set_libinput_setting(&self, setting: LibinputSetting) { + let mut client = self.create_input_client(); + + let setting = match setting { + LibinputSetting::AccelProfile(profile) => Setting::AccelProfile(profile as i32), + LibinputSetting::AccelSpeed(speed) => Setting::AccelSpeed(speed), + LibinputSetting::CalibrationMatrix(matrix) => { + Setting::CalibrationMatrix(CalibrationMatrix { + matrix: matrix.to_vec(), + }) + } + LibinputSetting::ClickMethod(method) => Setting::ClickMethod(method as i32), + LibinputSetting::DisableWhileTyping(disable) => Setting::DisableWhileTyping(disable), + LibinputSetting::LeftHanded(enable) => Setting::LeftHanded(enable), + LibinputSetting::MiddleEmulation(enable) => Setting::MiddleEmulation(enable), + LibinputSetting::RotationAngle(angle) => Setting::RotationAngle(angle), + LibinputSetting::ScrollButton(button) => Setting::RotationAngle(button), + LibinputSetting::ScrollButtonLock(enable) => Setting::ScrollButtonLock(enable), + LibinputSetting::ScrollMethod(method) => Setting::ScrollMethod(method as i32), + LibinputSetting::NaturalScroll(enable) => Setting::NaturalScroll(enable), + LibinputSetting::TapButtonMap(map) => Setting::TapButtonMap(map as i32), + LibinputSetting::TapDrag(enable) => Setting::TapDrag(enable), + LibinputSetting::TapDragLock(enable) => Setting::TapDragLock(enable), + LibinputSetting::Tap(enable) => Setting::Tap(enable), + }; + + block_on(client.set_libinput_setting(SetLibinputSettingRequest { + setting: Some(setting), + })) + .unwrap(); + } +} + +/// A trait that designates anything that can be converted into a [`Keysym`]. +pub trait Key { + /// Convert this into a [`Keysym`]. + fn into_keysym(self) -> Keysym; +} + +impl Key for Keysym { + fn into_keysym(self) -> Keysym { + self + } +} + +impl Key for char { + fn into_keysym(self) -> Keysym { + Keysym::from_char(self) + } +} + +impl Key for &str { + fn into_keysym(self) -> Keysym { + xkbcommon::xkb::keysym_from_name(self, xkbcommon::xkb::KEYSYM_NO_FLAGS) + } +} + +impl Key for String { + fn into_keysym(self) -> Keysym { + xkbcommon::xkb::keysym_from_name(&self, xkbcommon::xkb::KEYSYM_NO_FLAGS) + } +} + +impl Key for u32 { + fn into_keysym(self) -> Keysym { + Keysym::from(self) + } } diff --git a/api/rust/src/input/libinput.rs b/api/rust/src/input/libinput.rs index 23dddeb..5e24df7 100644 --- a/api/rust/src/input/libinput.rs +++ b/api/rust/src/input/libinput.rs @@ -1,40 +1,35 @@ -//! Libinput settings. +//! Types for libinput configuration. -use crate::{msg::Msg, send_msg}; - -/// Set a libinput setting. -/// -/// This takes a [`LibinputSetting`] containing what you want set. -pub fn set(setting: LibinputSetting) { - let msg = Msg::SetLibinputSetting(setting); - send_msg(msg).unwrap(); -} - -/// The acceleration profile. -#[derive(Debug, PartialEq, Copy, Clone, serde::Serialize)] +/// Pointer acceleration profile +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum AccelProfile { - /// Flat pointer acceleration. - Flat, - /// Adaptive pointer acceleration. + /// A flat acceleration profile. /// - /// This is the default for most devices. + /// Pointer motion is accelerated by a constant (device-specific) factor, depending on the current speed. + Flat = 1, + /// An adaptive acceleration profile. + /// + /// Pointer acceleration depends on the input speed. This is the default profile for most devices. Adaptive, } -/// The click method for a touchpad. -#[derive(Debug, PartialEq, Copy, Clone, serde::Serialize)] +/// The click method defines when to generate software-emulated buttons, usually on a device +/// that does not have a specific physical button available. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ClickMethod { /// Use software-button areas to generate button events. - ButtonAreas, + ButtonAreas = 1, /// The number of fingers decides which button press to generate. Clickfinger, } -/// The scroll method for a touchpad. -#[derive(Debug, PartialEq, Copy, Clone, serde::Serialize)] +/// The scroll method of a device selects when to generate scroll axis events instead of pointer motion events. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ScrollMethod { - /// Never send scroll events. - NoScroll, + /// Never send scroll events instead of pointer motion events. + /// + /// This has no effect on events generated by scroll wheels. + NoScroll = 1, /// Send scroll events when two fingers are logically down on the device. TwoFinger, /// Send scroll events when a finger moves along the bottom or right edge of a device. @@ -43,63 +38,48 @@ pub enum ScrollMethod { OnButtonDown, } -/// The mapping between finger count and button event for a touchpad. -#[derive(Debug, PartialEq, Copy, Clone, serde::Serialize)] +/// Map 1/2/3 finger tips to buttons. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum TapButtonMap { - /// 1/2/3 finger tap is mapped to left/right/middle click. + /// 1/2/3 finger tap maps to left/right/middle LeftRightMiddle, - /// 1/2/3 finger tap is mapped to left/middle/right click. + /// 1/2/3 finger tap maps to left/middle/right LeftMiddleRight, } -/// Libinput settings. -#[derive(Debug, PartialEq, Copy, Clone, serde::Serialize)] +/// Possible settings for libinput. +#[derive(Debug, Clone, Copy, PartialEq)] pub enum LibinputSetting { - /// Set the acceleration profile. + /// Set the pointer acceleration profile AccelProfile(AccelProfile), - /// Set the acceleration speed. - /// - /// This should be a float from -1.0 to 1.0. + /// Set pointer acceleration speed AccelSpeed(f64), - /// Set the calibration matrix. + /// Set the calibration matrix CalibrationMatrix([f32; 6]), - /// Set the click method. - /// - /// The click method defines when to generate software-emulated buttons, usually on a device - /// that does not have a specific physical button available. + /// Set the [`ClickMethod`] ClickMethod(ClickMethod), - /// Set whether or not the device will be disabled while typing. - DisableWhileTypingEnabled(bool), - /// Set device left-handedness. + /// Set whether the device gets disabled while typing + DisableWhileTyping(bool), + /// Set left handed mode LeftHanded(bool), - /// Set whether or not the middle click can be emulated. - MiddleEmulationEnabled(bool), - /// Set the rotation angle of a device. + /// Allow or disallow middle mouse button emulation + MiddleEmulation(bool), + /// Set the rotation angle RotationAngle(u32), - /// Set the scroll method. - ScrollMethod(ScrollMethod), - /// Set whether or not natural scroll is enabled. - /// - /// This reverses the direction of scrolling and is mainly used with touchpads. - NaturalScrollEnabled(bool), - /// Set the scroll button. + /// Set the scroll button ScrollButton(u32), - /// Set the tap button map, - /// - /// This determines whether taps with 2 and 3 fingers register as right and middle clicks or - /// the reverse. + /// Set whether the scroll button should be a drag or toggle + ScrollButtonLock(bool), + /// Set the [`ScrollMethod`] + ScrollMethod(ScrollMethod), + /// Enable or disable natural scrolling + NaturalScroll(bool), + /// Set the [`TapButtonMap`] TapButtonMap(TapButtonMap), - /// Set whether or not tap-and-drag is enabled. - /// - /// When enabled, a single-finger tap immediately followed by a finger down results in - /// a button down event, and subsequent finger motion thus triggers a drag. - /// The button is released on finger up. - TapDragEnabled(bool), - /// Set whether or not tap drag lock is enabled. - /// - /// When enabled, a finger may be lifted and put back on the touchpad within a timeout and the drag process - /// continues. When disabled, lifting the finger during a tap-and-drag will immediately stop the drag. - TapDragLockEnabled(bool), - /// Set whether or not tap-to-click is enabled. - TapEnabled(bool), + /// Enable or disable tap-to-drag + TapDrag(bool), + /// Enable or disable a timeout where lifting a finger off the device will not stop dragging + TapDragLock(bool), + /// Enable or disable tap-to-click + Tap(bool), } diff --git a/api/rust/src/lib.rs b/api/rust/src/lib.rs index b403eec..e573209 100644 --- a/api/rust/src/lib.rs +++ b/api/rust/src/lib.rs @@ -1,234 +1,202 @@ -//! The Rust implementation of the configuration API for Pinnacle, -//! a [Smithay](https://github.com/Smithay/smithay)-based Wayland compositor -//! inspired by [AwesomeWM](https://github.com/awesomeWM/awesome). - #![warn(missing_docs)] +//! The Rust implementation of [Pinnacle](https://github.com/pinnacle-comp/pinnacle)'s +//! configuration API. +//! +//! This library allows to to interface with the Pinnacle compositor and configure various aspects +//! like input and the tag system. +//! +//! # Configuration +//! +//! ## 1. Create a cargo project +//! To create your own Rust config, create a Cargo project in `~/.config/pinnacle`. +//! +//! ## 2. Create `metaconfig.toml` +//! Then, create a file named `metaconfig.toml`. This is the file Pinnacle will use to determine +//! what to run, kill and reload-config keybinds, an optional socket directory, and any environment +//! variables to give the config client. +//! +//! In `metaconfig.toml`, put the following: +//! ```toml +//! # `command` will tell Pinnacle to run `cargo run` in your config directory. +//! # You can add stuff like "--release" here if you want to. +//! command = ["cargo", "run"] +//! +//! # You must define a keybind to reload your config if it crashes, otherwise you'll get stuck if +//! # the Lua config doesn't kick in properly. +//! reload_keybind = { modifiers = ["Ctrl", "Alt"], key = "r" } +//! +//! # Similarly, you must define a keybind to kill Pinnacle. +//! kill_keybind = { modifiers = ["Ctrl", "Alt", "Shift"], key = "escape" } +//! +//! # You can specify an optional socket directory if you need to place the socket Pinnacle will +//! # use for configuration in a different place. +//! # socket_dir = "your/dir/here" +//! +//! # If you need to set any environment variables for the config process, you can do so here if +//! # you don't want to do it in the config itself. +//! [envs] +//! # key = "value" +//! ``` +//! +//! ## 3. Set up dependencies +//! In your `Cargo.toml`, add a dependency to `pinnacle-api`: +//! +//! ```toml +//! # Cargo.toml +//! +//! [dependencies] +//! pinnacle-api = { git = "https://github.com/pinnacle-comp/pinnacle" } +//! ``` +//! +//! ## 4. Set up the main function +//! In `main.rs`, change `fn main()` to `async fn main()` and annotate it with the +//! [`pinnacle_api::config`][`crate::config`] macro. Pass in the identifier you want to bind the +//! config modules to: +//! +//! ``` +//! use pinnacle_api::ApiModules; +//! +//! #[pinnacle_api::config(modules)] +//! async fn main() { +//! // `modules` is now available in the function body. +//! // You can deconstruct `ApiModules` to get all the config structs. +//! let ApiModules { +//! pinnacle, +//! process, +//! window, +//! input, +//! output, +//! tag, +//! } = modules; +//! } +//! ``` +//! +//! ## 5. Begin crafting your config! +//! You can peruse the documentation for things to configure. + +use std::sync::OnceLock; + +use futures::{ + channel::mpsc::UnboundedReceiver, future::BoxFuture, stream::FuturesUnordered, StreamExt, +}; +use input::Input; +use output::Output; +use pinnacle::Pinnacle; +use process::Process; +use tag::Tag; +use tonic::transport::{Endpoint, Uri}; +use tower::service_fn; +use window::Window; + pub mod input; -mod msg; pub mod output; +pub mod pinnacle; pub mod process; pub mod tag; +pub mod util; pub mod window; -/// The xkbcommon crate, re-exported for your convenience. +pub use pinnacle_api_macros::config; +pub use tokio; pub use xkbcommon; -/// The prelude for the Pinnacle API. +static PINNACLE: OnceLock = OnceLock::new(); +static PROCESS: OnceLock = OnceLock::new(); +static WINDOW: OnceLock = OnceLock::new(); +static INPUT: OnceLock = OnceLock::new(); +static OUTPUT: OnceLock = OnceLock::new(); +static TAG: OnceLock = OnceLock::new(); + +/// A struct containing static references to all of the configuration structs. +#[derive(Debug, Clone, Copy)] +pub struct ApiModules { + /// The [`Pinnacle`] struct + pub pinnacle: &'static Pinnacle, + /// The [`Process`] struct + pub process: &'static Process, + /// The [`Window`] struct + pub window: &'static Window, + /// The [`Input`] struct + pub input: &'static Input, + /// The [`Output`] struct + pub output: &'static Output, + /// The [`Tag`] struct + pub tag: &'static Tag, +} + +/// Connects to Pinnacle and builds the configuration structs. /// -/// This contains useful imports that you will likely need. -/// To that end, you can do `use pinnacle_api::prelude::*` to -/// prevent your config file from being cluttered with imports. -pub mod prelude { - pub use crate::input::libinput::*; - pub use crate::input::Modifier; - pub use crate::input::MouseButton; - pub use crate::input::MouseEdge; - pub use crate::output::AlignmentHorizontal; - pub use crate::output::AlignmentVertical; - pub use crate::tag::Layout; - pub use crate::window::rules::WindowRule; - pub use crate::window::rules::WindowRuleCondition; - pub use crate::window::FloatingOrTiled; - pub use crate::window::FullscreenOrMaximized; -} +/// This function is inserted at the top of your config through the [`config`] macro. +/// You should use that macro instead of this function directly. +pub async fn connect( +) -> Result<(ApiModules, UnboundedReceiver>), Box> { + let channel = Endpoint::try_from("http://[::]:50051")? // port doesn't matter, we use a unix socket + .connect_with_connector(service_fn(|_: Uri| { + tokio::net::UnixStream::connect( + std::env::var("PINNACLE_GRPC_SOCKET") + .expect("PINNACLE_GRPC_SOCKET was not set; is Pinnacle running?"), + ) + })) + .await?; -use std::{ - collections::{hash_map::Entry, HashMap}, - convert::Infallible, - io::{Read, Write}, - os::unix::net::UnixStream, - path::PathBuf, - sync::{atomic::AtomicU32, Mutex, OnceLock}, -}; + let (fut_sender, fut_recv) = futures::channel::mpsc::unbounded::>(); -use msg::{Args, CallbackId, IncomingMsg, Msg, Request, RequestResponse}; + let output = Output::new(channel.clone(), fut_sender.clone()); -use crate::msg::RequestId; + let pinnacle = PINNACLE.get_or_init(|| Pinnacle::new(channel.clone())); + let process = PROCESS.get_or_init(|| Process::new(channel.clone(), fut_sender.clone())); + let window = WINDOW.get_or_init(|| Window::new(channel.clone())); + let input = INPUT.get_or_init(|| Input::new(channel.clone(), fut_sender.clone())); + let tag = TAG.get_or_init(|| Tag::new(channel.clone(), fut_sender.clone())); + let output = OUTPUT.get_or_init(|| output); -static STREAM: OnceLock> = OnceLock::new(); -lazy_static::lazy_static! { - static ref UNREAD_CALLBACK_MSGS: Mutex> = Mutex::new(HashMap::new()); - static ref UNREAD_REQUEST_MSGS: Mutex> = Mutex::new(HashMap::new()); -} - -static REQUEST_ID_COUNTER: AtomicU32 = AtomicU32::new(0); - -fn send_msg(msg: Msg) -> anyhow::Result<()> { - let mut msg = rmp_serde::encode::to_vec_named(&msg)?; - let mut msg_len = (msg.len() as u32).to_ne_bytes(); - - let mut stream = STREAM.get().unwrap().lock().unwrap(); - - stream.write_all(msg_len.as_mut_slice())?; - stream.write_all(msg.as_mut_slice())?; - - Ok(()) -} - -fn read_msg(request_id: Option) -> IncomingMsg { - loop { - if let Some(request_id) = request_id { - if let Some(msg) = UNREAD_REQUEST_MSGS.lock().unwrap().remove(&request_id) { - return msg; - } - } - - let mut stream = STREAM.get().unwrap().lock().unwrap(); - let mut msg_len_bytes = [0u8; 4]; - stream.read_exact(msg_len_bytes.as_mut_slice()).unwrap(); - - let msg_len = u32::from_ne_bytes(msg_len_bytes); - let mut msg_bytes = vec![0u8; msg_len as usize]; - stream.read_exact(msg_bytes.as_mut_slice()).unwrap(); - - let incoming_msg: IncomingMsg = rmp_serde::from_slice(msg_bytes.as_slice()).unwrap(); - - if let Some(request_id) = request_id { - match &incoming_msg { - IncomingMsg::CallCallback { - callback_id, - args: _, - } => { - UNREAD_CALLBACK_MSGS - .lock() - .unwrap() - .insert(*callback_id, incoming_msg); - } - IncomingMsg::RequestResponse { - request_id: req_id, - response: _, - } => { - if req_id != &request_id { - UNREAD_REQUEST_MSGS - .lock() - .unwrap() - .insert(*req_id, incoming_msg); - } else { - return incoming_msg; - } - } - } - } else { - return incoming_msg; - } - } -} - -fn request(request: Request) -> RequestResponse { - use std::sync::atomic::Ordering; - let request_id = REQUEST_ID_COUNTER.fetch_add(1, Ordering::Relaxed); - - let msg = Msg::Request { - request_id: RequestId(request_id), - request, - }; - send_msg(msg).unwrap(); // TODO: propogate - - let IncomingMsg::RequestResponse { - request_id: _, - response, - } = read_msg(Some(RequestId(request_id))) - else { - unreachable!() + let modules = ApiModules { + pinnacle, + process, + window, + input, + output, + tag, }; - response + Ok((modules, fut_recv)) } -/// Connect to Pinnacle. This needs to be called before you begin calling config functions. +/// Listen to Pinnacle for incoming messages. /// -/// This will open up a connection to the Unix socket at `$PINNACLE_SOCKET`, -/// which should be set when you start the compositor. -pub fn connect() -> anyhow::Result<()> { - STREAM - .set(Mutex::new( - UnixStream::connect(PathBuf::from( - std::env::var("PINNACLE_SOCKET").unwrap_or("/tmp/pinnacle_socket".to_string()), - )) - .unwrap(), - )) - .unwrap(); - - Ok(()) -} - -/// Begin listening for messages coming from Pinnacle. +/// This will run all futures returned by configuration methods that take in callbacks in order to +/// call them. /// -/// This needs to be called at the very end of your `setup` function. -pub fn listen(mut callback_vec: CallbackVec) -> Infallible { - loop { - let mut unread_callback_msgs = UNREAD_CALLBACK_MSGS.lock().unwrap(); +/// This function is inserted at the end of your config through the [`config`] macro. +/// You should use the macro instead of this function directly. +pub async fn listen( + fut_recv: UnboundedReceiver>, // api_modules: ApiModules<'a>, +) { + let mut future_set = FuturesUnordered::< + BoxFuture<( + Option>, + Option>>, + )>, + >::new(); - for cb_id in unread_callback_msgs.keys().copied().collect::>() { - let Entry::Occupied(entry) = unread_callback_msgs.entry(cb_id) else { - unreachable!(); - }; - let IncomingMsg::CallCallback { callback_id, args } = entry.remove() else { - unreachable!(); - }; + future_set.push(Box::pin(async move { + let (fut, stream) = fut_recv.into_future().await; + (fut, Some(stream)) + })); - // Take the callback out and replace it with a dummy callback - // to allow callback_vec to be used mutably below. - let mut callback = std::mem::replace( - &mut callback_vec.callbacks[callback_id.0 as usize], - Box::new(|_, _| {}), - ); - - callback(args, &mut callback_vec); - - // Put it back. - callback_vec.callbacks[callback_id.0 as usize] = callback; + while let Some((fut, stream)) = future_set.next().await { + if let Some(fut) = fut { + future_set.push(Box::pin(async move { + fut.await; + (None, None) + })); + } + if let Some(stream) = stream { + future_set.push(Box::pin(async move { + let (fut, stream) = stream.into_future().await; + (fut, Some(stream)) + })) } - - let incoming_msg = read_msg(None); - - let IncomingMsg::CallCallback { callback_id, args } = incoming_msg else { - unreachable!(); - }; - - let mut callback = std::mem::replace( - &mut callback_vec.callbacks[callback_id.0 as usize], - Box::new(|_, _| {}), - ); - - callback(args, &mut callback_vec); - - callback_vec.callbacks[callback_id.0 as usize] = callback; - } -} - -/// Quit Pinnacle. -pub fn quit() { - send_msg(Msg::Quit).unwrap(); -} - -/// A wrapper around a vector that holds all of your callbacks. -/// -/// You will need to create this before you can start calling config functions -/// that require callbacks. -/// -/// Because your callbacks can capture things, we need a non-static way to hold them. -/// That's where this struct comes in. -/// -/// Every function that needs you to provide a callback will also need you to -/// provide a `&mut CallbackVec`. This will insert the callback for use in [`listen`]. -/// -/// Additionally, all callbacks will also take in `&mut CallbackVec`. This is so you can -/// call functions that need it inside of other callbacks. -/// -/// At the end of your config, you will need to call [`listen`] to begin listening for -/// messages from Pinnacle that will call your callbacks. Here, you must in pass your -/// `CallbackVec`. -#[derive(Default)] -pub struct CallbackVec<'a> { - #[allow(clippy::type_complexity)] - pub(crate) callbacks: Vec, &mut CallbackVec) + 'a>>, -} - -impl<'a> CallbackVec<'a> { - /// Create a new, empty `CallbackVec`. - pub fn new() -> Self { - Default::default() } } diff --git a/api/rust/src/msg.rs b/api/rust/src/msg.rs deleted file mode 100644 index 9aae141..0000000 --- a/api/rust/src/msg.rs +++ /dev/null @@ -1,290 +0,0 @@ -use std::num::NonZeroU32; - -use crate::{ - input::{libinput::LibinputSetting, Modifier, MouseEdge}, - output::OutputName, - tag::{Layout, TagId}, - window::{FloatingOrTiled, FullscreenOrMaximized, WindowId}, -}; - -#[derive(Debug, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize, Clone, Copy)] -pub(crate) struct CallbackId(pub u32); - -#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub(crate) struct WindowRuleCondition { - /// This condition is met when any of the conditions provided is met. - #[serde(default)] - pub cond_any: Option>, - /// This condition is met when all of the conditions provided are met. - #[serde(default)] - pub cond_all: Option>, - /// This condition is met when the class matches. - #[serde(default)] - pub class: Option>, - /// This condition is met when the title matches. - #[serde(default)] - pub title: Option>, - /// This condition is met when the tag matches. - #[serde(default)] - pub tag: Option>, -} - -#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize)] -pub(crate) struct WindowRule { - /// Set the output the window will open on. - #[serde(default)] - pub output: Option, - /// Set the tags the output will have on open. - #[serde(default)] - pub tags: Option>, - /// Set the window to floating or tiled on open. - #[serde(default)] - pub floating_or_tiled: Option, - /// Set the window to fullscreen, maximized, or force it to neither. - #[serde(default)] - pub fullscreen_or_maximized: Option, - /// Set the window's initial size. - #[serde(default)] - pub size: Option<(NonZeroU32, NonZeroU32)>, - /// Set the window's initial location. If the window is tiled, it will snap to this position - /// when set to floating. - #[serde(default)] - pub location: Option<(i32, i32)>, -} - -#[derive(Debug, Hash, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct RequestId(pub u32); - -#[derive(Debug, serde::Serialize)] -pub(crate) enum Msg { - // Input - SetKeybind { - key: KeyIntOrString, - modifiers: Vec, - callback_id: CallbackId, - }, - SetMousebind { - modifiers: Vec, - button: u32, - edge: MouseEdge, - callback_id: CallbackId, - }, - - // Window management - CloseWindow { - window_id: WindowId, - }, - SetWindowSize { - window_id: WindowId, - #[serde(default)] - width: Option, - #[serde(default)] - height: Option, - }, - MoveWindowToTag { - window_id: WindowId, - tag_id: TagId, - }, - ToggleTagOnWindow { - window_id: WindowId, - tag_id: TagId, - }, - ToggleFloating { - window_id: WindowId, - }, - ToggleFullscreen { - window_id: WindowId, - }, - ToggleMaximized { - window_id: WindowId, - }, - AddWindowRule { - cond: WindowRuleCondition, - rule: WindowRule, - }, - WindowMoveGrab { - button: u32, - }, - WindowResizeGrab { - button: u32, - }, - - // Tag management - ToggleTag { - tag_id: TagId, - }, - SwitchToTag { - tag_id: TagId, - }, - AddTags { - /// The name of the output you want these tags on. - output_name: OutputName, - tag_names: Vec, - }, - // TODO: - RemoveTags { - /// The name of the output you want these tags removed from. - tag_ids: Vec, - }, - SetLayout { - tag_id: TagId, - layout: Layout, - }, - - // Output management - ConnectForAllOutputs { - callback_id: CallbackId, - }, - SetOutputLocation { - output_name: OutputName, - #[serde(default)] - x: Option, - #[serde(default)] - y: Option, - }, - - // Process management - /// Spawn a program with an optional callback. - Spawn { - command: Vec, - #[serde(default)] - callback_id: Option, - }, - /// Spawn a program with an optional callback only if it isn't running. - SpawnOnce { - command: Vec, - #[serde(default)] - callback_id: Option, - }, - SetEnv { - key: String, - value: String, - }, - - // Pinnacle management - /// Quit the compositor. - Quit, - - // Input management - SetXkbConfig { - #[serde(default)] - rules: Option, - #[serde(default)] - variant: Option, - #[serde(default)] - layout: Option, - #[serde(default)] - model: Option, - #[serde(default)] - options: Option, - }, - - SetLibinputSetting(LibinputSetting), - - Request { - request_id: RequestId, - request: Request, - }, -} - -#[allow(clippy::enum_variant_names)] -#[derive(Debug, serde::Serialize, serde::Deserialize)] -/// Messages that require a server response, usually to provide some data. -pub(crate) enum Request { - // Windows - GetWindows, - GetWindowProps { window_id: WindowId }, - // Outputs - GetOutputs, - GetOutputProps { output_name: String }, - // Tags - GetTags, - GetTagProps { tag_id: TagId }, -} - -#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)] -pub enum KeyIntOrString { - Int(u32), - String(String), -} - -#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)] -pub enum Args { - /// Send a message with lines from the spawned process. - Spawn { - #[serde(default)] - stdout: Option, - #[serde(default)] - stderr: Option, - #[serde(default)] - exit_code: Option, - #[serde(default)] - exit_msg: Option, - }, - ConnectForAllOutputs { - output_name: String, - }, -} - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub(crate) enum IncomingMsg { - CallCallback { - callback_id: CallbackId, - #[serde(default)] - args: Option, - }, - RequestResponse { - request_id: RequestId, - response: RequestResponse, - }, -} - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub(crate) enum RequestResponse { - Window { - window_id: Option, - }, - Windows { - window_ids: Vec, - }, - WindowProps { - size: Option<(i32, i32)>, - loc: Option<(i32, i32)>, - class: Option, - title: Option, - focused: Option, - floating: Option, - fullscreen_or_maximized: Option, - }, - Output { - output_name: Option, - }, - Outputs { - output_names: Vec, - }, - OutputProps { - /// The make of the output. - make: Option, - /// The model of the output. - model: Option, - /// The location of the output in the space. - loc: Option<(i32, i32)>, - /// The resolution of the output. - res: Option<(i32, i32)>, - /// The refresh rate of the output. - refresh_rate: Option, - /// The size of the output, in millimeters. - physical_size: Option<(i32, i32)>, - /// Whether the output is focused or not. - focused: Option, - tag_ids: Option>, - }, - Tags { - tag_ids: Vec, - }, - TagProps { - active: Option, - name: Option, - output_name: Option, - }, -} diff --git a/api/rust/src/output.rs b/api/rust/src/output.rs index e94dc20..6ef6d9b 100644 --- a/api/rust/src/output.rs +++ b/api/rust/src/output.rs @@ -1,301 +1,515 @@ //! Output management. +//! +//! An output is Pinnacle's terminology for a monitor. +//! +//! This module provides [`Output`], which allows you to get [`OutputHandle`]s for different +//! connected monitors and set them up. -use crate::{ - msg::{Args, CallbackId, Msg, Request, RequestResponse}, - request, send_msg, - tag::TagHandle, - CallbackVec, +use futures::{ + channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture, FutureExt, StreamExt, }; +use pinnacle_api_defs::pinnacle::{ + output::{ + self, + v0alpha1::{ + output_service_client::OutputServiceClient, ConnectForAllRequest, SetLocationRequest, + }, + }, + tag::v0alpha1::tag_service_client::TagServiceClient, +}; +use tonic::transport::Channel; -/// A unique identifier for an output. +use crate::tag::TagHandle; + +/// A struct that allows you to get handles to connected outputs and set them up. /// -/// An empty string represents an invalid output. -#[derive(Debug, Hash, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] -pub(crate) struct OutputName(pub String); - -/// Get an [`OutputHandle`] by its name. -/// -/// `name` is the name of the port the output is plugged in to. -/// This is something like `HDMI-1` or `eDP-0`. -pub fn get_by_name(name: &str) -> Option { - let RequestResponse::Outputs { output_names } = request(Request::GetOutputs) else { - unreachable!() - }; - - output_names - .into_iter() - .find(|s| s == name) - .map(|s| OutputHandle(OutputName(s))) +/// See [`OutputHandle`] for more information. +#[derive(Debug, Clone)] +pub struct Output { + channel: Channel, + fut_sender: UnboundedSender>, } -/// Get a handle to all connected outputs. -pub fn get_all() -> impl Iterator { - let RequestResponse::Outputs { output_names } = request(Request::GetOutputs) else { - unreachable!() - }; - - output_names - .into_iter() - .map(|name| OutputHandle(OutputName(name))) -} - -/// Get the currently focused output. -/// -/// This is currently defined as the one with the cursor on it. -pub fn get_focused() -> Option { - let RequestResponse::Outputs { output_names } = request(Request::GetOutputs) else { - unreachable!() - }; - - output_names - .into_iter() - .map(|s| OutputHandle(OutputName(s))) - .find(|op| op.properties().focused == Some(true)) -} - -/// Connect a function to be run on all current and future outputs. -/// -/// When called, `connect_for_all` will run `func` with all currently connected outputs. -/// If a new output is connected, `func` will also be called with it. -/// -/// `func` takes in two parameters: -/// - `0`: An [`OutputHandle`] you can act on. -/// - `1`: A `&mut `[`CallbackVec`] for use in the closure. -/// -/// This will *not* be called if it has already been called for a given connector. -/// This means turning your monitor off and on or unplugging and replugging it *to the same port* -/// won't trigger `func`. Plugging it in to a new port *will* trigger `func`. -/// This is intended to prevent duplicate setup. -/// -/// Please note: this function will be run *after* Pinnacle processes your entire config. -/// For example, if you define tags in `func` but toggle them directly after `connect_for_all`, -/// nothing will happen as the tags haven't been added yet. -pub fn connect_for_all<'a, F>(mut func: F, callback_vec: &mut CallbackVec<'a>) -where - F: FnMut(OutputHandle, &mut CallbackVec) + 'a, -{ - let args_callback = move |args: Option, callback_vec: &mut CallbackVec<'_>| { - if let Some(Args::ConnectForAllOutputs { output_name }) = args { - func(OutputHandle(OutputName(output_name)), callback_vec); +impl Output { + pub(crate) fn new( + channel: Channel, + fut_sender: UnboundedSender>, + ) -> Self { + Self { + channel, + fut_sender, } - }; + } - let len = callback_vec.callbacks.len(); - callback_vec.callbacks.push(Box::new(args_callback)); + fn create_output_client(&self) -> OutputServiceClient { + OutputServiceClient::new(self.channel.clone()) + } - let msg = Msg::ConnectForAllOutputs { - callback_id: CallbackId(len as u32), - }; + fn create_tag_client(&self) -> TagServiceClient { + TagServiceClient::new(self.channel.clone()) + } - send_msg(msg).unwrap(); + /// Get a handle to all connected outputs. + /// + /// # Examples + /// + /// ``` + /// let outputs = output.get_all(); + /// ``` + pub fn get_all(&self) -> impl Iterator { + let mut client = self.create_output_client(); + let tag_client = self.create_tag_client(); + block_on(client.get(output::v0alpha1::GetRequest {})) + .unwrap() + .into_inner() + .output_names + .into_iter() + .map(move |name| OutputHandle { + client: client.clone(), + tag_client: tag_client.clone(), + name, + }) + } + + /// Get a handle to the output with the given name. + /// + /// By "name", we mean the name of the connector the output is connected to. + /// + /// # Examples + /// + /// ``` + /// let op = output.get_by_name("eDP-1")?; + /// let op2 = output.get_by_name("HDMI-2")?; + /// ``` + pub fn get_by_name(&self, name: impl Into) -> Option { + let name: String = name.into(); + self.get_all().find(|output| output.name == name) + } + + /// Get a handle to the focused output. + /// + /// This is currently implemented as the one that has had the most recent pointer movement. + /// + /// # Examples + /// + /// ``` + /// let op = output.get_focused()?; + /// ``` + pub fn get_focused(&self) -> Option { + self.get_all() + .find(|output| matches!(output.props().focused, Some(true))) + } + + /// Connect a closure to be run on all current and future outputs. + /// + /// When called, `connect_for_all` will do two things: + /// 1. Immediately run `for_all` with all currently connected outputs. + /// 2. Create a future that will call `for_all` with any newly connected outputs. + /// + /// Note that `for_all` will *not* run with outputs that have been unplugged and replugged. + /// This is to prevent duplicate setup. Instead, the compositor keeps track of any tags and + /// state the output had when unplugged and restores them on replug. + /// + /// # Examples + /// + /// ``` + /// // Add tags 1-3 to all outputs and set tag "1" to active + /// output.connect_for_all(|op| { + /// let tags = tag.add(&op, ["1", "2", "3"]); + /// tags.next().unwrap().set_active(true); + /// }); + /// ``` + pub fn connect_for_all(&self, mut for_all: impl FnMut(OutputHandle) + Send + 'static) { + for output in self.get_all() { + for_all(output); + } + + let mut client = self.create_output_client(); + let tag_client = self.create_tag_client(); + + self.fut_sender + .unbounded_send( + async move { + let mut stream = client + .connect_for_all(ConnectForAllRequest {}) + .await + .unwrap() + .into_inner(); + + while let Some(Ok(response)) = stream.next().await { + let Some(output_name) = response.output_name else { + continue; + }; + + let output = OutputHandle { + client: client.clone(), + tag_client: tag_client.clone(), + name: output_name, + }; + + for_all(output); + } + } + .boxed(), + ) + .unwrap(); + } } -/// An output handle. +/// A handle to an 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. -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct OutputHandle(pub(crate) OutputName); +/// This allows you to manipulate outputs and get their properties. +#[derive(Clone, Debug)] +pub struct OutputHandle { + pub(crate) client: OutputServiceClient, + pub(crate) tag_client: TagServiceClient, + pub(crate) name: String, +} -/// Properties of an output. -pub struct OutputProperties { - /// The make. - pub make: Option, - /// The model. - /// - /// This is something like `27GL850` or whatever gibberish monitor manufacturers name their - /// displays. - pub model: Option, - /// The location of the output in the global space. - pub loc: Option<(i32, i32)>, - /// The resolution of the output in pixels, where `res.0` is the width and `res.1` is the - /// height. - pub res: Option<(i32, i32)>, - /// The refresh rate of the output in millihertz. - /// - /// For example, 60Hz is returned as 60000. - pub refresh_rate: Option, - /// The physical size of the output in millimeters. - pub physical_size: Option<(i32, i32)>, - /// Whether or not the output is focused. - pub focused: Option, - /// The tags on this output. - pub tags: Vec, +/// The alignment to use for [`OutputHandle::set_loc_adj_to`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Alignment { + /// Set above, align left borders + TopAlignLeft, + /// Set above, align centers + TopAlignCenter, + /// Set above, align right borders + TopAlignRight, + /// Set below, align left borders + BottomAlignLeft, + /// Set below, align centers + BottomAlignCenter, + /// Set below, align right borders + BottomAlignRight, + /// Set to left, align top borders + LeftAlignTop, + /// Set to left, align centers + LeftAlignCenter, + /// Set to left, align bottom borders + LeftAlignBottom, + /// Set to right, align top borders + RightAlignTop, + /// Set to right, align centers + RightAlignCenter, + /// Set to right, align bottom borders + RightAlignBottom, } impl OutputHandle { - /// Get this output's name. - pub fn name(&self) -> String { - self.0 .0.clone() + /// Set the location of this output in the global space. + /// + /// On startup, Pinnacle will lay out all connected outputs starting at (0, 0) + /// and going to the right, with their top borders aligned. + /// + /// This method allows you to move outputs where necessary. + /// + /// Note: If you leave space between two outputs when setting their locations, + /// the pointer will not be able to move between them. + /// + /// # Examples + /// + /// ``` + /// // Assume two monitors in order, "DP-1" and "HDMI-1", with the following dimensions: + /// // - "DP-1": ┌─────┐ + /// // │ │1920x1080 + /// // └─────┘ + /// // - "HDMI-1": ┌───────┐ + /// // │ 2560x │ + /// // │ 1440 │ + /// // └───────┘ + /// + /// output.get_by_name("DP-1")?.set_location(0, 0); + /// output.get_by_name("HDMI-1")?.set_location(1920, -360); + /// + /// // Results in: + /// // x=0 ┌───────┐y=-360 + /// // y=0┌─────┤ │ + /// // │DP-1 │HDMI-1 │ + /// // └─────┴───────┘ + /// // ^x=1920 + /// ``` + pub fn set_location(&self, x: impl Into>, y: impl Into>) { + let mut client = self.client.clone(); + block_on(client.set_location(SetLocationRequest { + output_name: Some(self.name.clone()), + x: x.into(), + y: y.into(), + })) + .unwrap(); } - // TODO: Make OutputProperties an option, make non null fields not options - /// Get all properties of this output. - pub fn properties(&self) -> OutputProperties { - let RequestResponse::OutputProps { - make, - model, - loc, - res, - refresh_rate, - physical_size, - focused, - tag_ids, - } = request(Request::GetOutputProps { - output_name: self.0 .0.clone(), - }) - else { - unreachable!() + /// Set this output adjacent to another one. + /// + /// This is a helper method over [`OutputHandle::set_location`] to make laying out outputs + /// easier. + /// + /// `alignment` is an [`Alignment`] of how you want this output to be placed. + /// For example, [`TopAlignLeft`][Alignment::TopAlignLeft] will place this output + /// above `other` and align the left borders. + /// Similarly, [`RightAlignCenter`][Alignment::RightAlignCenter] will place this output + /// to the right of `other` and align their centers. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::output::Alignment; + /// + /// // Assume two monitors in order, "DP-1" and "HDMI-1", with the following dimensions: + /// // - "DP-1": ┌─────┐ + /// // │ │1920x1080 + /// // └─────┘ + /// // - "HDMI-1": ┌───────┐ + /// // │ 2560x │ + /// // │ 1440 │ + /// // └───────┘ + /// + /// output.get_by_name("DP-1")?.set_loc_adj_to(output.get_by_name("HDMI-1")?, Alignment::BottomAlignRight); + /// + /// // Results in: + /// // ┌───────┐ + /// // │ │ + /// // │HDMI-1 │ + /// // └──┬────┤ + /// // │DP-1│ + /// // └────┘ + /// // Notice that "DP-1" now has the coordinates (2280, 1440) because "DP-1" is getting moved, not "HDMI-1". + /// // "HDMI-1" was placed at (1920, 0) during the compositor's initial output layout. + /// ``` + pub fn set_loc_adj_to(&self, other: &OutputHandle, alignment: Alignment) { + let self_props = self.props(); + let other_props = other.props(); + + // poor man's try {} + let attempt_set_loc = || -> Option<()> { + let other_x = other_props.x?; + let other_y = other_props.y?; + let other_width = other_props.pixel_width? as i32; + let other_height = other_props.pixel_height? as i32; + + let self_width = self_props.pixel_width? as i32; + let self_height = self_props.pixel_height? as i32; + + use Alignment::*; + + let x: i32; + let y: i32; + + if let TopAlignLeft | TopAlignCenter | TopAlignRight | BottomAlignLeft + | BottomAlignCenter | BottomAlignRight = alignment + { + if let TopAlignLeft | TopAlignCenter | TopAlignRight = alignment { + y = other_y - self_height; + } else { + // bottom + y = other_y + other_height; + } + + match alignment { + TopAlignLeft | BottomAlignLeft => x = other_x, + TopAlignCenter | BottomAlignCenter => { + x = other_x + (other_width - self_width) / 2; + } + TopAlignRight | BottomAlignRight => x = other_x + (other_width - self_width), + _ => unreachable!(), + } + } else { + if let LeftAlignTop | LeftAlignCenter | LeftAlignBottom = alignment { + x = other_x - self_width; + } else { + x = other_x + other_width; + } + + match alignment { + LeftAlignTop | RightAlignTop => y = other_y, + LeftAlignCenter | RightAlignCenter => { + y = other_y + (other_height - self_height) / 2; + } + LeftAlignBottom | RightAlignBottom => { + y = other_y + (other_height - self_height); + } + _ => unreachable!(), + } + } + + self.set_location(Some(x), Some(y)); + + Some(()) }; + attempt_set_loc(); + } + + /// Get all properties of this output. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::output::OutputProperties; + /// + /// let OutputProperties { + /// make, + /// model, + /// x, + /// y, + /// pixel_width, + /// pixel_height, + /// refresh_rate, + /// physical_width, + /// physical_height, + /// focused, + /// tags, + /// } = output.get_focused()?.props(); + /// ``` + pub fn props(&self) -> OutputProperties { + let mut client = self.client.clone(); + let response = block_on( + client.get_properties(output::v0alpha1::GetPropertiesRequest { + output_name: Some(self.name.clone()), + }), + ) + .unwrap() + .into_inner(); + OutputProperties { - make, - model, - loc, - res, - refresh_rate, - physical_size, - focused, - tags: tag_ids - .unwrap_or(vec![]) + make: response.make, + model: response.model, + x: response.x, + y: response.y, + pixel_width: response.pixel_width, + pixel_height: response.pixel_height, + refresh_rate: response.refresh_rate, + physical_width: response.physical_width, + physical_height: response.physical_height, + focused: response.focused, + tags: response + .tag_ids .into_iter() - .map(TagHandle) + .map(|id| TagHandle { + client: self.tag_client.clone(), + output_client: self.client.clone(), + id, + }) .collect(), } } - /// Add tags with the given `names` to this output. - pub fn add_tags(&self, names: &[&str]) { - crate::tag::add(self, names); - } + // TODO: make a macro for the following or something - /// Set this output's location in the global space. - pub fn set_loc(&self, x: Option, y: Option) { - let msg = Msg::SetOutputLocation { - output_name: self.0.clone(), - x, - y, - }; - - send_msg(msg).unwrap(); - } - - /// Set this output's location to the right of `other`. + /// Get this output's make. /// - /// It will be aligned vertically based on the given `alignment`. - pub fn set_loc_right_of(&self, other: &OutputHandle, alignment: AlignmentVertical) { - self.set_loc_horizontal(other, LeftOrRight::Right, alignment); + /// Shorthand for `self.props().make`. + pub fn make(&self) -> Option { + self.props().make } - /// Set this output's location to the left of `other`. + /// Get this output's model. /// - /// It will be aligned vertically based on the given `alignment`. - pub fn set_loc_left_of(&self, other: &OutputHandle, alignment: AlignmentVertical) { - self.set_loc_horizontal(other, LeftOrRight::Left, alignment); + /// Shorthand for `self.props().make`. + pub fn model(&self) -> Option { + self.props().model } - /// Set this output's location to the top of `other`. + /// Get this output's x position in the global space. /// - /// It will be aligned horizontally based on the given `alignment`. - pub fn set_loc_top_of(&self, other: &OutputHandle, alignment: AlignmentHorizontal) { - self.set_loc_vertical(other, TopOrBottom::Top, alignment); + /// Shorthand for `self.props().x`. + pub fn x(&self) -> Option { + self.props().x } - /// Set this output's location to the bottom of `other`. + /// Get this output's y position in the global space. /// - /// It will be aligned horizontally based on the given `alignment`. - pub fn set_loc_bottom_of(&self, other: &OutputHandle, alignment: AlignmentHorizontal) { - self.set_loc_vertical(other, TopOrBottom::Bottom, alignment); + /// Shorthand for `self.props().y`. + pub fn y(&self) -> Option { + self.props().y } - fn set_loc_horizontal( - &self, - other: &OutputHandle, - left_or_right: LeftOrRight, - alignment: AlignmentVertical, - ) { - let op1_props = self.properties(); - let op2_props = other.properties(); - - let (Some(_self_loc), Some(self_res), Some(other_loc), Some(other_res)) = - (op1_props.loc, op1_props.res, op2_props.loc, op2_props.res) - else { - return; - }; - - let x = match left_or_right { - LeftOrRight::Left => other_loc.0 - self_res.0, - LeftOrRight::Right => other_loc.0 + self_res.0, - }; - - let y = match alignment { - AlignmentVertical::Top => other_loc.1, - AlignmentVertical::Center => other_loc.1 + (other_res.1 - self_res.1) / 2, - AlignmentVertical::Bottom => other_loc.1 + (other_res.1 - self_res.1), - }; - - self.set_loc(Some(x), Some(y)); + /// Get this output's screen width in pixels. + /// + /// Shorthand for `self.props().pixel_width`. + pub fn pixel_width(&self) -> Option { + self.props().pixel_width } - fn set_loc_vertical( - &self, - other: &OutputHandle, - top_or_bottom: TopOrBottom, - alignment: AlignmentHorizontal, - ) { - let op1_props = self.properties(); - let op2_props = other.properties(); + /// Get this output's screen height in pixels. + /// + /// Shorthand for `self.props().pixel_height`. + pub fn pixel_height(&self) -> Option { + self.props().pixel_height + } - let (Some(_self_loc), Some(self_res), Some(other_loc), Some(other_res)) = - (op1_props.loc, op1_props.res, op2_props.loc, op2_props.res) - else { - return; - }; + /// Get this output's refresh rate in millihertz. + /// + /// For example, 144Hz will be returned as 144000. + /// + /// Shorthand for `self.props().refresh_rate`. + pub fn refresh_rate(&self) -> Option { + self.props().refresh_rate + } - let y = match top_or_bottom { - TopOrBottom::Top => other_loc.1 - self_res.1, - TopOrBottom::Bottom => other_loc.1 + other_res.1, - }; + /// Get this output's physical width in millimeters. + /// + /// Shorthand for `self.props().physical_width`. + pub fn physical_width(&self) -> Option { + self.props().physical_width + } - let x = match alignment { - AlignmentHorizontal::Left => other_loc.0, - AlignmentHorizontal::Center => other_loc.0 + (other_res.0 - self_res.0) / 2, - AlignmentHorizontal::Right => other_loc.0 + (other_res.0 - self_res.0), - }; + /// Get this output's physical height in millimeters. + /// + /// Shorthand for `self.props().physical_height`. + pub fn physical_height(&self) -> Option { + self.props().physical_height + } - self.set_loc(Some(x), Some(y)); + /// Get whether this output is focused or not. + /// + /// This is currently implemented as the output with the most recent pointer motion. + /// + /// Shorthand for `self.props().focused`. + pub fn focused(&self) -> Option { + self.props().focused + } + + /// Get the tags this output has. + /// + /// Shorthand for `self.props().tags` + pub fn tags(&self) -> Vec { + self.props().tags + } + + /// Get this output's unique name (the name of its connector). + pub fn name(&self) -> &str { + &self.name } } -enum TopOrBottom { - Top, - Bottom, -} - -enum LeftOrRight { - Left, - Right, -} - -/// Horizontal alignment. -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum AlignmentHorizontal { - /// Align the outputs such that the left edges are in line. - Left, - /// Center the outputs horizontally. - Center, - /// Align the outputs such that the right edges are in line. - Right, -} - -/// Vertical alignment. -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum AlignmentVertical { - /// Align the outputs such that the top edges are in line. - Top, - /// Center the outputs vertically. - Center, - /// Align the outputs such that the bottom edges are in line. - Bottom, +/// The properties of an output. +#[derive(Clone, Debug)] +pub struct OutputProperties { + /// The make of the output + pub make: Option, + /// The model of the output + /// + /// This is something like "27GL83A" or whatever crap monitor manufacturers name their monitors + /// these days. + pub model: Option, + /// The x position of the output in the global space + pub x: Option, + /// The y position of the output in the global space + pub y: Option, + /// The output's screen width in pixels + pub pixel_width: Option, + /// The output's screen height in pixels + pub pixel_height: Option, + /// The output's refresh rate in millihertz + pub refresh_rate: Option, + /// The output's physical width in millimeters + pub physical_width: Option, + /// The output's physical height in millimeters + pub physical_height: Option, + /// Whether this output is focused or not + /// + /// This is currently implemented as the output with the most recent pointer motion. + pub focused: Option, + /// The tags this output has + pub tags: Vec, } diff --git a/api/rust_grpc/src/pinnacle.rs b/api/rust/src/pinnacle.rs similarity index 100% rename from api/rust_grpc/src/pinnacle.rs rename to api/rust/src/pinnacle.rs diff --git a/api/rust/src/process.rs b/api/rust/src/process.rs index ae412a3..121f2ec 100644 --- a/api/rust/src/process.rs +++ b/api/rust/src/process.rs @@ -1,132 +1,178 @@ //! Process management. +//! +//! This module provides [`Process`], which allows you to spawn processes and set environment +//! variables. -use crate::{ - msg::{Args, CallbackId, Msg}, - send_msg, CallbackVec, +use futures::{ + channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture, FutureExt, StreamExt, }; +use pinnacle_api_defs::pinnacle::process::v0alpha1::{ + process_service_client::ProcessServiceClient, SetEnvRequest, SpawnRequest, +}; +use tonic::transport::Channel; -/// Spawn a process. -/// -/// This will use Rust's (more specifically `async_process`'s) `Command` to spawn the provided -/// arguments. If you are using any shell syntax like `~`, you may need to spawn a shell -/// instead. If so, you may *also* need to correctly escape the input. -pub fn spawn(command: Vec<&str>) -> anyhow::Result<()> { - let msg = Msg::Spawn { - command: command.into_iter().map(|s| s.to_string()).collect(), - callback_id: None, - }; - - send_msg(msg) +/// A struct containing methods to spawn processes with optional callbacks and set environment +/// variables. +#[derive(Debug, Clone)] +pub struct Process { + channel: Channel, + fut_sender: UnboundedSender>, } -/// Spawn a process only if it isn't already running. -/// -/// This will use Rust's (more specifically `async_process`'s) `Command` to spawn the provided -/// arguments. If you are using any shell syntax like `~`, you may need to spawn a shell -/// instead. If so, you may *also* need to correctly escape the input. -pub fn spawn_once(command: Vec<&str>) -> anyhow::Result<()> { - let msg = Msg::SpawnOnce { - command: command.into_iter().map(|s| s.to_string()).collect(), - callback_id: None, - }; - - send_msg(msg) +/// Optional callbacks to be run when a spawned process prints to stdout or stderr or exits. +pub struct SpawnCallbacks { + /// A callback that will be run when a process prints to stdout with a line + pub stdout: Option>, + /// A callback that will be run when a process prints to stderr with a line + pub stderr: Option>, + /// A callback that will be run when a process exits with a status code and message + #[allow(clippy::type_complexity)] + pub exit: Option, String) + Send>>, } -/// Spawn a process with an optional callback for its stdout, stderr, and exit information. -/// -/// `callback` has the following parameters: -/// - `0`: The process's stdout printed this line. -/// - `1`: The process's stderr printed this line. -/// - `2`: The process exited with this code. -/// - `3`: The process exited with this message. -/// - `4`: A `&mut `[`CallbackVec`] for use inside the closure. -/// -/// You must also pass in a mutable reference to a [`CallbackVec`] in order to store your callback. -pub fn spawn_with_callback<'a, F>( - command: Vec<&str>, - mut callback: F, - callback_vec: &mut CallbackVec<'a>, -) -> anyhow::Result<()> -where - F: FnMut(Option, Option, Option, Option, &mut CallbackVec) + 'a, -{ - let args_callback = move |args: Option, callback_vec: &mut CallbackVec<'_>| { - if let Some(Args::Spawn { - stdout, - stderr, - exit_code, - exit_msg, - }) = args - { - callback(stdout, stderr, exit_code, exit_msg, callback_vec); +impl Process { + pub(crate) fn new( + channel: Channel, + fut_sender: UnboundedSender>, + ) -> Process { + Self { + channel, + fut_sender, } - }; + } - let len = callback_vec.callbacks.len(); - callback_vec.callbacks.push(Box::new(args_callback)); + fn create_process_client(&self) -> ProcessServiceClient { + ProcessServiceClient::new(self.channel.clone()) + } - let msg = Msg::Spawn { - command: command.into_iter().map(|s| s.to_string()).collect(), - callback_id: Some(CallbackId(len as u32)), - }; + /// Spawn a process. + /// + /// Note that windows spawned *before* tags are added will not be displayed. + /// This will be changed in the future to be more like Awesome, where windows with no tags are + /// displayed on every tag instead. + /// + /// # Examples + /// + /// ``` + /// process.spawn(["alacritty"]); + /// process.spawn(["bash", "-c", "swaybg -i ~/path_to_wallpaper"]); + /// ``` + pub fn spawn(&self, args: impl IntoIterator>) { + self.spawn_inner(args, false, None); + } - send_msg(msg) -} - -// TODO: literally copy pasted from above, but will be rewritten so meh -/// Spawn a process with an optional callback for its stdout, stderr, and exit information, -/// only if it isn't already running. -/// -/// `callback` has the following parameters: -/// - `0`: The process's stdout printed this line. -/// - `1`: The process's stderr printed this line. -/// - `2`: The process exited with this code. -/// - `3`: The process exited with this message. -/// - `4`: A `&mut `[`CallbackVec`] for use inside the closure. -/// -/// You must also pass in a mutable reference to a [`CallbackVec`] in order to store your callback. -pub fn spawn_once_with_callback<'a, F>( - command: Vec<&str>, - mut callback: F, - callback_vec: &mut CallbackVec<'a>, -) -> anyhow::Result<()> -where - F: FnMut(Option, Option, Option, Option, &mut CallbackVec) + 'a, -{ - let args_callback = move |args: Option, callback_vec: &mut CallbackVec<'_>| { - if let Some(Args::Spawn { - stdout, - stderr, - exit_code, - exit_msg, - }) = args - { - callback(stdout, stderr, exit_code, exit_msg, callback_vec); - } - }; - - let len = callback_vec.callbacks.len(); - callback_vec.callbacks.push(Box::new(args_callback)); - - let msg = Msg::SpawnOnce { - command: command.into_iter().map(|s| s.to_string()).collect(), - callback_id: Some(CallbackId(len as u32)), - }; - - send_msg(msg) -} - -/// Set an environment variable for Pinnacle. All future processes spawned will have this env set. -/// -/// Note that this will only set the variable for the compositor, not the running config process. -/// If you need to set an environment variable for this config, place them in the `metaconfig.toml` file instead -/// or use [`std::env::set_var`]. -pub fn set_env(key: &str, value: &str) { - let msg = Msg::SetEnv { - key: key.to_string(), - value: value.to_string(), - }; - - send_msg(msg).unwrap(); + /// Spawn a process with callbacks for its stdout, stderr, and exit information. + /// + /// See [`SpawnCallbacks`] for the passed in struct. + /// + /// Note that windows spawned *before* tags are added will not be displayed. + /// This will be changed in the future to be more like Awesome, where windows with no tags are + /// displayed on every tag instead. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::process::SpawnCallbacks; + /// + /// process.spawn_with_callbacks(["alacritty"], SpawnCallbacks { + /// stdout: Some(Box::new(|line| println!("stdout: {line}"))), + /// stderr: Some(Box::new(|line| println!("stderr: {line}"))), + /// stdout: Some(Box::new(|code, msg| println!("exit code: {code:?}, exit_msg: {msg}"))), + /// }); + /// ``` + pub fn spawn_with_callbacks( + &self, + args: impl IntoIterator>, + callbacks: SpawnCallbacks, + ) { + self.spawn_inner(args, false, Some(callbacks)); + } + + /// Spawn a process only if it isn't already running. + /// + /// This is useful for startup programs. + /// + /// See [`Process::spawn`] for details. + pub fn spawn_once(&self, args: impl IntoIterator>) { + self.spawn_inner(args, true, None); + } + + /// Spawn a process only if it isn't already running with optional callbacks for its stdout, + /// stderr, and exit information. + /// + /// This is useful for startup programs. + /// + /// See [`Process::spawn_with_callbacks`] for details. + pub fn spawn_once_with_callbacks( + &self, + args: impl IntoIterator>, + callbacks: SpawnCallbacks, + ) { + self.spawn_inner(args, true, Some(callbacks)); + } + + fn spawn_inner( + &self, + args: impl IntoIterator>, + once: bool, + callbacks: Option, + ) { + let mut client = self.create_process_client(); + + let args = args.into_iter().map(Into::into).collect::>(); + + let request = SpawnRequest { + args, + once: Some(once), + has_callback: Some(callbacks.is_some()), + }; + + self.fut_sender + .unbounded_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 { + if let Some(stdout) = callbacks.stdout.as_mut() { + stdout(line); + } + } + if let Some(line) = response.stderr { + if let Some(stderr) = callbacks.stderr.as_mut() { + stderr(line); + } + } + if let Some(exit_msg) = response.exit_message { + if let Some(exit) = callbacks.exit.as_mut() { + exit(response.exit_code, exit_msg); + } + } + } + } + .boxed(), + ) + .unwrap(); + } + + /// Set an environment variable for the compositor. + /// This will cause any future spawned processes to have this environment variable. + /// + /// # Examples + /// + /// ``` + /// process.set_env("ENV", "a value lalala"); + /// ``` + pub fn set_env(&self, key: impl Into, value: impl Into) { + let key = key.into(); + let value = value.into(); + + let mut client = self.create_process_client(); + + block_on(client.set_env(SetEnvRequest { + key: Some(key), + value: Some(value), + })) + .unwrap(); + } } diff --git a/api/rust/src/tag.rs b/api/rust/src/tag.rs index 45714d9..256fdd3 100644 --- a/api/rust/src/tag.rs +++ b/api/rust/src/tag.rs @@ -1,208 +1,528 @@ //! Tag management. +//! +//! This module allows you to interact with Pinnacle's tag system. +//! +//! # The Tag System +//! Many Wayland compositors use workspaces for window management. +//! Each window is assigned to a workspace and will only show up if that workspace is being +//! viewed. This is a find way to manage windows, but it's not that powerful. +//! +//! Instead, Pinnacle works with a tag system similar to window managers like [dwm](https://dwm.suckless.org/) +//! and, the window manager Pinnacle takes inspiration from, [awesome](https://awesomewm.org/). +//! +//! In a tag system, there are no workspaces. Instead, each window can be tagged with zero or more +//! tags, and zero or more tags can be displayed on a monitor at once. This allows you to, for +//! example, bring in your browsers on the same screen as your IDE by toggling the "Browser" tag. +//! +//! Workspaces can be emulated by only displaying one tag at a time. Combining this feature with +//! the ability to tag windows with multiple tags allows you to have one window show up on multiple +//! different "workspaces". As you can see, this system is much more powerful than workspaces +//! alone. +//! +//! # Configuration +//! `tag` contains the [`Tag`] struct, which allows you to add new tags +//! and get handles to already defined ones. +//! +//! These [`TagHandle`]s allow you to manipulate individual tags and get their properties. -use std::collections::HashMap; - -use crate::{ - msg::{Msg, Request, RequestResponse}, - output::{OutputHandle, OutputName}, - request, send_msg, +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, }; -/// 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(name: &str, output: Option<&OutputHandle>) -> Option { - get_all() - .filter(|tag| { - tag.properties().output.is_some_and(|op| match output { - Some(output) => &op == output, - None => Some(op) == crate::output::get_focused(), - }) +use futures::{channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture}; +use num_enum::TryFromPrimitive; +use pinnacle_api_defs::pinnacle::{ + output::v0alpha1::output_service_client::OutputServiceClient, + tag::{ + self, + v0alpha1::{ + tag_service_client::TagServiceClient, AddRequest, RemoveRequest, SetActiveRequest, + SetLayoutRequest, SwitchToRequest, + }, + }, +}; +use tonic::transport::Channel; + +use crate::output::{Output, OutputHandle}; + +/// A struct that allows you to add and remove tags and get [`TagHandle`]s. +#[derive(Clone, Debug)] +pub struct Tag { + channel: Channel, + fut_sender: UnboundedSender>, +} + +impl Tag { + pub(crate) fn new( + channel: Channel, + fut_sender: UnboundedSender>, + ) -> Self { + Self { + channel, + fut_sender, + } + } + + fn create_tag_client(&self) -> TagServiceClient { + TagServiceClient::new(self.channel.clone()) + } + + fn create_output_client(&self) -> OutputServiceClient { + OutputServiceClient::new(self.channel.clone()) + } + + /// Add tags to the specified output. + /// + /// This will add tags with the given names to `output` and return [`TagHandle`]s to all of + /// them. + /// + /// # Examples + /// + /// ``` + /// // Add tags 1-5 to the focused output + /// if let Some(op) = output.get_focused() { + /// let tags = tag.add(&op, ["1", "2", "3", "4", "5"]); + /// } + /// ``` + pub fn add( + &self, + output: &OutputHandle, + tag_names: impl IntoIterator>, + ) -> impl Iterator { + let mut client = self.create_tag_client(); + let output_client = self.create_output_client(); + + let tag_names = tag_names.into_iter().map(Into::into).collect(); + + let response = block_on(client.add(AddRequest { + output_name: Some(output.name.clone()), + tag_names, + })) + .unwrap() + .into_inner(); + + response.tag_ids.into_iter().map(move |id| TagHandle { + client: client.clone(), + output_client: output_client.clone(), + id, }) - .find(|tag| tag.properties().name.is_some_and(|s| s == name)) -} + } -/// Get all tags. -pub fn get_all() -> impl Iterator { - let RequestResponse::Tags { tag_ids } = request(Request::GetTags) else { - unreachable!() - }; + /// Get handles to all tags across all outputs. + /// + /// # Examples + /// + /// ``` + /// let all_tags = tag.get_all(); + /// ``` + pub fn get_all(&self) -> impl Iterator { + let mut client = self.create_tag_client(); + let output_client = self.create_output_client(); - tag_ids.into_iter().map(TagHandle) -} + let response = block_on(client.get(tag::v0alpha1::GetRequest {})) + .unwrap() + .into_inner(); -// TODO: return taghandles here -/// Add tags with the names from `names` to `output`. -pub fn add(output: &OutputHandle, names: &[&str]) { - let msg = Msg::AddTags { - output_name: output.0.clone(), - tag_names: names.iter().map(|s| s.to_string()).collect(), - }; + response.tag_ids.into_iter().map(move |id| TagHandle { + client: client.clone(), + output_client: output_client.clone(), + id, + }) + } - send_msg(msg).unwrap(); -} + /// Get a handle to the first tag with the given name on `output`. + /// + /// If `output` is `None`, the focused output will be used. + /// + /// # Examples + /// + /// ``` + /// // Get tag "1" on output "HDMI-1" + /// if let Some(op) = output.get_by_name("HDMI-1") { + /// let tg = tag.get("1", &op); + /// } + /// + /// // Get tag "Thing" on the focused output + /// let tg = tag.get("Thing", None); + /// ``` + pub fn get<'a>( + &self, + name: impl Into, + output: impl Into>, + ) -> Option { + let name = name.into(); + let output: Option<&OutputHandle> = output.into(); + let output_module = Output::new(self.channel.clone(), self.fut_sender.clone()); -/// Create a `LayoutCycler` to cycle layouts on tags. -/// -/// Given a slice of layouts, this will create a `LayoutCycler` with two methods; -/// one will cycle forward the layout for the active tag, and one will cycle backward. -/// -/// # Example -/// ``` -/// todo!() -/// ``` -pub fn layout_cycler(layouts: &[Layout]) -> LayoutCycler { - let indices = std::rc::Rc::new(std::cell::RefCell::new(HashMap::::new())); - let indices_clone = indices.clone(); - let layouts = layouts.to_vec(); - let layouts_clone = layouts.clone(); - let len = layouts.len(); - let next = move |output: Option<&OutputHandle>| { - let Some(output) = output.cloned().or_else(crate::output::get_focused) else { - return; + self.get_all().find(|tag| { + let props = tag.props(); + + let same_tag_name = props.name.as_ref() == Some(&name); + let same_output = props.output.is_some_and(|op| { + Some(op.name) + == output + .map(|o| o.name.clone()) + .or_else(|| output_module.get_focused().map(|o| o.name)) + }); + + same_tag_name && same_output + }) + } + + /// Remove the given tags from their outputs. + /// + /// # Examples + /// + /// ``` + /// let tags = tag.add(output.get_by_name("DP-1")?, ["1", "2", "Buckle", "Shoe"]); + /// + /// tag.remove(tags); // "DP-1" no longer has any tags + /// ``` + pub fn remove(&self, tags: impl IntoIterator) { + let tag_ids = tags.into_iter().map(|handle| handle.id).collect::>(); + + let mut client = self.create_tag_client(); + + block_on(client.remove(RemoveRequest { tag_ids })).unwrap(); + } + + /// Create a [`LayoutCycler`] to cycle layouts on outputs. + /// + /// This will create a `LayoutCycler` with two functions: one to cycle forward the layout for + /// the first active tag on the specified output, and one to cycle backward. + /// + /// If you do not specify an output for `LayoutCycler` functions, it will default to the + /// focused output. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::tag::{Layout, LayoutCycler}; + /// use pinnacle_api::xkbcommon::xkb::Keysym; + /// use pinnacle_api::input::Mod; + /// + /// // Create a layout cycler that cycles through the listed layouts + /// let LayoutCycler { + /// prev: layout_prev, + /// next: layout_next, + /// } = tag.new_layout_cycler([ + /// Layout::MasterStack, + /// Layout::Dwindle, + /// Layout::Spiral, + /// Layout::CornerTopLeft, + /// Layout::CornerTopRight, + /// Layout::CornerBottomLeft, + /// Layout::CornerBottomRight, + /// ]); + /// + /// // Cycle layouts forward on the focused output + /// layout_next(None); + /// + /// // Cycle layouts backward on the focused output + /// layout_prev(None); + /// + /// // Cycle layouts forward on "eDP-1" + /// layout_next(output.get_by_name("eDP-1")?); + /// ``` + pub fn new_layout_cycler(&self, layouts: impl IntoIterator) -> LayoutCycler { + let indices = Arc::new(Mutex::new(HashMap::::new())); + let indices_clone = indices.clone(); + + let layouts = layouts.into_iter().collect::>(); + let layouts_clone = layouts.clone(); + let len = layouts.len(); + + let output_module = Output::new(self.channel.clone(), self.fut_sender.clone()); + let output_module_clone = output_module.clone(); + + let next = move |output: Option<&OutputHandle>| { + let Some(output) = output.cloned().or_else(|| output_module.get_focused()) else { + return; + }; + + let Some(first_tag) = output + .props() + .tags + .into_iter() + .find(|tag| tag.active() == Some(true)) + else { + return; + }; + + let mut indices = indices.lock().expect("layout next mutex lock failed"); + let index = indices.entry(first_tag.id).or_insert(0); + + if *index + 1 >= len { + *index = 0; + } else { + *index += 1; + } + + first_tag.set_layout(layouts[*index]); }; - let Some(tag) = output - .properties() - .tags - .into_iter() - .find(|tag| tag.properties().active == Some(true)) - else { - return; + let prev = move |output: Option<&OutputHandle>| { + let Some(output) = output + .cloned() + .or_else(|| output_module_clone.get_focused()) + else { + return; + }; + + let Some(first_tag) = output + .props() + .tags + .into_iter() + .find(|tag| tag.active() == Some(true)) + else { + return; + }; + + let mut indices = indices_clone.lock().expect("layout next mutex lock failed"); + let index = indices.entry(first_tag.id).or_insert(0); + + if index.checked_sub(1).is_none() { + *index = len - 1; + } else { + *index -= 1; + } + + first_tag.set_layout(layouts_clone[*index]); }; - let mut indices = indices.borrow_mut(); - let index = indices.entry(tag.0).or_insert(0); - - if *index + 1 >= len { - *index = 0; - } else { - *index += 1; + LayoutCycler { + prev: Box::new(prev), + next: Box::new(next), } - - tag.set_layout(layouts[*index]); - }; - let prev = move |output: Option<&OutputHandle>| { - let Some(output) = output.cloned().or_else(crate::output::get_focused) else { - return; - }; - - let Some(tag) = output - .properties() - .tags - .into_iter() - .find(|tag| tag.properties().active == Some(true)) - else { - return; - }; - - let mut indices = indices_clone.borrow_mut(); - let index = indices.entry(tag.0).or_insert(0); - - if index.wrapping_sub(1) == usize::MAX { - *index = len - 1; - } else { - *index -= 1; - } - - tag.set_layout(layouts_clone[*index]); - }; - - LayoutCycler { - next: Box::new(next), - prev: Box::new(prev), } } -/// A layout cycler that keeps track of tags and their layouts and provides methods to cycle +/// A layout cycler that keeps track of tags and their layouts and provides functions to cycle /// layouts on them. #[allow(clippy::type_complexity)] pub struct LayoutCycler { /// Cycle to the next layout on the given output, or the focused output if `None`. - pub next: Box)>, + pub prev: Box) + Send + Sync + 'static>, /// Cycle to the previous layout on the given output, or the focused output if `None`. - pub prev: Box)>, -} - -#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy, serde::Serialize, serde::Deserialize)] -pub(crate) enum TagId { - None, - #[serde(untagged)] - Some(u32), + pub next: Box) + Send + Sync + 'static>, } /// A handle to a tag. -pub struct TagHandle(pub(crate) TagId); - -/// Properties of a tag, retrieved through [`TagHandle::properties`]. -#[derive(Debug)] -pub struct TagProperties { - /// Whether or not the tag is active. - pub active: Option, - /// The tag's name. - pub name: Option, - /// The output the tag is on. - pub output: Option, +/// +/// This handle allows you to do things like switch to tags and get their properties. +#[derive(Debug, Clone)] +pub struct TagHandle { + pub(crate) client: TagServiceClient, + pub(crate) output_client: OutputServiceClient, + pub(crate) id: u32, } -impl TagHandle { - /// Get this tag's [`TagProperties`]. - 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))), - } - } - - /// Toggle this tag. - pub fn toggle(&self) { - let msg = Msg::ToggleTag { tag_id: self.0 }; - send_msg(msg).unwrap(); - } - - /// Switch to this tag, deactivating all others on its output. - pub fn switch_to(&self) { - let msg = Msg::SwitchToTag { tag_id: self.0 }; - send_msg(msg).unwrap(); - } - - /// Set this tag's [`Layout`]. - pub fn set_layout(&self, layout: Layout) { - let msg = Msg::SetLayout { - tag_id: self.0, - layout, - }; - - send_msg(msg).unwrap() - } -} - -/// Layouts for tags. -#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)] +/// Various static layouts. +#[repr(i32)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, TryFromPrimitive)] pub enum Layout { - /// One master window on the left with all other windows stacked to the right. - MasterStack, - /// Windows split in half towards the bottom right corner. + /// One master window on the left with all other windows stacked to the right + MasterStack = 1, + /// Windows split in half towards the bottom right corner Dwindle, /// Windows split in half in a spiral Spiral, - /// One main corner window in the top left with a column of windows on the right and a row on the bottom. + /// One main corner window in the top left with a column of windows on the right and a row on the bottom CornerTopLeft, - /// One main corner window in the top right with a column of windows on the left and a row on the bottom. + /// One main corner window in the top right with a column of windows on the left and a row on the bottom CornerTopRight, /// One main corner window in the bottom left with a column of windows on the right and a row on the top. CornerBottomLeft, /// One main corner window in the bottom right with a column of windows on the left and a row on the top. CornerBottomRight, } + +impl TagHandle { + /// Activate this tag and deactivate all other ones on the same output. + /// + /// This essentially emulates what a traditional workspace is. + /// + /// # Examples + /// + /// ``` + /// // Assume the focused output has the following inactive tags and windows: + /// // "1": Alacritty + /// // "2": Firefox, Discord + /// // "3": Steam + /// tag.get("2")?.switch_to(); // Displays Firefox and Discord + /// tag.get("3")?.switch_to(); // Displays Steam + /// ``` + pub fn switch_to(&self) { + let mut client = self.client.clone(); + block_on(client.switch_to(SwitchToRequest { + tag_id: Some(self.id), + })) + .unwrap(); + } + + /// Set this tag to active or not. + /// + /// While active, windows with this tag will be displayed. + /// + /// While inactive, windows with this tag will not be displayed unless they have other active + /// tags. + /// + /// # Examples + /// + /// ``` + /// // Assume the focused output has the following inactive tags and windows: + /// // "1": Alacritty + /// // "2": Firefox, Discord + /// // "3": Steam + /// tag.get("2")?.set_active(true); // Displays Firefox and Discord + /// tag.get("3")?.set_active(true); // Displays Firefox, Discord, and Steam + /// tag.get("2")?.set_active(false); // Displays Steam + /// ``` + pub fn set_active(&self, set: bool) { + let mut client = self.client.clone(); + block_on(client.set_active(SetActiveRequest { + tag_id: Some(self.id), + set_or_toggle: Some(tag::v0alpha1::set_active_request::SetOrToggle::Set(set)), + })) + .unwrap(); + } + + /// Toggle this tag between active and inactive. + /// + /// While active, windows with this tag will be displayed. + /// + /// While inactive, windows with this tag will not be displayed unless they have other active + /// tags. + /// + /// # Examples + /// + /// ``` + /// // Assume the focused output has the following inactive tags and windows: + /// // "1": Alacritty + /// // "2": Firefox, Discord + /// // "3": Steam + /// tag.get("2")?.toggle(); // Displays Firefox and Discord + /// tag.get("3")?.toggle(); // Displays Firefox, Discord, and Steam + /// tag.get("3")?.toggle(); // Displays Firefox, Discord + /// tag.get("2")?.toggle(); // Displays nothing + /// ``` + pub fn toggle_active(&self) { + let mut client = self.client.clone(); + block_on(client.set_active(SetActiveRequest { + tag_id: Some(self.id), + set_or_toggle: Some(tag::v0alpha1::set_active_request::SetOrToggle::Toggle(())), + })) + .unwrap(); + } + + /// Remove this tag from its output. + /// + /// # Examples + /// + /// ``` + /// let tags = tag + /// .add(output.get_by_name("DP-1")?, ["1", "2", "Buckle", "Shoe"]) + /// .collect::>; + /// + /// tags[1].remove(); + /// tags[3].remove(); + /// // "DP-1" now only has tags "1" and "Buckle" + /// ``` + pub fn remove(mut self) { + block_on(self.client.remove(RemoveRequest { + tag_ids: vec![self.id], + })) + .unwrap(); + } + + /// Set this tag's layout. + /// + /// Layouting only applies to tiled windows (windows that are not floating, maximized, or + /// fullscreen). If multiple tags are active on an output, the first active tag's layout will + /// determine the layout strategy. + /// + /// See [`Layout`] for the different static layouts Pinnacle currently has to offer. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::tag::Layout; + /// + /// // Set the layout of tag "1" on the focused output to "corner top left". + /// tag.get("1", None)?.set_layout(Layout::CornerTopLeft); + /// ``` + pub fn set_layout(&self, layout: Layout) { + let mut client = self.client.clone(); + block_on(client.set_layout(SetLayoutRequest { + tag_id: Some(self.id), + layout: Some(layout as i32), + })) + .unwrap(); + } + + /// Get all properties of this tag. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::tag::TagProperties; + /// + /// let TagProperties { + /// active, + /// name, + /// output, + /// } = tag.get("1", None)?.props(); + /// ``` + pub fn props(&self) -> TagProperties { + let mut client = self.client.clone(); + let output_client = self.output_client.clone(); + + let response = block_on(client.get_properties(tag::v0alpha1::GetPropertiesRequest { + tag_id: Some(self.id), + })) + .unwrap() + .into_inner(); + + TagProperties { + active: response.active, + name: response.name, + output: response.output_name.map(|name| OutputHandle { + client: output_client, + tag_client: client, + name, + }), + } + } + + /// Get this tag's active status. + /// + /// Shorthand for `self.props().active`. + pub fn active(&self) -> Option { + self.props().active + } + + /// Get this tag's name. + /// + /// Shorthand for `self.props().name`. + pub fn name(&self) -> Option { + self.props().name + } + + /// Get a handle to the output this tag is on. + /// + /// Shorthand for `self.props().output`. + pub fn output(&self) -> Option { + self.props().output + } +} + +/// Properties of a tag. +pub struct TagProperties { + /// Whether the tag is active or not + pub active: Option, + /// The name of the tag + pub name: Option, + /// The output the tag is on + pub output: Option, +} diff --git a/api/rust_grpc/src/util.rs b/api/rust/src/util.rs similarity index 100% rename from api/rust_grpc/src/util.rs rename to api/rust/src/util.rs diff --git a/api/rust/src/window.rs b/api/rust/src/window.rs index 8423ebc..c120ce8 100644 --- a/api/rust/src/window.rs +++ b/api/rust/src/window.rs @@ -1,206 +1,530 @@ //! Window management. +//! +//! This module provides [`Window`], which allows you to get [`WindowHandle`]s and move and resize +//! windows using the mouse. +//! +//! [`WindowHandle`]s allow you to do things like resize and move windows, toggle them between +//! floating and tiled, close them, and more. +//! +//! This module also allows you to set window rules; see the [rules] module for more information. + +use futures::executor::block_on; +use num_enum::TryFromPrimitive; +use pinnacle_api_defs::pinnacle::{ + output::v0alpha1::output_service_client::OutputServiceClient, + tag::v0alpha1::tag_service_client::TagServiceClient, + window::v0alpha1::{ + window_service_client::WindowServiceClient, AddWindowRuleRequest, CloseRequest, + MoveToTagRequest, SetTagRequest, + }, + window::{ + self, + v0alpha1::{ + GetRequest, MoveGrabRequest, ResizeGrabRequest, SetFloatingRequest, + SetFullscreenRequest, SetMaximizedRequest, + }, + }, +}; +use tonic::transport::Channel; + +use crate::{input::MouseButton, tag::TagHandle, util::Geometry}; + +use self::rules::{WindowRule, WindowRuleCondition}; pub mod rules; -use crate::{ - input::MouseButton, - msg::{Msg, Request, RequestResponse}, - request, send_msg, - tag::TagHandle, -}; - -/// A unique identifier for each window. -#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub(crate) enum WindowId { - /// A config API returned an invalid window. It should be using this variant. - None, - /// A valid window id. - #[serde(untagged)] - Some(u32), -} - -/// Get all windows with the class `class`. -pub fn get_by_class(class: &str) -> impl Iterator + '_ { - get_all().filter(|win| win.properties().class.as_deref() == Some(class)) -} - -/// Get the currently focused window, or `None` if there isn't one. -pub fn get_focused() -> Option { - get_all().find(|win| win.properties().focused.is_some_and(|focused| focused)) -} - -/// Get all windows. -pub fn get_all() -> impl Iterator { - let RequestResponse::Windows { window_ids } = request(Request::GetWindows) else { - unreachable!() - }; - - window_ids.into_iter().map(WindowHandle) -} - -/// Begin a window move. +/// A struct containing methods that get [`WindowHandle`]s and move windows with the mouse. /// -/// This will start a window move grab with the provided button on the window the pointer -/// is currently hovering over. Once `button` is let go, the move will end. -pub fn begin_move(button: MouseButton) { - let msg = Msg::WindowMoveGrab { - button: button as u32, - }; - - send_msg(msg).unwrap(); +/// See [`WindowHandle`] for more information. +#[derive(Debug, Clone)] +pub struct Window { + channel: Channel, } -/// Begin a window resize. -/// -/// This will start a window resize grab with the provided button on the window the -/// pointer is currently hovering over. Once `button` is let go, the resize will end. -pub fn begin_resize(button: MouseButton) { - let msg = Msg::WindowResizeGrab { - button: button as u32, - }; - - send_msg(msg).unwrap(); -} - -/// A handle to a window. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct WindowHandle(WindowId); - -/// Properties of a window, retrieved through [`WindowHandle::properties`]. -#[derive(Debug)] -pub struct WindowProperties { - /// The size of the window, in pixels. - pub size: Option<(i32, i32)>, - /// The location of the window in the global space. - pub loc: Option<(i32, i32)>, - /// The window's class. - pub class: Option, - /// The window's title. - pub title: Option, - /// Whether or not the window is focused. - pub focused: Option, - /// Whether or not the window is floating. - pub floating: Option, - /// Whether the window is fullscreen, maximized, or neither. - pub fullscreen_or_maximized: Option, -} - -impl WindowHandle { - /// Toggle this window between floating and tiled. - pub fn toggle_floating(&self) { - send_msg(Msg::ToggleFloating { window_id: self.0 }).unwrap(); +impl Window { + pub(crate) fn new(channel: Channel) -> Self { + Self { channel } } - /// Toggle this window's fullscreen status. + fn create_window_client(&self) -> WindowServiceClient { + WindowServiceClient::new(self.channel.clone()) + } + + fn create_tag_client(&self) -> TagServiceClient { + TagServiceClient::new(self.channel.clone()) + } + + fn create_output_client(&self) -> OutputServiceClient { + OutputServiceClient::new(self.channel.clone()) + } + + /// Start moving the window with the mouse. /// - /// If used while not fullscreen, it becomes fullscreen. - /// If used while fullscreen, it becomes unfullscreen. - /// If used while maximized, it becomes fullscreen. - pub fn toggle_fullscreen(&self) { - send_msg(Msg::ToggleFullscreen { window_id: self.0 }).unwrap(); - } - - /// Toggle this window's maximized status. + /// This will begin moving the window under the pointer using the specified [`MouseButton`]. + /// The button must be held down at the time this method is called for the move to start. /// - /// If used while not maximized, it becomes maximized. - /// If used while maximized, it becomes unmaximized. - /// If used while fullscreen, it becomes maximized. - pub fn toggle_maximized(&self) { - send_msg(Msg::ToggleMaximized { window_id: self.0 }).unwrap(); - } - - /// Set this window's size. None parameters will be ignored. - pub fn set_size(&self, width: Option, height: Option) { - send_msg(Msg::SetWindowSize { - window_id: self.0, - width, - height, - }) + /// This is intended to be used with [`Input::keybind`][crate::input::Input::keybind]. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::input::{Mod, MouseButton, MouseEdge}; + /// + /// // Set `Super + left click` to begin moving a window + /// input.mousebind([Mod::Super], MouseButton::Left, MouseEdge::Press, || { + /// window.begin_move(MouseButton::Left); + /// }); + /// ``` + pub fn begin_move(&self, button: MouseButton) { + let mut client = self.create_window_client(); + block_on(client.move_grab(MoveGrabRequest { + button: Some(button as u32), + })) .unwrap(); } - /// Send a close event to this window. - pub fn close(&self) { - send_msg(Msg::CloseWindow { window_id: self.0 }).unwrap(); + /// Start resizing the window with the mouse. + /// + /// This will begin resizing the window under the pointer using the specified [`MouseButton`]. + /// The button must be held down at the time this method is called for the resize to start. + /// + /// This is intended to be used with [`Input::keybind`][crate::input::Input::keybind]. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::input::{Mod, MouseButton, MouseEdge}; + /// + /// // Set `Super + right click` to begin moving a window + /// input.mousebind([Mod::Super], MouseButton::Right, MouseEdge::Press, || { + /// window.begin_resize(MouseButton::Right); + /// }); + /// ``` + pub fn begin_resize(&self, button: MouseButton) { + let mut client = self.create_window_client(); + block_on(client.resize_grab(ResizeGrabRequest { + button: Some(button as u32), + })) + .unwrap(); } - /// Get this window's [`WindowProperties`]. - pub fn properties(&self) -> WindowProperties { - let RequestResponse::WindowProps { - size, - loc, - class, - title, - focused, - floating, - fullscreen_or_maximized, - } = request(Request::GetWindowProps { window_id: self.0 }) - else { - unreachable!() - }; + /// Get all windows. + /// + /// # Examples + /// + /// ``` + /// let windows = window.get_all(); + /// ``` + pub fn get_all(&self) -> impl Iterator { + let mut client = self.create_window_client(); + let tag_client = self.create_tag_client(); + let output_client = self.create_output_client(); + block_on(client.get(GetRequest {})) + .unwrap() + .into_inner() + .window_ids + .into_iter() + .map(move |id| WindowHandle { + client: client.clone(), + id, + tag_client: tag_client.clone(), + output_client: output_client.clone(), + }) + } + + /// Get the currently focused window. + /// + /// # Examples + /// + /// ``` + /// let focused_window = window.get_focused()?; + /// ``` + pub fn get_focused(&self) -> Option { + self.get_all() + .find(|window| matches!(window.props().focused, Some(true))) + } + + /// Add a window rule. + /// + /// A window rule is a set of criteria that a window must open with. + /// For it to apply, a [`WindowRuleCondition`] must evaluate to true for the window in question. + /// + /// TODO: + pub fn add_window_rule(&self, cond: WindowRuleCondition, rule: WindowRule) { + let mut client = self.create_window_client(); + + block_on(client.add_window_rule(AddWindowRuleRequest { + cond: Some(cond.0), + rule: Some(rule.0), + })) + .unwrap(); + } +} + +/// A handle to a window. +/// +/// This allows you to manipulate the window and get its properties. +#[derive(Debug, Clone)] +pub struct WindowHandle { + pub(crate) client: WindowServiceClient, + pub(crate) id: u32, + pub(crate) tag_client: TagServiceClient, + pub(crate) output_client: OutputServiceClient, +} + +/// Whether a window is fullscreen, maximized, or neither. +#[repr(i32)] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, TryFromPrimitive)] +pub enum FullscreenOrMaximized { + /// The window is neither fullscreen nor maximized + Neither = 1, + /// The window is fullscreen + Fullscreen, + /// The window is maximized + Maximized, +} + +/// Properties of a window. +#[derive(Debug, Clone)] +pub struct WindowProperties { + /// The location and size of the window + pub geometry: Option, + /// The window's class + pub class: Option, + /// The window's title + pub title: Option, + /// Whether the window is focused or not + pub focused: Option, + /// Whether the window is floating or not + /// + /// Note that a window can still be floating even if it's fullscreen or maximized; those two + /// state will just override the floating state. + pub floating: Option, + /// Whether the window is fullscreen, maximized, or neither + pub fullscreen_or_maximized: Option, + /// All the tags on the window + pub tags: Vec, +} + +impl WindowHandle { + /// Send a close request to this window. + /// + /// If the window is unresponsive, it may not close. + /// + /// # Examples + /// + /// ``` + /// // Close the focused window + /// window.get_focused()?.close() + /// ``` + pub fn close(mut self) { + block_on(self.client.close(CloseRequest { + window_id: Some(self.id), + })) + .unwrap(); + } + + /// Set this window to fullscreen or not. + /// + /// If it is maximized, setting it to fullscreen will remove the maximized state. + /// + /// # Examples + /// + /// ``` + /// // Set the focused window to fullscreen. + /// window.get_focused()?.set_fullscreen(true); + /// ``` + pub fn set_fullscreen(&self, set: bool) { + let mut client = self.client.clone(); + block_on(client.set_fullscreen(SetFullscreenRequest { + window_id: Some(self.id), + set_or_toggle: Some(window::v0alpha1::set_fullscreen_request::SetOrToggle::Set( + set, + )), + })) + .unwrap(); + } + + /// Toggle this window between fullscreen and not. + /// + /// If it is maximized, toggling it to fullscreen will remove the maximized state. + /// + /// # Examples + /// + /// ``` + /// // Toggle the focused window to and from fullscreen. + /// window.get_focused()?.toggle_fullscreen(); + /// ``` + pub fn toggle_fullscreen(&self) { + let mut client = self.client.clone(); + block_on(client.set_fullscreen(SetFullscreenRequest { + window_id: Some(self.id), + set_or_toggle: Some(window::v0alpha1::set_fullscreen_request::SetOrToggle::Toggle(())), + })) + .unwrap(); + } + + /// Set this window to maximized or not. + /// + /// If it is fullscreen, setting it to maximized will remove the fullscreen state. + /// + /// # Examples + /// + /// ``` + /// // Set the focused window to maximized. + /// window.get_focused()?.set_maximized(true); + /// ``` + pub fn set_maximized(&self, set: bool) { + let mut client = self.client.clone(); + block_on(client.set_maximized(SetMaximizedRequest { + window_id: Some(self.id), + set_or_toggle: Some(window::v0alpha1::set_maximized_request::SetOrToggle::Set( + set, + )), + })) + .unwrap(); + } + + /// Toggle this window between maximized and not. + /// + /// If it is fullscreen, setting it to maximized will remove the fullscreen state. + /// + /// # Examples + /// + /// ``` + /// // Toggle the focused window to and from maximized. + /// window.get_focused()?.toggle_maximized(); + /// ``` + pub fn toggle_maximized(&self) { + let mut client = self.client.clone(); + block_on(client.set_maximized(SetMaximizedRequest { + window_id: Some(self.id), + set_or_toggle: Some(window::v0alpha1::set_maximized_request::SetOrToggle::Toggle(())), + })) + .unwrap(); + } + + /// Set this window to floating or not. + /// + /// Floating windows will not be tiled and can be moved around and resized freely. + /// + /// Note that fullscreen and maximized windows can still be floating; those two states will + /// just override the floating state. + /// + /// # Examples + /// + /// ``` + /// // Set the focused window to floating. + /// window.get_focused()?.set_floating(true); + /// ``` + pub fn set_floating(&self, set: bool) { + let mut client = self.client.clone(); + block_on(client.set_floating(SetFloatingRequest { + window_id: Some(self.id), + set_or_toggle: Some(window::v0alpha1::set_floating_request::SetOrToggle::Set( + set, + )), + })) + .unwrap(); + } + + /// Toggle this window to and from floating. + /// + /// Floating windows will not be tiled and can be moved around and resized freely. + /// + /// Note that fullscreen and maximized windows can still be floating; those two states will + /// just override the floating state. + /// + /// # Examples + /// + /// ``` + /// // Toggle the focused window to and from floating. + /// window.get_focused()?.toggle_floating(); + /// ``` + pub fn toggle_floating(&self) { + let mut client = self.client.clone(); + block_on(client.set_floating(SetFloatingRequest { + window_id: Some(self.id), + set_or_toggle: Some(window::v0alpha1::set_floating_request::SetOrToggle::Toggle( + (), + )), + })) + .unwrap(); + } + + /// Move this window to the given `tag`. + /// + /// This will remove all tags from this window then tag it with `tag`, essentially moving the + /// window to that tag. + /// + /// # Examples + /// + /// ``` + /// // Move the focused window to tag "Code" on the focused output + /// window.get_focused()?.move_to_tag(&tag.get("Code", None)?); + /// ``` + pub fn move_to_tag(&self, tag: &TagHandle) { + let mut client = self.client.clone(); + + block_on(client.move_to_tag(MoveToTagRequest { + window_id: Some(self.id), + tag_id: Some(tag.id), + })) + .unwrap(); + } + + /// Set or unset a tag on this window. + /// + /// # Examples + /// + /// ``` + /// let focused = window.get_focused()?; + /// let tg = tag.get("Potato", None)?; + /// + /// focused.set_tag(&tg, true); // `focused` now has tag "Potato" + /// focused.set_tag(&tg, false); // `focused` no longer has tag "Potato" + /// ``` + pub fn set_tag(&self, tag: &TagHandle, set: bool) { + let mut client = self.client.clone(); + + block_on(client.set_tag(SetTagRequest { + window_id: Some(self.id), + tag_id: Some(tag.id), + set_or_toggle: Some(window::v0alpha1::set_tag_request::SetOrToggle::Set(set)), + })) + .unwrap(); + } + + /// Toggle a tag on this window. + /// + /// # Examples + /// + /// ``` + /// let focused = window.get_focused()?; + /// let tg = tag.get("Potato", None)?; + /// + /// // Assume `focused` does not have tag `tg` + /// + /// focused.toggle_tag(&tg); // `focused` now has tag "Potato" + /// focused.toggle_tag(&tg); // `focused` no longer has tag "Potato" + /// ``` + pub fn toggle_tag(&self, tag: &TagHandle) { + let mut client = self.client.clone(); + + block_on(client.set_tag(SetTagRequest { + window_id: Some(self.id), + tag_id: Some(tag.id), + set_or_toggle: Some(window::v0alpha1::set_tag_request::SetOrToggle::Toggle(())), + })) + .unwrap(); + } + + /// Get all properties of this window. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::WindowProperties; + /// + /// let WindowProperties { + /// geometry, + /// class, + /// title, + /// focused, + /// floating, + /// fullscreen_or_maximized, + /// tags, + /// } = window.get_focused()?.props(); + /// ``` + pub fn props(&self) -> WindowProperties { + let mut client = self.client.clone(); + let tag_client = self.tag_client.clone(); + let response = block_on( + client.get_properties(window::v0alpha1::GetPropertiesRequest { + window_id: Some(self.id), + }), + ) + .unwrap() + .into_inner(); + + let fullscreen_or_maximized = response + .fullscreen_or_maximized + .unwrap_or_default() + .try_into() + .ok(); + + let geometry = response.geometry.map(|geo| Geometry { + x: geo.x(), + y: geo.y(), + width: geo.width() as u32, + height: geo.height() as u32, + }); WindowProperties { - size, - loc, - class, - title, - focused, - floating, + geometry, + class: response.class, + title: response.title, + focused: response.focused, + floating: response.floating, fullscreen_or_maximized, + tags: response + .tag_ids + .into_iter() + .map(|id| TagHandle { + client: tag_client.clone(), + output_client: self.output_client.clone(), + id, + }) + .collect(), } } - /// Toggle `tag` on this window. - pub fn toggle_tag(&self, tag: &TagHandle) { - let msg = Msg::ToggleTagOnWindow { - window_id: self.0, - tag_id: tag.0, - }; - - send_msg(msg).unwrap(); + /// Get this window's location and size. + /// + /// Shorthand for `self.props().geometry`. + pub fn geometry(&self) -> Option { + self.props().geometry } - /// Move this window to `tag`. + /// Get this window's class. /// - /// This will remove all other tags on this window. - pub fn move_to_tag(&self, tag: &TagHandle) { - let msg = Msg::MoveWindowToTag { - window_id: self.0, - tag_id: tag.0, - }; + /// Shorthand for `self.props().class`. + pub fn class(&self) -> Option { + self.props().class + } - send_msg(msg).unwrap(); + /// Get this window's title. + /// + /// Shorthand for `self.props().title`. + pub fn title(&self) -> Option { + self.props().title + } + + /// Get whether or not this window is focused. + /// + /// Shorthand for `self.props().focused`. + pub fn focused(&self) -> Option { + self.props().focused + } + + /// Get whether or not this window is floating. + /// + /// Shorthand for `self.props().floating`. + pub fn floating(&self) -> Option { + self.props().floating + } + + /// Get whether this window is fullscreen, maximized, or neither. + /// + /// Shorthand for `self.props().fullscreen_or_maximized`. + pub fn fullscreen_or_maximized(&self) -> Option { + self.props().fullscreen_or_maximized + } + + /// Get all the tags on this window. + /// + /// Shorthand for `self.props().tags`. + pub fn tags(&self) -> Vec { + self.props().tags } } - -/// Whether or not a window is floating or tiled. -#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize)] -pub enum FloatingOrTiled { - /// The window is floating. - /// - /// It can be freely moved around and resized and will not respond to layouts. - Floating, - /// The window is tiled. - /// - /// It cannot be resized and can only move by swapping places with other tiled windows. - Tiled, -} - -/// Whether the window is fullscreen, maximized, or neither. -/// -/// These three states are mutually exclusive. Setting a window to maximized while it is fullscreen -/// will make it stop being fullscreen and vice versa. -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub enum FullscreenOrMaximized { - /// The window is not fullscreen or maximized. - Neither, - /// The window is fullscreen. - /// - /// It will be the only rendered window on screen and will fill the output it resides on. - /// Layer surfaces will also not be rendered while a window is fullscreen. - Fullscreen, - /// The window is maximized. - /// - /// It will fill up as much space on its output as it can, respecting any layer surfaces. - Maximized, -} diff --git a/api/rust/src/window/rules.rs b/api/rust/src/window/rules.rs index e2b2d17..86e5a0a 100644 --- a/api/rust/src/window/rules.rs +++ b/api/rust/src/window/rules.rs @@ -1,83 +1,222 @@ -//! Window rules. +//! Types for window rules. +//! +//! A window rule is a way to set the properties of a window on open. +//! +//! They are comprised of two parts: the [condition][WindowRuleCondition] and the actual [rule][WindowRule]. +//! +//! # [`WindowRuleCondition`]s +//! `WindowRuleCondition`s are conditions that the window needs to open with in order to apply a +//! rule. For example, you may want to set a window to maximized if it has the class "steam", or +//! you might want to open all Firefox instances on tag "3". +//! +//! To do this, you must build a `WindowRuleCondition` to tell the compositor when to apply any +//! rules. +//! +//! ## Building `WindowRuleCondition`s +//! A condition is created through [`WindowRuleCondition::new`]: +//! ``` +//! let cond = WindowRuleCondition::new(); +//! ``` +//! +//! In order to understand conditions, you must understand the concept of "any" and "all". +//! +//! **"Any"** +//! +//! "Any" conditions only need one of their constituent items to be true for the whole condition to +//! evaluate to true. Think of it as one big `if a || b || c || d || ... {}` block. +//! +//! **"All"** +//! +//! "All" conditions need *all* of their constituent items to be true for the condition to evaluate +//! to true. This is like a big `if a && b && c && d && ... {}` block. +//! +//! Note that any items in a top level `WindowRuleCondition` fall under "all", so all those items +//! must be true. +//! +//! With that out of the way, we can get started building conditions. +//! +//! ### `WindowRuleCondition::classes` +//! With [`WindowRuleCondition::classes`], you can specify what classes a window needs to have for +//! a rule to apply. +//! +//! The following will apply to windows with the class "firefox": +//! ``` +//! let cond = WindowRuleCondition::new().classes(["firefox"]); +//! ``` +//! +//! Note that you pass in some `impl IntoIterator>`. This means you can +//! pass in more than one class here: +//! ``` +//! let failing_cond = WindowRuleCondition::new().classes(["firefox", "steam"]); +//! ``` +//! *HOWEVER*: this will not work. Recall that top level conditions are implicitly "all". This +//! means the above would require windows to have *both classes*, which is impossible. Thus, the +//! condition above will never be true. +//! +//! ### `WindowRuleCondition::titles` +//! Like `classes`, you can use `titles` to specify that the window needs to open with a specific +//! title for the condition to apply. +//! +//! ``` +//! let cond = WindowRuleCondition::new().titles(["Steam"]); +//! ``` +//! +//! Like `classes`, passing in multiple titles at the top level will cause the condition to always +//! fail. +//! +//! ### `WindowRuleCondition::tags` +//! You can specify that the window needs to open on the given tags in order to apply a rule. +//! +//! ``` +//! let cond = WindowRuleCondition::new().tags([&tag.get("3", output.get_by_name("HDMI-1")?)?]); +//! ``` +//! +//! Here, if you have tag "3" active on "HDMI-1" and spawn a window on that output, this condition +//! will apply. +//! +//! Unlike `classes` and `titles`, you can specify multiple tags at the top level: +//! +//! ``` +//! let op = output.get_by_name("HDMI-1")?; +//! let tag1 = tag.get("1", &op)?; +//! let tag2 = tag.get("2", &op)?; +//! +//! let cond = WindowRuleCondition::new().tags([&tag1, &tag2]); +//! ``` +//! +//! Now, you must have both tags "1" and "2" active and spawn a window for the condition to apply. +//! +//! ### `WindowRuleCondition::any` +//! Now we can get to ways to compose more complex conditions. +//! +//! `WindowRuleCondition::any` takes in conditions and will evaluate to true if *anything* in those +//! conditions are true. +//! +//! ``` +//! let cond = WindowRuleCondition::new() +//! .any([ +//! WindowRuleCondition::new().classes(["Alacritty"]), +//! WindowRuleCondition::new().tags([&tag.get("2", None)?]), +//! ]); +//! ``` +//! +//! This condition will apply if the window is *either* "Alacritty" *or* opens on tag "2". +//! +//! ### `WindowRuleCondition::all` +//! With `WindowRuleCondition::all`, *all* specified conditions must be true for the condition to +//! be true. +//! +//! ``` +//! let cond = WindowRuleCondition::new() +//! .all([ +//! WindowRuleCondition::new().classes(["Alacritty"]), +//! WindowRuleCondition::new().tags([&tag.get("2", None)?]), +//! ]); +//! ``` +//! +//! This condition applies if the window has the class "Alacritty" *and* opens on tag "2". +//! +//! You can write the above a bit shorter, as top level conditions are already "all": +//! +//! ``` +//! let cond = WindowRuleCondition::new() +//! .classes(["Alacritty"]) +//! .tags([&tag.get("2", None)?]); +//! ``` +//! +//! ## Complex condition composition +//! You can arbitrarily nest `any` and `all` to achieve desired logic. +//! +//! ``` +//! let op = output.get_by_name("HDMI-1")?; +//! let tag1 = tag.get("1", &op)?; +//! let tag2 = tag.get("2", &op)?; +//! +//! let complex_cond = WindowRuleCondition::new() +//! .any([ +//! WindowRuleCondition::new().all([ +//! WindowRuleCondition::new() +//! .classes("Alacritty") +//! .tags([&tag1, &tag2]) +//! ]), +//! WindowRuleCondition::new().all([ +//! WindowRuleCondition::new().any([ +//! WindowRuleCondition::new().titles(["nvim", "emacs", "nano"]), +//! ]), +//! WindowRuleCondition::new().any([ +//! WindowRuleCondition::new().tags([&tag1, &tag2]), +//! ]), +//! ]) +//! ]) +//! ``` +//! +//! The above is true if either of the following are true: +//! - The window has class "Alacritty" and opens on both tags "1" and "2", or +//! - The window's class is either "nvim", "emacs", or "nano" *and* it opens on either tag "1" or +//! "2". +//! +//! # [`WindowRule`]s +//! `WindowRuleCondition`s are half of a window rule. The other half is the [`WindowRule`] itself. +//! +//! A `WindowRule` is what will apply to a window if a condition is true. +//! +//! ## Building `WindowRule`s +//! +//! Create a new window rule with [`WindowRule::new`]: +//! +//! ``` +//! let rule = WindowRule::new(); +//! ``` +//! +//! There are several rules you can set currently. +//! +//! ### [`WindowRule::output`] +//! This will cause the window to open on the specified output. +//! +//! ### [`WindowRule::tags`] +//! This will cause the window to open with the given tags. +//! +//! ### [`WindowRule::floating`] +//! This will cause the window to open either floating or tiled. +//! +//! ### [`WindowRule::fullscreen_or_maximized`] +//! This will cause the window to open either fullscreen, maximized, or neither. +//! +//! ### [`WindowRule::x`] +//! This will cause the window to open at the given x-coordinate. +//! +//! Note: this only applies to floating windows; tiled windows' geometry will be overridden by +//! layouting. +//! +//! ### [`WindowRule::y`] +//! This will cause the window to open at the given y-coordinate. +//! +//! Note: this only applies to floating windows; tiled windows' geometry will be overridden by +//! layouting. +//! +//! ### [`WindowRule::width`] +//! This will cause the window to open with the given width in pixels. +//! +//! Note: this only applies to floating windows; tiled windows' geometry will be overridden by +//! layouting. +//! +//! ### [`WindowRule::height`] +//! This will cause the window to open with the given height in pixels. +//! +//! Note: this only applies to floating windows; tiled windows' geometry will be overridden by +//! layouting. -use std::num::NonZeroU32; +use pinnacle_api_defs::pinnacle::window; -use crate::{msg::Msg, output::OutputHandle, send_msg, tag::TagHandle}; +use crate::{output::OutputHandle, tag::TagHandle}; -use super::{FloatingOrTiled, FullscreenOrMaximized}; - -/// Add a window rule. -pub fn add(cond: WindowRuleCondition, rule: WindowRule) { - let msg = Msg::AddWindowRule { - cond: cond.0, - rule: rule.0, - }; - - send_msg(msg).unwrap(); -} - -/// A window rule. -/// -/// This is what will be applied to a window if it meets a [`WindowRuleCondition`]. -/// -/// `WindowRule`s are built using the builder pattern. -/// // TODO: show example -#[derive(Default)] -pub struct WindowRule(crate::msg::WindowRule); - -impl WindowRule { - /// Create a new, empty window rule. - pub fn new() -> Self { - Default::default() - } - - /// This rule will force windows to open on the provided `output`. - pub fn output(mut self, output: &OutputHandle) -> Self { - self.0.output = Some(output.0.clone()); - self - } - - /// This rule will force windows to open with the provided `tags`. - pub fn tags(mut self, tags: &[TagHandle]) -> Self { - self.0.tags = Some(tags.iter().map(|tag| tag.0).collect()); - self - } - - /// This rule will force windows to open either floating or tiled. - pub fn floating_or_tiled(mut self, floating_or_tiled: FloatingOrTiled) -> Self { - self.0.floating_or_tiled = Some(floating_or_tiled); - self - } - - /// This rule will force windows to open either fullscreen, maximized, or neither. - pub fn fullscreen_or_maximized( - mut self, - fullscreen_or_maximized: FullscreenOrMaximized, - ) -> Self { - self.0.fullscreen_or_maximized = Some(fullscreen_or_maximized); - self - } - - /// This rule will force windows to open with a specific size. - /// - /// This will only actually be visible if the window is also floating. - pub fn size(mut self, width: NonZeroU32, height: NonZeroU32) -> Self { - self.0.size = Some((width, height)); - self - } - - /// This rule will force windows to open at a specific location. - /// - /// This will only actually be visible if the window is also floating. - pub fn location(mut self, x: i32, y: i32) -> Self { - self.0.location = Some((x, y)); - self - } -} +use super::FullscreenOrMaximized; /// A condition for a [`WindowRule`] to apply to a window. -#[derive(Default, Debug)] -pub struct WindowRuleCondition(crate::msg::WindowRuleCondition); +/// +/// `WindowRuleCondition`s are built using the builder pattern. +#[derive(Default, Debug, Clone)] +pub struct WindowRuleCondition(pub(super) window::v0alpha1::WindowRuleCondition); impl WindowRuleCondition { /// Create a new, empty `WindowRuleCondition`. @@ -86,14 +225,41 @@ impl WindowRuleCondition { } /// This condition requires that at least one provided condition is true. - pub fn any(mut self, conds: &[WindowRuleCondition]) -> Self { - self.0.cond_any = Some(conds.iter().map(|cond| cond.0.clone()).collect()); + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRuleCondition; + /// + /// // `cond` will be true if the window opens with *either* class "Alacritty" or "firefox" + /// // *or* with title "Steam" + /// let cond = WindowRuleCondition::new() + /// .any([ + /// WindowRuleCondition::new().classes(["Alacritty", "firefox"]), + /// WindowRuleCondition::new().titles(["Steam"]). + /// ]); + /// ``` + pub fn any(mut self, conds: impl IntoIterator) -> Self { + self.0.any = conds.into_iter().map(|cond| cond.0).collect(); self } /// This condition requires that all provided conditions are true. - pub fn all(mut self, conds: &[WindowRuleCondition]) -> Self { - self.0.cond_all = Some(conds.iter().map(|cond| cond.0.clone()).collect()); + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRuleCondition; + /// + /// // `cond` will be true if the window opens with class "Alacritty" *and* on tag "1" + /// let cond = WindowRuleCondition::new() + /// .any([ + /// WindowRuleCondition::new().tags([tag.get("1", None)?]), + /// WindowRuleCondition::new().titles(["Alacritty"]). + /// ]); + /// ``` + pub fn all(mut self, conds: impl IntoIterator) -> Self { + self.0.all = conds.into_iter().map(|cond| cond.0).collect(); self } @@ -104,8 +270,26 @@ impl WindowRuleCondition { /// /// When used in [`WindowRuleCondition::any`], at least one of the /// provided classes must match. - pub fn class(mut self, classes: &[&str]) -> Self { - self.0.class = Some(classes.iter().map(|s| s.to_string()).collect()); + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRuleCondition; + /// + /// // `cond` will be true if the window opens with class "Alacritty" + /// let cond = WindowRuleCondition::new().classes(["Alacritty"]); + /// + /// // Top level conditions need all items to be true, + /// // so the following will never be true as windows can't have two classes at once + /// let always_false = WindowRuleCondition::new().classes(["Alacritty", "firefox"]); + /// + /// // To make the above work, use [`WindowRuleCondition::any`]. + /// // The following will be true if the window is "Alacritty" or "firefox" + /// let any_class = WindowRuleCondition::new() + /// .any([ WindowRuleCondition::new().classes(["Alacritty", "firefox"]) ]); + /// ``` + pub fn classes(mut self, classes: impl IntoIterator>) -> Self { + self.0.classes = classes.into_iter().map(Into::into).collect(); self } @@ -116,8 +300,26 @@ impl WindowRuleCondition { /// /// When used in [`WindowRuleCondition::any`], at least one of the /// provided titles must match. - pub fn title(mut self, titles: &[&str]) -> Self { - self.0.title = Some(titles.iter().map(|s| s.to_string()).collect()); + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRuleCondition; + /// + /// // `cond` will be true if the window opens with title "vim" + /// let cond = WindowRuleCondition::new().titles(["vim"]); + /// + /// // Top level conditions need all items to be true, + /// // so the following will never be true as windows can't have two titles at once + /// let always_false = WindowRuleCondition::new().titles(["vim", "emacs"]); + /// + /// // To make the above work, use [`WindowRuleCondition::any`]. + /// // The following will be true if the window has the title "vim" or "emacs" + /// let any_title = WindowRuleCondition::new() + /// .any([WindowRuleCondition::new().titles(["vim", "emacs"])]); + /// ``` + pub fn titles(mut self, titles: impl IntoIterator>) -> Self { + self.0.titles = titles.into_iter().map(Into::into).collect(); self } @@ -128,8 +330,192 @@ impl WindowRuleCondition { /// /// When used in [`WindowRuleCondition::any`], the window must open on at least /// one of the given tags. - pub fn tag(mut self, tags: &[TagHandle]) -> Self { - self.0.tag = Some(tags.iter().map(|tag| tag.0).collect()); + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRuleCondition; + /// + /// let tag1 = tag.get("1", None)?; + /// let tag2 = tag.get("2", None)?; + /// + /// // `cond` will be true if the window opens with tag "1" + /// let cond = WindowRuleCondition::new().tags([&tag1]); + /// + /// // Top level conditions need all items to be true, + /// // so the following will be true if the window opens with both tags "1" and "2" + /// let all_tags = WindowRuleCondition::new().tags([&tag1, &tag2]); + /// + /// // This does the same as the above + /// let all_tags = WindowRuleCondition::new() + /// .all([WindowRuleCondition::new().tags([&tag1, &tag2])]); + /// + /// // The following will be true if the window opens with *either* tag "1" or "2" + /// let any_tag = WindowRuleCondition::new() + /// .any([WindowRuleCondition::new().tags([&tag1, &tag2])]); + /// ``` + pub fn tags<'a>(mut self, tags: impl IntoIterator) -> Self { + self.0.tags = tags.into_iter().map(|tag| tag.id).collect(); + self + } +} + +/// A window rule. +/// +/// This is what will be applied to a window if it meets a [`WindowRuleCondition`]. +/// +/// `WindowRule`s are built using the builder pattern. +#[derive(Clone, Debug, Default)] +pub struct WindowRule(pub(super) window::v0alpha1::WindowRule); + +impl WindowRule { + /// Create a new, empty window rule. + pub fn new() -> Self { + Default::default() + } + + /// This rule will force windows to open on the provided `output`. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRule; + /// + /// // Force the window to open on "HDMI-1" + /// let rule = WindowRule::new().output(output.get_by_name("HDMI-1")?); + /// ``` + pub fn output(mut self, output: &OutputHandle) -> Self { + self.0.output = Some(output.name.clone()); + self + } + + /// This rule will force windows to open with the provided `tags`. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRule; + /// + /// let op = output.get_by_name("HDMI-1")?; + /// let tag1 = tag.get("1", &op)?; + /// let tag2 = tag.get("2", &op)?; + /// + /// // Force the window to open with tags "1" and "2" + /// let rule = WindowRule::new().tags([&tag1, &tag2]); + /// ``` + pub fn tags<'a>(mut self, tags: impl IntoIterator) -> Self { + self.0.tags = tags.into_iter().map(|tag| tag.id).collect(); + self + } + + /// This rule will force windows to open either floating or not. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRule; + /// + /// // Force the window to open floating + /// let rule = WindowRule::new().floating(true); + /// + /// // Force the window to open tiled + /// let rule = WindowRule::new().floating(false); + /// ``` + pub fn floating(mut self, floating: bool) -> Self { + self.0.floating = Some(floating); + self + } + + /// This rule will force windows to open either fullscreen, maximized, or neither. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRule; + /// use pinnacle_api::window::FullscreenOrMaximized; + /// + /// // Force the window to open fullscreen + /// let rule = WindowRule::new().fullscreen_or_maximized(FullscreenOrMaximized::Fullscreen); + /// + /// // Force the window to open maximized + /// let rule = WindowRule::new().fullscreen_or_maximized(FullscreenOrMaximized::Maximized); + /// + /// // Force the window to open not fullscreen nor maximized + /// let rule = WindowRule::new().fullscreen_or_maximized(FullscreenOrMaximized::Neither); + /// ``` + pub fn fullscreen_or_maximized( + mut self, + fullscreen_or_maximized: FullscreenOrMaximized, + ) -> Self { + self.0.fullscreen_or_maximized = Some(fullscreen_or_maximized as i32); + self + } + + /// This rule will force windows to open at a specific x-coordinate. + /// + /// This will only actually be visible if the window is also floating. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRule; + /// + /// // Force the window to open at x = 480 + /// let rule = WindowRule::new().x(480); + /// ``` + pub fn x(mut self, x: i32) -> Self { + self.0.x = Some(x); + self + } + + /// This rule will force windows to open at a specific y-coordinate. + /// + /// This will only actually be visible if the window is also floating. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRule; + /// + /// // Force the window to open at y = 240 + /// let rule = WindowRule::new().y(240); + /// ``` + pub fn y(mut self, y: i32) -> Self { + self.0.y = Some(y); + self + } + + /// This rule will force windows to open with a specific width. + /// + /// This will only actually be visible if the window is also floating. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRule; + /// + /// // Force the window to open with a width of 500 pixels + /// let rule = WindowRule::new().width(500); + /// ``` + pub fn width(mut self, width: u32) -> Self { + self.0.width = Some(width as i32); + self + } + + /// This rule will force windows to open with a specific height. + /// + /// This will only actually be visible if the window is also floating. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRule; + /// + /// // Force the window to open with a height of 250 pixels + /// let rule = WindowRule::new().height(250); + /// ``` + pub fn height(mut self, height: u32) -> Self { + self.0.height = Some(height as i32); self } } diff --git a/api/rust_grpc/Cargo.toml b/api/rust_grpc/Cargo.toml deleted file mode 100644 index 0d0caf2..0000000 --- a/api/rust_grpc/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "pinnacle-api" -version = "0.0.2" -edition = "2021" -authors = ["Ottatop "] -description = "The Rust implementation of the Pinnacle compositor's configuration API" -license = "MPL-2.0" -repository = "https://github.com/pinnacle-comp/pinnacle" -keywords = ["compositor", "pinnacle", "api", "config"] -categories = ["api-bindings", "config"] - -[dependencies] -pinnacle-api-defs = { path = "../../pinnacle-api-defs" } -pinnacle-api-macros = { path = "./pinnacle-api-macros" } -tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread", "net"] } -async-net = "2.0.0" -async-compat = "0.2.3" -tonic = "0.10.2" -tower = { version = "0.4.13", features = ["util"] } -futures = "0.3.30" -num_enum = "0.7.2" -xkbcommon = "0.7.0" - -[workspace] -members = ["pinnacle-api-macros"] diff --git a/api/rust_grpc/src/input.rs b/api/rust_grpc/src/input.rs deleted file mode 100644 index 5127c0a..0000000 --- a/api/rust_grpc/src/input.rs +++ /dev/null @@ -1,383 +0,0 @@ -//! Input management. -//! -//! This module provides [`Input`], a struct that gives you several different -//! methods for setting key- and mousebinds, changing xkeyboard settings, and more. -//! View the struct's documentation for more information. - -use futures::{ - channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture, FutureExt, StreamExt, -}; -use num_enum::TryFromPrimitive; -use pinnacle_api_defs::pinnacle::input::{ - self, - v0alpha1::{ - input_service_client::InputServiceClient, - set_libinput_setting_request::{CalibrationMatrix, Setting}, - SetKeybindRequest, SetLibinputSettingRequest, SetMousebindRequest, SetRepeatRateRequest, - SetXkbConfigRequest, - }, -}; -use tonic::transport::Channel; -use xkbcommon::xkb::Keysym; - -use self::libinput::LibinputSetting; - -pub mod libinput; - -/// A mouse button. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] -pub enum MouseButton { - /// The left mouse button - Left = 0x110, - /// The right mouse button - Right = 0x111, - /// The middle mouse button - Middle = 0x112, - /// The side mouse button - Side = 0x113, - /// The extra mouse button - Extra = 0x114, - /// The forward mouse button - Forward = 0x115, - /// The backward mouse button - Back = 0x116, -} - -/// Keyboard modifiers. -#[repr(i32)] -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, TryFromPrimitive)] -pub enum Mod { - /// The shift key - Shift = 1, - /// The ctrl key - Ctrl, - /// The alt key - Alt, - /// The super key, aka meta, win, mod4 - Super, -} - -/// Press or release. -#[repr(i32)] -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, TryFromPrimitive)] -pub enum MouseEdge { - /// Perform actions on button press - Press = 1, - /// Perform actions on button release - Release, -} - -/// A struct that lets you define xkeyboard config options. -/// -/// See `xkeyboard-config(7)` for more information. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] -pub struct XkbConfig { - /// Files of rules to be used for keyboard mapping composition - pub rules: Option<&'static str>, - /// Name of the model of your keyboard type - pub model: Option<&'static str>, - /// Layout(s) you intend to use - pub layout: Option<&'static str>, - /// Variant(s) of the layout you intend to use - pub variant: Option<&'static str>, - /// Extra xkb configuration options - pub options: Option<&'static str>, -} - -/// The `Input` struct. -/// -/// This struct contains methods that allow you to set key- and mousebinds, -/// change xkeyboard and libinput settings, and change the keyboard's repeat rate. -#[derive(Debug, Clone)] -pub struct Input { - channel: Channel, - fut_sender: UnboundedSender>, -} - -impl Input { - pub(crate) fn new( - channel: Channel, - fut_sender: UnboundedSender>, - ) -> Self { - Self { - channel, - fut_sender, - } - } - - fn create_input_client(&self) -> InputServiceClient { - InputServiceClient::new(self.channel.clone()) - } - - /// Set a keybind. - /// - /// If called with an already set keybind, it gets replaced. - /// - /// You must supply: - /// - `mods`: A list of [`Mod`]s. These must be held down for the keybind to trigger. - /// - `key`: The key that needs to be pressed. This can be anything that implements the [Key] trait: - /// - `char` - /// - `&str` and `String`: This is any name from - /// [xkbcommon-keysyms.h](https://xkbcommon.org/doc/current/xkbcommon-keysyms_8h.html) - /// without the `XKB_KEY_` prefix. - /// - `u32`: The numerical key code from the website above. - /// - A [`keysym`][Keysym] from the [`xkbcommon`] re-export. - /// - `action`: A closure that will be run when the keybind is triggered. - /// - Currently, any captures must be both `Send` and `'static`. If you want to mutate - /// something, consider using channels or [`Box::leak`]. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::input::Mod; - /// - /// // Set `Super + Shift + c` to close the focused window - /// input.keybind([Mod::Super, Mod::Shift], 'c', || { - /// if let Some(win) = window.get_focused() { - /// win.close(); - /// } - /// }); - /// - /// // With a string key - /// input.keybind([], "BackSpace", || { /* ... */ }); - /// - /// // With a numeric key - /// input.keybind([], 65, || { /* ... */ }); // 65 = 'A' - /// - /// // With a `Keysym` - /// input.keybind([], pinnacle_api::xkbcommon::xkb::Keysym::Return, || { /* ... */ }); - /// ``` - pub fn keybind( - &self, - mods: impl IntoIterator, - key: impl Key + Send + 'static, - mut action: impl FnMut() + Send + 'static, - ) { - let mut client = self.create_input_client(); - - let modifiers = mods.into_iter().map(|modif| modif as i32).collect(); - - self.fut_sender - .unbounded_send( - async move { - let mut stream = client - .set_keybind(SetKeybindRequest { - modifiers, - key: Some(input::v0alpha1::set_keybind_request::Key::RawCode( - key.into_keysym().raw(), - )), - }) - .await - .unwrap() - .into_inner(); - - while let Some(Ok(_response)) = stream.next().await { - action(); - } - } - .boxed(), - ) - .unwrap(); - } - - /// Set a mousebind. - /// - /// If called with an already set mousebind, it gets replaced. - /// - /// You must supply: - /// - `mods`: A list of [`Mod`]s. These must be held down for the keybind to trigger. - /// - `button`: A [`MouseButton`]. - /// - `edge`: A [`MouseEdge`]. This allows you to trigger the bind on either mouse press or release. - /// - `action`: A closure that will be run when the mousebind is triggered. - /// - Currently, any captures must be both `Send` and `'static`. If you want to mutate - /// something, consider using channels or [`Box::leak`]. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::input::{Mod, MouseButton, MouseEdge}; - /// - /// // Set `Super + left click` to start moving a window - /// input.mousebind([Mod::Super], MouseButton::Left, MouseEdge::Press, || { - /// window.begin_move(MouseButton::Press); - /// }); - /// ``` - pub fn mousebind( - &self, - mods: impl IntoIterator, - button: MouseButton, - edge: MouseEdge, - mut action: impl FnMut() + 'static + Send, - ) { - let mut client = self.create_input_client(); - - let modifiers = mods.into_iter().map(|modif| modif as i32).collect(); - - self.fut_sender - .unbounded_send( - async move { - let mut stream = client - .set_mousebind(SetMousebindRequest { - modifiers, - button: Some(button as u32), - edge: Some(edge as i32), - }) - .await - .unwrap() - .into_inner(); - - while let Some(Ok(_response)) = stream.next().await { - action(); - } - } - .boxed(), - ) - .unwrap(); - } - - /// Set the xkeyboard config. - /// - /// This allows you to set several xkeyboard options like `layout` and `rules`. - /// - /// See `xkeyboard-config(7)` for more information. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::input::XkbConfig; - /// - /// input.set_xkb_config(Xkbconfig { - /// layout: Some("us,fr,ge"), - /// options: Some("ctrl:swapcaps,caps:shift"), - /// ..Default::default() - /// }); - /// ``` - pub fn set_xkb_config(&self, xkb_config: XkbConfig) { - let mut client = self.create_input_client(); - - block_on(client.set_xkb_config(SetXkbConfigRequest { - rules: xkb_config.rules.map(String::from), - variant: xkb_config.variant.map(String::from), - layout: xkb_config.layout.map(String::from), - model: xkb_config.model.map(String::from), - options: xkb_config.options.map(String::from), - })) - .unwrap(); - } - - /// Set the keyboard's repeat rate. - /// - /// This allows you to set the time between holding down a key and it repeating - /// as well as the time between each repeat. - /// - /// Units are in milliseconds. - /// - /// # Examples - /// - /// ``` - /// // Set keyboard to repeat after holding down for half a second, - /// // and repeat once every 25ms (40 times a second) - /// input.set_repeat_rate(25, 500); - /// ``` - pub fn set_repeat_rate(&self, rate: i32, delay: i32) { - let mut client = self.create_input_client(); - - block_on(client.set_repeat_rate(SetRepeatRateRequest { - rate: Some(rate), - delay: Some(delay), - })) - .unwrap(); - } - - /// Set a libinput setting. - /// - /// From [freedesktop.org](https://www.freedesktop.org/wiki/Software/libinput/): - /// > libinput is a library to handle input devices in Wayland compositors - /// - /// As such, this method allows you to set various settings related to input devices. - /// This includes things like pointer acceleration and natural scrolling. - /// - /// See [`LibinputSetting`] for all the settings you can change. - /// - /// Note: currently Pinnacle applies anything set here to *every* device, regardless of what it - /// actually is. This will be fixed in the future. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::input::libinput::*; - /// - /// // Set pointer acceleration to flat - /// input.set_libinput_setting(LibinputSetting::AccelProfile(AccelProfile::Flat)); - /// - /// // Enable natural scrolling (reverses scroll direction; usually used with trackpads) - /// input.set_libinput_setting(LibinputSetting::NaturalScroll(true)); - /// ``` - pub fn set_libinput_setting(&self, setting: LibinputSetting) { - let mut client = self.create_input_client(); - - let setting = match setting { - LibinputSetting::AccelProfile(profile) => Setting::AccelProfile(profile as i32), - LibinputSetting::AccelSpeed(speed) => Setting::AccelSpeed(speed), - LibinputSetting::CalibrationMatrix(matrix) => { - Setting::CalibrationMatrix(CalibrationMatrix { - matrix: matrix.to_vec(), - }) - } - LibinputSetting::ClickMethod(method) => Setting::ClickMethod(method as i32), - LibinputSetting::DisableWhileTyping(disable) => Setting::DisableWhileTyping(disable), - LibinputSetting::LeftHanded(enable) => Setting::LeftHanded(enable), - LibinputSetting::MiddleEmulation(enable) => Setting::MiddleEmulation(enable), - LibinputSetting::RotationAngle(angle) => Setting::RotationAngle(angle), - LibinputSetting::ScrollButton(button) => Setting::RotationAngle(button), - LibinputSetting::ScrollButtonLock(enable) => Setting::ScrollButtonLock(enable), - LibinputSetting::ScrollMethod(method) => Setting::ScrollMethod(method as i32), - LibinputSetting::NaturalScroll(enable) => Setting::NaturalScroll(enable), - LibinputSetting::TapButtonMap(map) => Setting::TapButtonMap(map as i32), - LibinputSetting::TapDrag(enable) => Setting::TapDrag(enable), - LibinputSetting::TapDragLock(enable) => Setting::TapDragLock(enable), - LibinputSetting::Tap(enable) => Setting::Tap(enable), - }; - - block_on(client.set_libinput_setting(SetLibinputSettingRequest { - setting: Some(setting), - })) - .unwrap(); - } -} - -/// A trait that designates anything that can be converted into a [`Keysym`]. -pub trait Key { - /// Convert this into a [`Keysym`]. - fn into_keysym(self) -> Keysym; -} - -impl Key for Keysym { - fn into_keysym(self) -> Keysym { - self - } -} - -impl Key for char { - fn into_keysym(self) -> Keysym { - Keysym::from_char(self) - } -} - -impl Key for &str { - fn into_keysym(self) -> Keysym { - xkbcommon::xkb::keysym_from_name(self, xkbcommon::xkb::KEYSYM_NO_FLAGS) - } -} - -impl Key for String { - fn into_keysym(self) -> Keysym { - xkbcommon::xkb::keysym_from_name(&self, xkbcommon::xkb::KEYSYM_NO_FLAGS) - } -} - -impl Key for u32 { - fn into_keysym(self) -> Keysym { - Keysym::from(self) - } -} diff --git a/api/rust_grpc/src/input/libinput.rs b/api/rust_grpc/src/input/libinput.rs deleted file mode 100644 index 5e24df7..0000000 --- a/api/rust_grpc/src/input/libinput.rs +++ /dev/null @@ -1,85 +0,0 @@ -//! Types for libinput configuration. - -/// Pointer acceleration profile -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum AccelProfile { - /// A flat acceleration profile. - /// - /// Pointer motion is accelerated by a constant (device-specific) factor, depending on the current speed. - Flat = 1, - /// An adaptive acceleration profile. - /// - /// Pointer acceleration depends on the input speed. This is the default profile for most devices. - Adaptive, -} - -/// The click method defines when to generate software-emulated buttons, usually on a device -/// that does not have a specific physical button available. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum ClickMethod { - /// Use software-button areas to generate button events. - ButtonAreas = 1, - /// The number of fingers decides which button press to generate. - Clickfinger, -} - -/// The scroll method of a device selects when to generate scroll axis events instead of pointer motion events. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum ScrollMethod { - /// Never send scroll events instead of pointer motion events. - /// - /// This has no effect on events generated by scroll wheels. - NoScroll = 1, - /// Send scroll events when two fingers are logically down on the device. - TwoFinger, - /// Send scroll events when a finger moves along the bottom or right edge of a device. - Edge, - /// Send scroll events when a button is down and the device moves along a scroll-capable axis. - OnButtonDown, -} - -/// Map 1/2/3 finger tips to buttons. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum TapButtonMap { - /// 1/2/3 finger tap maps to left/right/middle - LeftRightMiddle, - /// 1/2/3 finger tap maps to left/middle/right - LeftMiddleRight, -} - -/// Possible settings for libinput. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum LibinputSetting { - /// Set the pointer acceleration profile - AccelProfile(AccelProfile), - /// Set pointer acceleration speed - AccelSpeed(f64), - /// Set the calibration matrix - CalibrationMatrix([f32; 6]), - /// Set the [`ClickMethod`] - ClickMethod(ClickMethod), - /// Set whether the device gets disabled while typing - DisableWhileTyping(bool), - /// Set left handed mode - LeftHanded(bool), - /// Allow or disallow middle mouse button emulation - MiddleEmulation(bool), - /// Set the rotation angle - RotationAngle(u32), - /// Set the scroll button - ScrollButton(u32), - /// Set whether the scroll button should be a drag or toggle - ScrollButtonLock(bool), - /// Set the [`ScrollMethod`] - ScrollMethod(ScrollMethod), - /// Enable or disable natural scrolling - NaturalScroll(bool), - /// Set the [`TapButtonMap`] - TapButtonMap(TapButtonMap), - /// Enable or disable tap-to-drag - TapDrag(bool), - /// Enable or disable a timeout where lifting a finger off the device will not stop dragging - TapDragLock(bool), - /// Enable or disable tap-to-click - Tap(bool), -} diff --git a/api/rust_grpc/src/lib.rs b/api/rust_grpc/src/lib.rs deleted file mode 100644 index e573209..0000000 --- a/api/rust_grpc/src/lib.rs +++ /dev/null @@ -1,202 +0,0 @@ -#![warn(missing_docs)] - -//! The Rust implementation of [Pinnacle](https://github.com/pinnacle-comp/pinnacle)'s -//! configuration API. -//! -//! This library allows to to interface with the Pinnacle compositor and configure various aspects -//! like input and the tag system. -//! -//! # Configuration -//! -//! ## 1. Create a cargo project -//! To create your own Rust config, create a Cargo project in `~/.config/pinnacle`. -//! -//! ## 2. Create `metaconfig.toml` -//! Then, create a file named `metaconfig.toml`. This is the file Pinnacle will use to determine -//! what to run, kill and reload-config keybinds, an optional socket directory, and any environment -//! variables to give the config client. -//! -//! In `metaconfig.toml`, put the following: -//! ```toml -//! # `command` will tell Pinnacle to run `cargo run` in your config directory. -//! # You can add stuff like "--release" here if you want to. -//! command = ["cargo", "run"] -//! -//! # You must define a keybind to reload your config if it crashes, otherwise you'll get stuck if -//! # the Lua config doesn't kick in properly. -//! reload_keybind = { modifiers = ["Ctrl", "Alt"], key = "r" } -//! -//! # Similarly, you must define a keybind to kill Pinnacle. -//! kill_keybind = { modifiers = ["Ctrl", "Alt", "Shift"], key = "escape" } -//! -//! # You can specify an optional socket directory if you need to place the socket Pinnacle will -//! # use for configuration in a different place. -//! # socket_dir = "your/dir/here" -//! -//! # If you need to set any environment variables for the config process, you can do so here if -//! # you don't want to do it in the config itself. -//! [envs] -//! # key = "value" -//! ``` -//! -//! ## 3. Set up dependencies -//! In your `Cargo.toml`, add a dependency to `pinnacle-api`: -//! -//! ```toml -//! # Cargo.toml -//! -//! [dependencies] -//! pinnacle-api = { git = "https://github.com/pinnacle-comp/pinnacle" } -//! ``` -//! -//! ## 4. Set up the main function -//! In `main.rs`, change `fn main()` to `async fn main()` and annotate it with the -//! [`pinnacle_api::config`][`crate::config`] macro. Pass in the identifier you want to bind the -//! config modules to: -//! -//! ``` -//! use pinnacle_api::ApiModules; -//! -//! #[pinnacle_api::config(modules)] -//! async fn main() { -//! // `modules` is now available in the function body. -//! // You can deconstruct `ApiModules` to get all the config structs. -//! let ApiModules { -//! pinnacle, -//! process, -//! window, -//! input, -//! output, -//! tag, -//! } = modules; -//! } -//! ``` -//! -//! ## 5. Begin crafting your config! -//! You can peruse the documentation for things to configure. - -use std::sync::OnceLock; - -use futures::{ - channel::mpsc::UnboundedReceiver, future::BoxFuture, stream::FuturesUnordered, StreamExt, -}; -use input::Input; -use output::Output; -use pinnacle::Pinnacle; -use process::Process; -use tag::Tag; -use tonic::transport::{Endpoint, Uri}; -use tower::service_fn; -use window::Window; - -pub mod input; -pub mod output; -pub mod pinnacle; -pub mod process; -pub mod tag; -pub mod util; -pub mod window; - -pub use pinnacle_api_macros::config; -pub use tokio; -pub use xkbcommon; - -static PINNACLE: OnceLock = OnceLock::new(); -static PROCESS: OnceLock = OnceLock::new(); -static WINDOW: OnceLock = OnceLock::new(); -static INPUT: OnceLock = OnceLock::new(); -static OUTPUT: OnceLock = OnceLock::new(); -static TAG: OnceLock = OnceLock::new(); - -/// A struct containing static references to all of the configuration structs. -#[derive(Debug, Clone, Copy)] -pub struct ApiModules { - /// The [`Pinnacle`] struct - pub pinnacle: &'static Pinnacle, - /// The [`Process`] struct - pub process: &'static Process, - /// The [`Window`] struct - pub window: &'static Window, - /// The [`Input`] struct - pub input: &'static Input, - /// The [`Output`] struct - pub output: &'static Output, - /// The [`Tag`] struct - pub tag: &'static Tag, -} - -/// Connects to Pinnacle and builds the configuration structs. -/// -/// This function is inserted at the top of your config through the [`config`] macro. -/// You should use that macro instead of this function directly. -pub async fn connect( -) -> Result<(ApiModules, UnboundedReceiver>), Box> { - let channel = Endpoint::try_from("http://[::]:50051")? // port doesn't matter, we use a unix socket - .connect_with_connector(service_fn(|_: Uri| { - tokio::net::UnixStream::connect( - std::env::var("PINNACLE_GRPC_SOCKET") - .expect("PINNACLE_GRPC_SOCKET was not set; is Pinnacle running?"), - ) - })) - .await?; - - let (fut_sender, fut_recv) = futures::channel::mpsc::unbounded::>(); - - let output = Output::new(channel.clone(), fut_sender.clone()); - - let pinnacle = PINNACLE.get_or_init(|| Pinnacle::new(channel.clone())); - let process = PROCESS.get_or_init(|| Process::new(channel.clone(), fut_sender.clone())); - let window = WINDOW.get_or_init(|| Window::new(channel.clone())); - let input = INPUT.get_or_init(|| Input::new(channel.clone(), fut_sender.clone())); - let tag = TAG.get_or_init(|| Tag::new(channel.clone(), fut_sender.clone())); - let output = OUTPUT.get_or_init(|| output); - - let modules = ApiModules { - pinnacle, - process, - window, - input, - output, - tag, - }; - - Ok((modules, fut_recv)) -} - -/// Listen to Pinnacle for incoming messages. -/// -/// This will run all futures returned by configuration methods that take in callbacks in order to -/// call them. -/// -/// This function is inserted at the end of your config through the [`config`] macro. -/// You should use the macro instead of this function directly. -pub async fn listen( - fut_recv: UnboundedReceiver>, // api_modules: ApiModules<'a>, -) { - let mut future_set = FuturesUnordered::< - BoxFuture<( - Option>, - Option>>, - )>, - >::new(); - - future_set.push(Box::pin(async move { - let (fut, stream) = fut_recv.into_future().await; - (fut, Some(stream)) - })); - - while let Some((fut, stream)) = future_set.next().await { - if let Some(fut) = fut { - future_set.push(Box::pin(async move { - fut.await; - (None, None) - })); - } - if let Some(stream) = stream { - future_set.push(Box::pin(async move { - let (fut, stream) = stream.into_future().await; - (fut, Some(stream)) - })) - } - } -} diff --git a/api/rust_grpc/src/output.rs b/api/rust_grpc/src/output.rs deleted file mode 100644 index 6ef6d9b..0000000 --- a/api/rust_grpc/src/output.rs +++ /dev/null @@ -1,515 +0,0 @@ -//! Output management. -//! -//! An output is Pinnacle's terminology for a monitor. -//! -//! This module provides [`Output`], which allows you to get [`OutputHandle`]s for different -//! connected monitors and set them up. - -use futures::{ - channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture, FutureExt, StreamExt, -}; -use pinnacle_api_defs::pinnacle::{ - output::{ - self, - v0alpha1::{ - output_service_client::OutputServiceClient, ConnectForAllRequest, SetLocationRequest, - }, - }, - tag::v0alpha1::tag_service_client::TagServiceClient, -}; -use tonic::transport::Channel; - -use crate::tag::TagHandle; - -/// A struct that allows you to get handles to connected outputs and set them up. -/// -/// See [`OutputHandle`] for more information. -#[derive(Debug, Clone)] -pub struct Output { - channel: Channel, - fut_sender: UnboundedSender>, -} - -impl Output { - pub(crate) fn new( - channel: Channel, - fut_sender: UnboundedSender>, - ) -> Self { - Self { - channel, - fut_sender, - } - } - - fn create_output_client(&self) -> OutputServiceClient { - OutputServiceClient::new(self.channel.clone()) - } - - fn create_tag_client(&self) -> TagServiceClient { - TagServiceClient::new(self.channel.clone()) - } - - /// Get a handle to all connected outputs. - /// - /// # Examples - /// - /// ``` - /// let outputs = output.get_all(); - /// ``` - pub fn get_all(&self) -> impl Iterator { - let mut client = self.create_output_client(); - let tag_client = self.create_tag_client(); - block_on(client.get(output::v0alpha1::GetRequest {})) - .unwrap() - .into_inner() - .output_names - .into_iter() - .map(move |name| OutputHandle { - client: client.clone(), - tag_client: tag_client.clone(), - name, - }) - } - - /// Get a handle to the output with the given name. - /// - /// By "name", we mean the name of the connector the output is connected to. - /// - /// # Examples - /// - /// ``` - /// let op = output.get_by_name("eDP-1")?; - /// let op2 = output.get_by_name("HDMI-2")?; - /// ``` - pub fn get_by_name(&self, name: impl Into) -> Option { - let name: String = name.into(); - self.get_all().find(|output| output.name == name) - } - - /// Get a handle to the focused output. - /// - /// This is currently implemented as the one that has had the most recent pointer movement. - /// - /// # Examples - /// - /// ``` - /// let op = output.get_focused()?; - /// ``` - pub fn get_focused(&self) -> Option { - self.get_all() - .find(|output| matches!(output.props().focused, Some(true))) - } - - /// Connect a closure to be run on all current and future outputs. - /// - /// When called, `connect_for_all` will do two things: - /// 1. Immediately run `for_all` with all currently connected outputs. - /// 2. Create a future that will call `for_all` with any newly connected outputs. - /// - /// Note that `for_all` will *not* run with outputs that have been unplugged and replugged. - /// This is to prevent duplicate setup. Instead, the compositor keeps track of any tags and - /// state the output had when unplugged and restores them on replug. - /// - /// # Examples - /// - /// ``` - /// // Add tags 1-3 to all outputs and set tag "1" to active - /// output.connect_for_all(|op| { - /// let tags = tag.add(&op, ["1", "2", "3"]); - /// tags.next().unwrap().set_active(true); - /// }); - /// ``` - pub fn connect_for_all(&self, mut for_all: impl FnMut(OutputHandle) + Send + 'static) { - for output in self.get_all() { - for_all(output); - } - - let mut client = self.create_output_client(); - let tag_client = self.create_tag_client(); - - self.fut_sender - .unbounded_send( - async move { - let mut stream = client - .connect_for_all(ConnectForAllRequest {}) - .await - .unwrap() - .into_inner(); - - while let Some(Ok(response)) = stream.next().await { - let Some(output_name) = response.output_name else { - continue; - }; - - let output = OutputHandle { - client: client.clone(), - tag_client: tag_client.clone(), - name: output_name, - }; - - for_all(output); - } - } - .boxed(), - ) - .unwrap(); - } -} - -/// A handle to an output. -/// -/// This allows you to manipulate outputs and get their properties. -#[derive(Clone, Debug)] -pub struct OutputHandle { - pub(crate) client: OutputServiceClient, - pub(crate) tag_client: TagServiceClient, - pub(crate) name: String, -} - -/// The alignment to use for [`OutputHandle::set_loc_adj_to`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum Alignment { - /// Set above, align left borders - TopAlignLeft, - /// Set above, align centers - TopAlignCenter, - /// Set above, align right borders - TopAlignRight, - /// Set below, align left borders - BottomAlignLeft, - /// Set below, align centers - BottomAlignCenter, - /// Set below, align right borders - BottomAlignRight, - /// Set to left, align top borders - LeftAlignTop, - /// Set to left, align centers - LeftAlignCenter, - /// Set to left, align bottom borders - LeftAlignBottom, - /// Set to right, align top borders - RightAlignTop, - /// Set to right, align centers - RightAlignCenter, - /// Set to right, align bottom borders - RightAlignBottom, -} - -impl OutputHandle { - /// Set the location of this output in the global space. - /// - /// On startup, Pinnacle will lay out all connected outputs starting at (0, 0) - /// and going to the right, with their top borders aligned. - /// - /// This method allows you to move outputs where necessary. - /// - /// Note: If you leave space between two outputs when setting their locations, - /// the pointer will not be able to move between them. - /// - /// # Examples - /// - /// ``` - /// // Assume two monitors in order, "DP-1" and "HDMI-1", with the following dimensions: - /// // - "DP-1": ┌─────┐ - /// // │ │1920x1080 - /// // └─────┘ - /// // - "HDMI-1": ┌───────┐ - /// // │ 2560x │ - /// // │ 1440 │ - /// // └───────┘ - /// - /// output.get_by_name("DP-1")?.set_location(0, 0); - /// output.get_by_name("HDMI-1")?.set_location(1920, -360); - /// - /// // Results in: - /// // x=0 ┌───────┐y=-360 - /// // y=0┌─────┤ │ - /// // │DP-1 │HDMI-1 │ - /// // └─────┴───────┘ - /// // ^x=1920 - /// ``` - pub fn set_location(&self, x: impl Into>, y: impl Into>) { - let mut client = self.client.clone(); - block_on(client.set_location(SetLocationRequest { - output_name: Some(self.name.clone()), - x: x.into(), - y: y.into(), - })) - .unwrap(); - } - - /// Set this output adjacent to another one. - /// - /// This is a helper method over [`OutputHandle::set_location`] to make laying out outputs - /// easier. - /// - /// `alignment` is an [`Alignment`] of how you want this output to be placed. - /// For example, [`TopAlignLeft`][Alignment::TopAlignLeft] will place this output - /// above `other` and align the left borders. - /// Similarly, [`RightAlignCenter`][Alignment::RightAlignCenter] will place this output - /// to the right of `other` and align their centers. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::output::Alignment; - /// - /// // Assume two monitors in order, "DP-1" and "HDMI-1", with the following dimensions: - /// // - "DP-1": ┌─────┐ - /// // │ │1920x1080 - /// // └─────┘ - /// // - "HDMI-1": ┌───────┐ - /// // │ 2560x │ - /// // │ 1440 │ - /// // └───────┘ - /// - /// output.get_by_name("DP-1")?.set_loc_adj_to(output.get_by_name("HDMI-1")?, Alignment::BottomAlignRight); - /// - /// // Results in: - /// // ┌───────┐ - /// // │ │ - /// // │HDMI-1 │ - /// // └──┬────┤ - /// // │DP-1│ - /// // └────┘ - /// // Notice that "DP-1" now has the coordinates (2280, 1440) because "DP-1" is getting moved, not "HDMI-1". - /// // "HDMI-1" was placed at (1920, 0) during the compositor's initial output layout. - /// ``` - pub fn set_loc_adj_to(&self, other: &OutputHandle, alignment: Alignment) { - let self_props = self.props(); - let other_props = other.props(); - - // poor man's try {} - let attempt_set_loc = || -> Option<()> { - let other_x = other_props.x?; - let other_y = other_props.y?; - let other_width = other_props.pixel_width? as i32; - let other_height = other_props.pixel_height? as i32; - - let self_width = self_props.pixel_width? as i32; - let self_height = self_props.pixel_height? as i32; - - use Alignment::*; - - let x: i32; - let y: i32; - - if let TopAlignLeft | TopAlignCenter | TopAlignRight | BottomAlignLeft - | BottomAlignCenter | BottomAlignRight = alignment - { - if let TopAlignLeft | TopAlignCenter | TopAlignRight = alignment { - y = other_y - self_height; - } else { - // bottom - y = other_y + other_height; - } - - match alignment { - TopAlignLeft | BottomAlignLeft => x = other_x, - TopAlignCenter | BottomAlignCenter => { - x = other_x + (other_width - self_width) / 2; - } - TopAlignRight | BottomAlignRight => x = other_x + (other_width - self_width), - _ => unreachable!(), - } - } else { - if let LeftAlignTop | LeftAlignCenter | LeftAlignBottom = alignment { - x = other_x - self_width; - } else { - x = other_x + other_width; - } - - match alignment { - LeftAlignTop | RightAlignTop => y = other_y, - LeftAlignCenter | RightAlignCenter => { - y = other_y + (other_height - self_height) / 2; - } - LeftAlignBottom | RightAlignBottom => { - y = other_y + (other_height - self_height); - } - _ => unreachable!(), - } - } - - self.set_location(Some(x), Some(y)); - - Some(()) - }; - - attempt_set_loc(); - } - - /// Get all properties of this output. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::output::OutputProperties; - /// - /// let OutputProperties { - /// make, - /// model, - /// x, - /// y, - /// pixel_width, - /// pixel_height, - /// refresh_rate, - /// physical_width, - /// physical_height, - /// focused, - /// tags, - /// } = output.get_focused()?.props(); - /// ``` - pub fn props(&self) -> OutputProperties { - let mut client = self.client.clone(); - let response = block_on( - client.get_properties(output::v0alpha1::GetPropertiesRequest { - output_name: Some(self.name.clone()), - }), - ) - .unwrap() - .into_inner(); - - OutputProperties { - make: response.make, - model: response.model, - x: response.x, - y: response.y, - pixel_width: response.pixel_width, - pixel_height: response.pixel_height, - refresh_rate: response.refresh_rate, - physical_width: response.physical_width, - physical_height: response.physical_height, - focused: response.focused, - tags: response - .tag_ids - .into_iter() - .map(|id| TagHandle { - client: self.tag_client.clone(), - output_client: self.client.clone(), - id, - }) - .collect(), - } - } - - // TODO: make a macro for the following or something - - /// Get this output's make. - /// - /// Shorthand for `self.props().make`. - pub fn make(&self) -> Option { - self.props().make - } - - /// Get this output's model. - /// - /// Shorthand for `self.props().make`. - pub fn model(&self) -> Option { - self.props().model - } - - /// Get this output's x position in the global space. - /// - /// Shorthand for `self.props().x`. - pub fn x(&self) -> Option { - self.props().x - } - - /// Get this output's y position in the global space. - /// - /// Shorthand for `self.props().y`. - pub fn y(&self) -> Option { - self.props().y - } - - /// Get this output's screen width in pixels. - /// - /// Shorthand for `self.props().pixel_width`. - pub fn pixel_width(&self) -> Option { - self.props().pixel_width - } - - /// Get this output's screen height in pixels. - /// - /// Shorthand for `self.props().pixel_height`. - pub fn pixel_height(&self) -> Option { - self.props().pixel_height - } - - /// Get this output's refresh rate in millihertz. - /// - /// For example, 144Hz will be returned as 144000. - /// - /// Shorthand for `self.props().refresh_rate`. - pub fn refresh_rate(&self) -> Option { - self.props().refresh_rate - } - - /// Get this output's physical width in millimeters. - /// - /// Shorthand for `self.props().physical_width`. - pub fn physical_width(&self) -> Option { - self.props().physical_width - } - - /// Get this output's physical height in millimeters. - /// - /// Shorthand for `self.props().physical_height`. - pub fn physical_height(&self) -> Option { - self.props().physical_height - } - - /// Get whether this output is focused or not. - /// - /// This is currently implemented as the output with the most recent pointer motion. - /// - /// Shorthand for `self.props().focused`. - pub fn focused(&self) -> Option { - self.props().focused - } - - /// Get the tags this output has. - /// - /// Shorthand for `self.props().tags` - pub fn tags(&self) -> Vec { - self.props().tags - } - - /// Get this output's unique name (the name of its connector). - pub fn name(&self) -> &str { - &self.name - } -} - -/// The properties of an output. -#[derive(Clone, Debug)] -pub struct OutputProperties { - /// The make of the output - pub make: Option, - /// The model of the output - /// - /// This is something like "27GL83A" or whatever crap monitor manufacturers name their monitors - /// these days. - pub model: Option, - /// The x position of the output in the global space - pub x: Option, - /// The y position of the output in the global space - pub y: Option, - /// The output's screen width in pixels - pub pixel_width: Option, - /// The output's screen height in pixels - pub pixel_height: Option, - /// The output's refresh rate in millihertz - pub refresh_rate: Option, - /// The output's physical width in millimeters - pub physical_width: Option, - /// The output's physical height in millimeters - pub physical_height: Option, - /// Whether this output is focused or not - /// - /// This is currently implemented as the output with the most recent pointer motion. - pub focused: Option, - /// The tags this output has - pub tags: Vec, -} diff --git a/api/rust_grpc/src/process.rs b/api/rust_grpc/src/process.rs deleted file mode 100644 index 121f2ec..0000000 --- a/api/rust_grpc/src/process.rs +++ /dev/null @@ -1,178 +0,0 @@ -//! Process management. -//! -//! This module provides [`Process`], which allows you to spawn processes and set environment -//! variables. - -use futures::{ - channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture, FutureExt, StreamExt, -}; -use pinnacle_api_defs::pinnacle::process::v0alpha1::{ - process_service_client::ProcessServiceClient, SetEnvRequest, SpawnRequest, -}; -use tonic::transport::Channel; - -/// A struct containing methods to spawn processes with optional callbacks and set environment -/// variables. -#[derive(Debug, Clone)] -pub struct Process { - channel: Channel, - fut_sender: UnboundedSender>, -} - -/// Optional callbacks to be run when a spawned process prints to stdout or stderr or exits. -pub struct SpawnCallbacks { - /// A callback that will be run when a process prints to stdout with a line - pub stdout: Option>, - /// A callback that will be run when a process prints to stderr with a line - pub stderr: Option>, - /// A callback that will be run when a process exits with a status code and message - #[allow(clippy::type_complexity)] - pub exit: Option, String) + Send>>, -} - -impl Process { - pub(crate) fn new( - channel: Channel, - fut_sender: UnboundedSender>, - ) -> Process { - Self { - channel, - fut_sender, - } - } - - fn create_process_client(&self) -> ProcessServiceClient { - ProcessServiceClient::new(self.channel.clone()) - } - - /// Spawn a process. - /// - /// Note that windows spawned *before* tags are added will not be displayed. - /// This will be changed in the future to be more like Awesome, where windows with no tags are - /// displayed on every tag instead. - /// - /// # Examples - /// - /// ``` - /// process.spawn(["alacritty"]); - /// process.spawn(["bash", "-c", "swaybg -i ~/path_to_wallpaper"]); - /// ``` - pub fn spawn(&self, args: impl IntoIterator>) { - self.spawn_inner(args, false, None); - } - - /// Spawn a process with callbacks for its stdout, stderr, and exit information. - /// - /// See [`SpawnCallbacks`] for the passed in struct. - /// - /// Note that windows spawned *before* tags are added will not be displayed. - /// This will be changed in the future to be more like Awesome, where windows with no tags are - /// displayed on every tag instead. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::process::SpawnCallbacks; - /// - /// process.spawn_with_callbacks(["alacritty"], SpawnCallbacks { - /// stdout: Some(Box::new(|line| println!("stdout: {line}"))), - /// stderr: Some(Box::new(|line| println!("stderr: {line}"))), - /// stdout: Some(Box::new(|code, msg| println!("exit code: {code:?}, exit_msg: {msg}"))), - /// }); - /// ``` - pub fn spawn_with_callbacks( - &self, - args: impl IntoIterator>, - callbacks: SpawnCallbacks, - ) { - self.spawn_inner(args, false, Some(callbacks)); - } - - /// Spawn a process only if it isn't already running. - /// - /// This is useful for startup programs. - /// - /// See [`Process::spawn`] for details. - pub fn spawn_once(&self, args: impl IntoIterator>) { - self.spawn_inner(args, true, None); - } - - /// Spawn a process only if it isn't already running with optional callbacks for its stdout, - /// stderr, and exit information. - /// - /// This is useful for startup programs. - /// - /// See [`Process::spawn_with_callbacks`] for details. - pub fn spawn_once_with_callbacks( - &self, - args: impl IntoIterator>, - callbacks: SpawnCallbacks, - ) { - self.spawn_inner(args, true, Some(callbacks)); - } - - fn spawn_inner( - &self, - args: impl IntoIterator>, - once: bool, - callbacks: Option, - ) { - let mut client = self.create_process_client(); - - let args = args.into_iter().map(Into::into).collect::>(); - - let request = SpawnRequest { - args, - once: Some(once), - has_callback: Some(callbacks.is_some()), - }; - - self.fut_sender - .unbounded_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 { - if let Some(stdout) = callbacks.stdout.as_mut() { - stdout(line); - } - } - if let Some(line) = response.stderr { - if let Some(stderr) = callbacks.stderr.as_mut() { - stderr(line); - } - } - if let Some(exit_msg) = response.exit_message { - if let Some(exit) = callbacks.exit.as_mut() { - exit(response.exit_code, exit_msg); - } - } - } - } - .boxed(), - ) - .unwrap(); - } - - /// Set an environment variable for the compositor. - /// This will cause any future spawned processes to have this environment variable. - /// - /// # Examples - /// - /// ``` - /// process.set_env("ENV", "a value lalala"); - /// ``` - pub fn set_env(&self, key: impl Into, value: impl Into) { - let key = key.into(); - let value = value.into(); - - let mut client = self.create_process_client(); - - block_on(client.set_env(SetEnvRequest { - key: Some(key), - value: Some(value), - })) - .unwrap(); - } -} diff --git a/api/rust_grpc/src/tag.rs b/api/rust_grpc/src/tag.rs deleted file mode 100644 index 256fdd3..0000000 --- a/api/rust_grpc/src/tag.rs +++ /dev/null @@ -1,528 +0,0 @@ -//! Tag management. -//! -//! This module allows you to interact with Pinnacle's tag system. -//! -//! # The Tag System -//! Many Wayland compositors use workspaces for window management. -//! Each window is assigned to a workspace and will only show up if that workspace is being -//! viewed. This is a find way to manage windows, but it's not that powerful. -//! -//! Instead, Pinnacle works with a tag system similar to window managers like [dwm](https://dwm.suckless.org/) -//! and, the window manager Pinnacle takes inspiration from, [awesome](https://awesomewm.org/). -//! -//! In a tag system, there are no workspaces. Instead, each window can be tagged with zero or more -//! tags, and zero or more tags can be displayed on a monitor at once. This allows you to, for -//! example, bring in your browsers on the same screen as your IDE by toggling the "Browser" tag. -//! -//! Workspaces can be emulated by only displaying one tag at a time. Combining this feature with -//! the ability to tag windows with multiple tags allows you to have one window show up on multiple -//! different "workspaces". As you can see, this system is much more powerful than workspaces -//! alone. -//! -//! # Configuration -//! `tag` contains the [`Tag`] struct, which allows you to add new tags -//! and get handles to already defined ones. -//! -//! These [`TagHandle`]s allow you to manipulate individual tags and get their properties. - -use std::{ - collections::HashMap, - sync::{Arc, Mutex}, -}; - -use futures::{channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture}; -use num_enum::TryFromPrimitive; -use pinnacle_api_defs::pinnacle::{ - output::v0alpha1::output_service_client::OutputServiceClient, - tag::{ - self, - v0alpha1::{ - tag_service_client::TagServiceClient, AddRequest, RemoveRequest, SetActiveRequest, - SetLayoutRequest, SwitchToRequest, - }, - }, -}; -use tonic::transport::Channel; - -use crate::output::{Output, OutputHandle}; - -/// A struct that allows you to add and remove tags and get [`TagHandle`]s. -#[derive(Clone, Debug)] -pub struct Tag { - channel: Channel, - fut_sender: UnboundedSender>, -} - -impl Tag { - pub(crate) fn new( - channel: Channel, - fut_sender: UnboundedSender>, - ) -> Self { - Self { - channel, - fut_sender, - } - } - - fn create_tag_client(&self) -> TagServiceClient { - TagServiceClient::new(self.channel.clone()) - } - - fn create_output_client(&self) -> OutputServiceClient { - OutputServiceClient::new(self.channel.clone()) - } - - /// Add tags to the specified output. - /// - /// This will add tags with the given names to `output` and return [`TagHandle`]s to all of - /// them. - /// - /// # Examples - /// - /// ``` - /// // Add tags 1-5 to the focused output - /// if let Some(op) = output.get_focused() { - /// let tags = tag.add(&op, ["1", "2", "3", "4", "5"]); - /// } - /// ``` - pub fn add( - &self, - output: &OutputHandle, - tag_names: impl IntoIterator>, - ) -> impl Iterator { - let mut client = self.create_tag_client(); - let output_client = self.create_output_client(); - - let tag_names = tag_names.into_iter().map(Into::into).collect(); - - let response = block_on(client.add(AddRequest { - output_name: Some(output.name.clone()), - tag_names, - })) - .unwrap() - .into_inner(); - - response.tag_ids.into_iter().map(move |id| TagHandle { - client: client.clone(), - output_client: output_client.clone(), - id, - }) - } - - /// Get handles to all tags across all outputs. - /// - /// # Examples - /// - /// ``` - /// let all_tags = tag.get_all(); - /// ``` - pub fn get_all(&self) -> impl Iterator { - let mut client = self.create_tag_client(); - let output_client = self.create_output_client(); - - let response = block_on(client.get(tag::v0alpha1::GetRequest {})) - .unwrap() - .into_inner(); - - response.tag_ids.into_iter().map(move |id| TagHandle { - client: client.clone(), - output_client: output_client.clone(), - id, - }) - } - - /// Get a handle to the first tag with the given name on `output`. - /// - /// If `output` is `None`, the focused output will be used. - /// - /// # Examples - /// - /// ``` - /// // Get tag "1" on output "HDMI-1" - /// if let Some(op) = output.get_by_name("HDMI-1") { - /// let tg = tag.get("1", &op); - /// } - /// - /// // Get tag "Thing" on the focused output - /// let tg = tag.get("Thing", None); - /// ``` - pub fn get<'a>( - &self, - name: impl Into, - output: impl Into>, - ) -> Option { - let name = name.into(); - let output: Option<&OutputHandle> = output.into(); - let output_module = Output::new(self.channel.clone(), self.fut_sender.clone()); - - self.get_all().find(|tag| { - let props = tag.props(); - - let same_tag_name = props.name.as_ref() == Some(&name); - let same_output = props.output.is_some_and(|op| { - Some(op.name) - == output - .map(|o| o.name.clone()) - .or_else(|| output_module.get_focused().map(|o| o.name)) - }); - - same_tag_name && same_output - }) - } - - /// Remove the given tags from their outputs. - /// - /// # Examples - /// - /// ``` - /// let tags = tag.add(output.get_by_name("DP-1")?, ["1", "2", "Buckle", "Shoe"]); - /// - /// tag.remove(tags); // "DP-1" no longer has any tags - /// ``` - pub fn remove(&self, tags: impl IntoIterator) { - let tag_ids = tags.into_iter().map(|handle| handle.id).collect::>(); - - let mut client = self.create_tag_client(); - - block_on(client.remove(RemoveRequest { tag_ids })).unwrap(); - } - - /// Create a [`LayoutCycler`] to cycle layouts on outputs. - /// - /// This will create a `LayoutCycler` with two functions: one to cycle forward the layout for - /// the first active tag on the specified output, and one to cycle backward. - /// - /// If you do not specify an output for `LayoutCycler` functions, it will default to the - /// focused output. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::tag::{Layout, LayoutCycler}; - /// use pinnacle_api::xkbcommon::xkb::Keysym; - /// use pinnacle_api::input::Mod; - /// - /// // Create a layout cycler that cycles through the listed layouts - /// let LayoutCycler { - /// prev: layout_prev, - /// next: layout_next, - /// } = tag.new_layout_cycler([ - /// Layout::MasterStack, - /// Layout::Dwindle, - /// Layout::Spiral, - /// Layout::CornerTopLeft, - /// Layout::CornerTopRight, - /// Layout::CornerBottomLeft, - /// Layout::CornerBottomRight, - /// ]); - /// - /// // Cycle layouts forward on the focused output - /// layout_next(None); - /// - /// // Cycle layouts backward on the focused output - /// layout_prev(None); - /// - /// // Cycle layouts forward on "eDP-1" - /// layout_next(output.get_by_name("eDP-1")?); - /// ``` - pub fn new_layout_cycler(&self, layouts: impl IntoIterator) -> LayoutCycler { - let indices = Arc::new(Mutex::new(HashMap::::new())); - let indices_clone = indices.clone(); - - let layouts = layouts.into_iter().collect::>(); - let layouts_clone = layouts.clone(); - let len = layouts.len(); - - let output_module = Output::new(self.channel.clone(), self.fut_sender.clone()); - let output_module_clone = output_module.clone(); - - let next = move |output: Option<&OutputHandle>| { - let Some(output) = output.cloned().or_else(|| output_module.get_focused()) else { - return; - }; - - let Some(first_tag) = output - .props() - .tags - .into_iter() - .find(|tag| tag.active() == Some(true)) - else { - return; - }; - - let mut indices = indices.lock().expect("layout next mutex lock failed"); - let index = indices.entry(first_tag.id).or_insert(0); - - if *index + 1 >= len { - *index = 0; - } else { - *index += 1; - } - - first_tag.set_layout(layouts[*index]); - }; - - let prev = move |output: Option<&OutputHandle>| { - let Some(output) = output - .cloned() - .or_else(|| output_module_clone.get_focused()) - else { - return; - }; - - let Some(first_tag) = output - .props() - .tags - .into_iter() - .find(|tag| tag.active() == Some(true)) - else { - return; - }; - - let mut indices = indices_clone.lock().expect("layout next mutex lock failed"); - let index = indices.entry(first_tag.id).or_insert(0); - - if index.checked_sub(1).is_none() { - *index = len - 1; - } else { - *index -= 1; - } - - first_tag.set_layout(layouts_clone[*index]); - }; - - LayoutCycler { - prev: Box::new(prev), - next: Box::new(next), - } - } -} - -/// A layout cycler that keeps track of tags and their layouts and provides functions to cycle -/// layouts on them. -#[allow(clippy::type_complexity)] -pub struct LayoutCycler { - /// Cycle to the next layout on the given output, or the focused output if `None`. - pub prev: Box) + Send + Sync + 'static>, - /// Cycle to the previous layout on the given output, or the focused output if `None`. - pub next: Box) + Send + Sync + 'static>, -} - -/// A handle to a tag. -/// -/// This handle allows you to do things like switch to tags and get their properties. -#[derive(Debug, Clone)] -pub struct TagHandle { - pub(crate) client: TagServiceClient, - pub(crate) output_client: OutputServiceClient, - pub(crate) id: u32, -} - -/// Various static layouts. -#[repr(i32)] -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, TryFromPrimitive)] -pub enum Layout { - /// One master window on the left with all other windows stacked to the right - MasterStack = 1, - /// Windows split in half towards the bottom right corner - Dwindle, - /// Windows split in half in a spiral - Spiral, - /// One main corner window in the top left with a column of windows on the right and a row on the bottom - CornerTopLeft, - /// One main corner window in the top right with a column of windows on the left and a row on the bottom - CornerTopRight, - /// One main corner window in the bottom left with a column of windows on the right and a row on the top. - CornerBottomLeft, - /// One main corner window in the bottom right with a column of windows on the left and a row on the top. - CornerBottomRight, -} - -impl TagHandle { - /// Activate this tag and deactivate all other ones on the same output. - /// - /// This essentially emulates what a traditional workspace is. - /// - /// # Examples - /// - /// ``` - /// // Assume the focused output has the following inactive tags and windows: - /// // "1": Alacritty - /// // "2": Firefox, Discord - /// // "3": Steam - /// tag.get("2")?.switch_to(); // Displays Firefox and Discord - /// tag.get("3")?.switch_to(); // Displays Steam - /// ``` - pub fn switch_to(&self) { - let mut client = self.client.clone(); - block_on(client.switch_to(SwitchToRequest { - tag_id: Some(self.id), - })) - .unwrap(); - } - - /// Set this tag to active or not. - /// - /// While active, windows with this tag will be displayed. - /// - /// While inactive, windows with this tag will not be displayed unless they have other active - /// tags. - /// - /// # Examples - /// - /// ``` - /// // Assume the focused output has the following inactive tags and windows: - /// // "1": Alacritty - /// // "2": Firefox, Discord - /// // "3": Steam - /// tag.get("2")?.set_active(true); // Displays Firefox and Discord - /// tag.get("3")?.set_active(true); // Displays Firefox, Discord, and Steam - /// tag.get("2")?.set_active(false); // Displays Steam - /// ``` - pub fn set_active(&self, set: bool) { - let mut client = self.client.clone(); - block_on(client.set_active(SetActiveRequest { - tag_id: Some(self.id), - set_or_toggle: Some(tag::v0alpha1::set_active_request::SetOrToggle::Set(set)), - })) - .unwrap(); - } - - /// Toggle this tag between active and inactive. - /// - /// While active, windows with this tag will be displayed. - /// - /// While inactive, windows with this tag will not be displayed unless they have other active - /// tags. - /// - /// # Examples - /// - /// ``` - /// // Assume the focused output has the following inactive tags and windows: - /// // "1": Alacritty - /// // "2": Firefox, Discord - /// // "3": Steam - /// tag.get("2")?.toggle(); // Displays Firefox and Discord - /// tag.get("3")?.toggle(); // Displays Firefox, Discord, and Steam - /// tag.get("3")?.toggle(); // Displays Firefox, Discord - /// tag.get("2")?.toggle(); // Displays nothing - /// ``` - pub fn toggle_active(&self) { - let mut client = self.client.clone(); - block_on(client.set_active(SetActiveRequest { - tag_id: Some(self.id), - set_or_toggle: Some(tag::v0alpha1::set_active_request::SetOrToggle::Toggle(())), - })) - .unwrap(); - } - - /// Remove this tag from its output. - /// - /// # Examples - /// - /// ``` - /// let tags = tag - /// .add(output.get_by_name("DP-1")?, ["1", "2", "Buckle", "Shoe"]) - /// .collect::>; - /// - /// tags[1].remove(); - /// tags[3].remove(); - /// // "DP-1" now only has tags "1" and "Buckle" - /// ``` - pub fn remove(mut self) { - block_on(self.client.remove(RemoveRequest { - tag_ids: vec![self.id], - })) - .unwrap(); - } - - /// Set this tag's layout. - /// - /// Layouting only applies to tiled windows (windows that are not floating, maximized, or - /// fullscreen). If multiple tags are active on an output, the first active tag's layout will - /// determine the layout strategy. - /// - /// See [`Layout`] for the different static layouts Pinnacle currently has to offer. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::tag::Layout; - /// - /// // Set the layout of tag "1" on the focused output to "corner top left". - /// tag.get("1", None)?.set_layout(Layout::CornerTopLeft); - /// ``` - pub fn set_layout(&self, layout: Layout) { - let mut client = self.client.clone(); - block_on(client.set_layout(SetLayoutRequest { - tag_id: Some(self.id), - layout: Some(layout as i32), - })) - .unwrap(); - } - - /// Get all properties of this tag. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::tag::TagProperties; - /// - /// let TagProperties { - /// active, - /// name, - /// output, - /// } = tag.get("1", None)?.props(); - /// ``` - pub fn props(&self) -> TagProperties { - let mut client = self.client.clone(); - let output_client = self.output_client.clone(); - - let response = block_on(client.get_properties(tag::v0alpha1::GetPropertiesRequest { - tag_id: Some(self.id), - })) - .unwrap() - .into_inner(); - - TagProperties { - active: response.active, - name: response.name, - output: response.output_name.map(|name| OutputHandle { - client: output_client, - tag_client: client, - name, - }), - } - } - - /// Get this tag's active status. - /// - /// Shorthand for `self.props().active`. - pub fn active(&self) -> Option { - self.props().active - } - - /// Get this tag's name. - /// - /// Shorthand for `self.props().name`. - pub fn name(&self) -> Option { - self.props().name - } - - /// Get a handle to the output this tag is on. - /// - /// Shorthand for `self.props().output`. - pub fn output(&self) -> Option { - self.props().output - } -} - -/// Properties of a tag. -pub struct TagProperties { - /// Whether the tag is active or not - pub active: Option, - /// The name of the tag - pub name: Option, - /// The output the tag is on - pub output: Option, -} diff --git a/api/rust_grpc/src/window.rs b/api/rust_grpc/src/window.rs deleted file mode 100644 index c120ce8..0000000 --- a/api/rust_grpc/src/window.rs +++ /dev/null @@ -1,530 +0,0 @@ -//! Window management. -//! -//! This module provides [`Window`], which allows you to get [`WindowHandle`]s and move and resize -//! windows using the mouse. -//! -//! [`WindowHandle`]s allow you to do things like resize and move windows, toggle them between -//! floating and tiled, close them, and more. -//! -//! This module also allows you to set window rules; see the [rules] module for more information. - -use futures::executor::block_on; -use num_enum::TryFromPrimitive; -use pinnacle_api_defs::pinnacle::{ - output::v0alpha1::output_service_client::OutputServiceClient, - tag::v0alpha1::tag_service_client::TagServiceClient, - window::v0alpha1::{ - window_service_client::WindowServiceClient, AddWindowRuleRequest, CloseRequest, - MoveToTagRequest, SetTagRequest, - }, - window::{ - self, - v0alpha1::{ - GetRequest, MoveGrabRequest, ResizeGrabRequest, SetFloatingRequest, - SetFullscreenRequest, SetMaximizedRequest, - }, - }, -}; -use tonic::transport::Channel; - -use crate::{input::MouseButton, tag::TagHandle, util::Geometry}; - -use self::rules::{WindowRule, WindowRuleCondition}; - -pub mod rules; - -/// A struct containing methods that get [`WindowHandle`]s and move windows with the mouse. -/// -/// See [`WindowHandle`] for more information. -#[derive(Debug, Clone)] -pub struct Window { - channel: Channel, -} - -impl Window { - pub(crate) fn new(channel: Channel) -> Self { - Self { channel } - } - - fn create_window_client(&self) -> WindowServiceClient { - WindowServiceClient::new(self.channel.clone()) - } - - fn create_tag_client(&self) -> TagServiceClient { - TagServiceClient::new(self.channel.clone()) - } - - fn create_output_client(&self) -> OutputServiceClient { - OutputServiceClient::new(self.channel.clone()) - } - - /// Start moving the window with the mouse. - /// - /// This will begin moving the window under the pointer using the specified [`MouseButton`]. - /// The button must be held down at the time this method is called for the move to start. - /// - /// This is intended to be used with [`Input::keybind`][crate::input::Input::keybind]. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::input::{Mod, MouseButton, MouseEdge}; - /// - /// // Set `Super + left click` to begin moving a window - /// input.mousebind([Mod::Super], MouseButton::Left, MouseEdge::Press, || { - /// window.begin_move(MouseButton::Left); - /// }); - /// ``` - pub fn begin_move(&self, button: MouseButton) { - let mut client = self.create_window_client(); - block_on(client.move_grab(MoveGrabRequest { - button: Some(button as u32), - })) - .unwrap(); - } - - /// Start resizing the window with the mouse. - /// - /// This will begin resizing the window under the pointer using the specified [`MouseButton`]. - /// The button must be held down at the time this method is called for the resize to start. - /// - /// This is intended to be used with [`Input::keybind`][crate::input::Input::keybind]. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::input::{Mod, MouseButton, MouseEdge}; - /// - /// // Set `Super + right click` to begin moving a window - /// input.mousebind([Mod::Super], MouseButton::Right, MouseEdge::Press, || { - /// window.begin_resize(MouseButton::Right); - /// }); - /// ``` - pub fn begin_resize(&self, button: MouseButton) { - let mut client = self.create_window_client(); - block_on(client.resize_grab(ResizeGrabRequest { - button: Some(button as u32), - })) - .unwrap(); - } - - /// Get all windows. - /// - /// # Examples - /// - /// ``` - /// let windows = window.get_all(); - /// ``` - pub fn get_all(&self) -> impl Iterator { - let mut client = self.create_window_client(); - let tag_client = self.create_tag_client(); - let output_client = self.create_output_client(); - block_on(client.get(GetRequest {})) - .unwrap() - .into_inner() - .window_ids - .into_iter() - .map(move |id| WindowHandle { - client: client.clone(), - id, - tag_client: tag_client.clone(), - output_client: output_client.clone(), - }) - } - - /// Get the currently focused window. - /// - /// # Examples - /// - /// ``` - /// let focused_window = window.get_focused()?; - /// ``` - pub fn get_focused(&self) -> Option { - self.get_all() - .find(|window| matches!(window.props().focused, Some(true))) - } - - /// Add a window rule. - /// - /// A window rule is a set of criteria that a window must open with. - /// For it to apply, a [`WindowRuleCondition`] must evaluate to true for the window in question. - /// - /// TODO: - pub fn add_window_rule(&self, cond: WindowRuleCondition, rule: WindowRule) { - let mut client = self.create_window_client(); - - block_on(client.add_window_rule(AddWindowRuleRequest { - cond: Some(cond.0), - rule: Some(rule.0), - })) - .unwrap(); - } -} - -/// A handle to a window. -/// -/// This allows you to manipulate the window and get its properties. -#[derive(Debug, Clone)] -pub struct WindowHandle { - pub(crate) client: WindowServiceClient, - pub(crate) id: u32, - pub(crate) tag_client: TagServiceClient, - pub(crate) output_client: OutputServiceClient, -} - -/// Whether a window is fullscreen, maximized, or neither. -#[repr(i32)] -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, TryFromPrimitive)] -pub enum FullscreenOrMaximized { - /// The window is neither fullscreen nor maximized - Neither = 1, - /// The window is fullscreen - Fullscreen, - /// The window is maximized - Maximized, -} - -/// Properties of a window. -#[derive(Debug, Clone)] -pub struct WindowProperties { - /// The location and size of the window - pub geometry: Option, - /// The window's class - pub class: Option, - /// The window's title - pub title: Option, - /// Whether the window is focused or not - pub focused: Option, - /// Whether the window is floating or not - /// - /// Note that a window can still be floating even if it's fullscreen or maximized; those two - /// state will just override the floating state. - pub floating: Option, - /// Whether the window is fullscreen, maximized, or neither - pub fullscreen_or_maximized: Option, - /// All the tags on the window - pub tags: Vec, -} - -impl WindowHandle { - /// Send a close request to this window. - /// - /// If the window is unresponsive, it may not close. - /// - /// # Examples - /// - /// ``` - /// // Close the focused window - /// window.get_focused()?.close() - /// ``` - pub fn close(mut self) { - block_on(self.client.close(CloseRequest { - window_id: Some(self.id), - })) - .unwrap(); - } - - /// Set this window to fullscreen or not. - /// - /// If it is maximized, setting it to fullscreen will remove the maximized state. - /// - /// # Examples - /// - /// ``` - /// // Set the focused window to fullscreen. - /// window.get_focused()?.set_fullscreen(true); - /// ``` - pub fn set_fullscreen(&self, set: bool) { - let mut client = self.client.clone(); - block_on(client.set_fullscreen(SetFullscreenRequest { - window_id: Some(self.id), - set_or_toggle: Some(window::v0alpha1::set_fullscreen_request::SetOrToggle::Set( - set, - )), - })) - .unwrap(); - } - - /// Toggle this window between fullscreen and not. - /// - /// If it is maximized, toggling it to fullscreen will remove the maximized state. - /// - /// # Examples - /// - /// ``` - /// // Toggle the focused window to and from fullscreen. - /// window.get_focused()?.toggle_fullscreen(); - /// ``` - pub fn toggle_fullscreen(&self) { - let mut client = self.client.clone(); - block_on(client.set_fullscreen(SetFullscreenRequest { - window_id: Some(self.id), - set_or_toggle: Some(window::v0alpha1::set_fullscreen_request::SetOrToggle::Toggle(())), - })) - .unwrap(); - } - - /// Set this window to maximized or not. - /// - /// If it is fullscreen, setting it to maximized will remove the fullscreen state. - /// - /// # Examples - /// - /// ``` - /// // Set the focused window to maximized. - /// window.get_focused()?.set_maximized(true); - /// ``` - pub fn set_maximized(&self, set: bool) { - let mut client = self.client.clone(); - block_on(client.set_maximized(SetMaximizedRequest { - window_id: Some(self.id), - set_or_toggle: Some(window::v0alpha1::set_maximized_request::SetOrToggle::Set( - set, - )), - })) - .unwrap(); - } - - /// Toggle this window between maximized and not. - /// - /// If it is fullscreen, setting it to maximized will remove the fullscreen state. - /// - /// # Examples - /// - /// ``` - /// // Toggle the focused window to and from maximized. - /// window.get_focused()?.toggle_maximized(); - /// ``` - pub fn toggle_maximized(&self) { - let mut client = self.client.clone(); - block_on(client.set_maximized(SetMaximizedRequest { - window_id: Some(self.id), - set_or_toggle: Some(window::v0alpha1::set_maximized_request::SetOrToggle::Toggle(())), - })) - .unwrap(); - } - - /// Set this window to floating or not. - /// - /// Floating windows will not be tiled and can be moved around and resized freely. - /// - /// Note that fullscreen and maximized windows can still be floating; those two states will - /// just override the floating state. - /// - /// # Examples - /// - /// ``` - /// // Set the focused window to floating. - /// window.get_focused()?.set_floating(true); - /// ``` - pub fn set_floating(&self, set: bool) { - let mut client = self.client.clone(); - block_on(client.set_floating(SetFloatingRequest { - window_id: Some(self.id), - set_or_toggle: Some(window::v0alpha1::set_floating_request::SetOrToggle::Set( - set, - )), - })) - .unwrap(); - } - - /// Toggle this window to and from floating. - /// - /// Floating windows will not be tiled and can be moved around and resized freely. - /// - /// Note that fullscreen and maximized windows can still be floating; those two states will - /// just override the floating state. - /// - /// # Examples - /// - /// ``` - /// // Toggle the focused window to and from floating. - /// window.get_focused()?.toggle_floating(); - /// ``` - pub fn toggle_floating(&self) { - let mut client = self.client.clone(); - block_on(client.set_floating(SetFloatingRequest { - window_id: Some(self.id), - set_or_toggle: Some(window::v0alpha1::set_floating_request::SetOrToggle::Toggle( - (), - )), - })) - .unwrap(); - } - - /// Move this window to the given `tag`. - /// - /// This will remove all tags from this window then tag it with `tag`, essentially moving the - /// window to that tag. - /// - /// # Examples - /// - /// ``` - /// // Move the focused window to tag "Code" on the focused output - /// window.get_focused()?.move_to_tag(&tag.get("Code", None)?); - /// ``` - pub fn move_to_tag(&self, tag: &TagHandle) { - let mut client = self.client.clone(); - - block_on(client.move_to_tag(MoveToTagRequest { - window_id: Some(self.id), - tag_id: Some(tag.id), - })) - .unwrap(); - } - - /// Set or unset a tag on this window. - /// - /// # Examples - /// - /// ``` - /// let focused = window.get_focused()?; - /// let tg = tag.get("Potato", None)?; - /// - /// focused.set_tag(&tg, true); // `focused` now has tag "Potato" - /// focused.set_tag(&tg, false); // `focused` no longer has tag "Potato" - /// ``` - pub fn set_tag(&self, tag: &TagHandle, set: bool) { - let mut client = self.client.clone(); - - block_on(client.set_tag(SetTagRequest { - window_id: Some(self.id), - tag_id: Some(tag.id), - set_or_toggle: Some(window::v0alpha1::set_tag_request::SetOrToggle::Set(set)), - })) - .unwrap(); - } - - /// Toggle a tag on this window. - /// - /// # Examples - /// - /// ``` - /// let focused = window.get_focused()?; - /// let tg = tag.get("Potato", None)?; - /// - /// // Assume `focused` does not have tag `tg` - /// - /// focused.toggle_tag(&tg); // `focused` now has tag "Potato" - /// focused.toggle_tag(&tg); // `focused` no longer has tag "Potato" - /// ``` - pub fn toggle_tag(&self, tag: &TagHandle) { - let mut client = self.client.clone(); - - block_on(client.set_tag(SetTagRequest { - window_id: Some(self.id), - tag_id: Some(tag.id), - set_or_toggle: Some(window::v0alpha1::set_tag_request::SetOrToggle::Toggle(())), - })) - .unwrap(); - } - - /// Get all properties of this window. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::window::WindowProperties; - /// - /// let WindowProperties { - /// geometry, - /// class, - /// title, - /// focused, - /// floating, - /// fullscreen_or_maximized, - /// tags, - /// } = window.get_focused()?.props(); - /// ``` - pub fn props(&self) -> WindowProperties { - let mut client = self.client.clone(); - let tag_client = self.tag_client.clone(); - let response = block_on( - client.get_properties(window::v0alpha1::GetPropertiesRequest { - window_id: Some(self.id), - }), - ) - .unwrap() - .into_inner(); - - let fullscreen_or_maximized = response - .fullscreen_or_maximized - .unwrap_or_default() - .try_into() - .ok(); - - let geometry = response.geometry.map(|geo| Geometry { - x: geo.x(), - y: geo.y(), - width: geo.width() as u32, - height: geo.height() as u32, - }); - - WindowProperties { - geometry, - class: response.class, - title: response.title, - focused: response.focused, - floating: response.floating, - fullscreen_or_maximized, - tags: response - .tag_ids - .into_iter() - .map(|id| TagHandle { - client: tag_client.clone(), - output_client: self.output_client.clone(), - id, - }) - .collect(), - } - } - - /// Get this window's location and size. - /// - /// Shorthand for `self.props().geometry`. - pub fn geometry(&self) -> Option { - self.props().geometry - } - - /// Get this window's class. - /// - /// Shorthand for `self.props().class`. - pub fn class(&self) -> Option { - self.props().class - } - - /// Get this window's title. - /// - /// Shorthand for `self.props().title`. - pub fn title(&self) -> Option { - self.props().title - } - - /// Get whether or not this window is focused. - /// - /// Shorthand for `self.props().focused`. - pub fn focused(&self) -> Option { - self.props().focused - } - - /// Get whether or not this window is floating. - /// - /// Shorthand for `self.props().floating`. - pub fn floating(&self) -> Option { - self.props().floating - } - - /// Get whether this window is fullscreen, maximized, or neither. - /// - /// Shorthand for `self.props().fullscreen_or_maximized`. - pub fn fullscreen_or_maximized(&self) -> Option { - self.props().fullscreen_or_maximized - } - - /// Get all the tags on this window. - /// - /// Shorthand for `self.props().tags`. - pub fn tags(&self) -> Vec { - self.props().tags - } -} diff --git a/api/rust_grpc/src/window/rules.rs b/api/rust_grpc/src/window/rules.rs deleted file mode 100644 index 86e5a0a..0000000 --- a/api/rust_grpc/src/window/rules.rs +++ /dev/null @@ -1,521 +0,0 @@ -//! Types for window rules. -//! -//! A window rule is a way to set the properties of a window on open. -//! -//! They are comprised of two parts: the [condition][WindowRuleCondition] and the actual [rule][WindowRule]. -//! -//! # [`WindowRuleCondition`]s -//! `WindowRuleCondition`s are conditions that the window needs to open with in order to apply a -//! rule. For example, you may want to set a window to maximized if it has the class "steam", or -//! you might want to open all Firefox instances on tag "3". -//! -//! To do this, you must build a `WindowRuleCondition` to tell the compositor when to apply any -//! rules. -//! -//! ## Building `WindowRuleCondition`s -//! A condition is created through [`WindowRuleCondition::new`]: -//! ``` -//! let cond = WindowRuleCondition::new(); -//! ``` -//! -//! In order to understand conditions, you must understand the concept of "any" and "all". -//! -//! **"Any"** -//! -//! "Any" conditions only need one of their constituent items to be true for the whole condition to -//! evaluate to true. Think of it as one big `if a || b || c || d || ... {}` block. -//! -//! **"All"** -//! -//! "All" conditions need *all* of their constituent items to be true for the condition to evaluate -//! to true. This is like a big `if a && b && c && d && ... {}` block. -//! -//! Note that any items in a top level `WindowRuleCondition` fall under "all", so all those items -//! must be true. -//! -//! With that out of the way, we can get started building conditions. -//! -//! ### `WindowRuleCondition::classes` -//! With [`WindowRuleCondition::classes`], you can specify what classes a window needs to have for -//! a rule to apply. -//! -//! The following will apply to windows with the class "firefox": -//! ``` -//! let cond = WindowRuleCondition::new().classes(["firefox"]); -//! ``` -//! -//! Note that you pass in some `impl IntoIterator>`. This means you can -//! pass in more than one class here: -//! ``` -//! let failing_cond = WindowRuleCondition::new().classes(["firefox", "steam"]); -//! ``` -//! *HOWEVER*: this will not work. Recall that top level conditions are implicitly "all". This -//! means the above would require windows to have *both classes*, which is impossible. Thus, the -//! condition above will never be true. -//! -//! ### `WindowRuleCondition::titles` -//! Like `classes`, you can use `titles` to specify that the window needs to open with a specific -//! title for the condition to apply. -//! -//! ``` -//! let cond = WindowRuleCondition::new().titles(["Steam"]); -//! ``` -//! -//! Like `classes`, passing in multiple titles at the top level will cause the condition to always -//! fail. -//! -//! ### `WindowRuleCondition::tags` -//! You can specify that the window needs to open on the given tags in order to apply a rule. -//! -//! ``` -//! let cond = WindowRuleCondition::new().tags([&tag.get("3", output.get_by_name("HDMI-1")?)?]); -//! ``` -//! -//! Here, if you have tag "3" active on "HDMI-1" and spawn a window on that output, this condition -//! will apply. -//! -//! Unlike `classes` and `titles`, you can specify multiple tags at the top level: -//! -//! ``` -//! let op = output.get_by_name("HDMI-1")?; -//! let tag1 = tag.get("1", &op)?; -//! let tag2 = tag.get("2", &op)?; -//! -//! let cond = WindowRuleCondition::new().tags([&tag1, &tag2]); -//! ``` -//! -//! Now, you must have both tags "1" and "2" active and spawn a window for the condition to apply. -//! -//! ### `WindowRuleCondition::any` -//! Now we can get to ways to compose more complex conditions. -//! -//! `WindowRuleCondition::any` takes in conditions and will evaluate to true if *anything* in those -//! conditions are true. -//! -//! ``` -//! let cond = WindowRuleCondition::new() -//! .any([ -//! WindowRuleCondition::new().classes(["Alacritty"]), -//! WindowRuleCondition::new().tags([&tag.get("2", None)?]), -//! ]); -//! ``` -//! -//! This condition will apply if the window is *either* "Alacritty" *or* opens on tag "2". -//! -//! ### `WindowRuleCondition::all` -//! With `WindowRuleCondition::all`, *all* specified conditions must be true for the condition to -//! be true. -//! -//! ``` -//! let cond = WindowRuleCondition::new() -//! .all([ -//! WindowRuleCondition::new().classes(["Alacritty"]), -//! WindowRuleCondition::new().tags([&tag.get("2", None)?]), -//! ]); -//! ``` -//! -//! This condition applies if the window has the class "Alacritty" *and* opens on tag "2". -//! -//! You can write the above a bit shorter, as top level conditions are already "all": -//! -//! ``` -//! let cond = WindowRuleCondition::new() -//! .classes(["Alacritty"]) -//! .tags([&tag.get("2", None)?]); -//! ``` -//! -//! ## Complex condition composition -//! You can arbitrarily nest `any` and `all` to achieve desired logic. -//! -//! ``` -//! let op = output.get_by_name("HDMI-1")?; -//! let tag1 = tag.get("1", &op)?; -//! let tag2 = tag.get("2", &op)?; -//! -//! let complex_cond = WindowRuleCondition::new() -//! .any([ -//! WindowRuleCondition::new().all([ -//! WindowRuleCondition::new() -//! .classes("Alacritty") -//! .tags([&tag1, &tag2]) -//! ]), -//! WindowRuleCondition::new().all([ -//! WindowRuleCondition::new().any([ -//! WindowRuleCondition::new().titles(["nvim", "emacs", "nano"]), -//! ]), -//! WindowRuleCondition::new().any([ -//! WindowRuleCondition::new().tags([&tag1, &tag2]), -//! ]), -//! ]) -//! ]) -//! ``` -//! -//! The above is true if either of the following are true: -//! - The window has class "Alacritty" and opens on both tags "1" and "2", or -//! - The window's class is either "nvim", "emacs", or "nano" *and* it opens on either tag "1" or -//! "2". -//! -//! # [`WindowRule`]s -//! `WindowRuleCondition`s are half of a window rule. The other half is the [`WindowRule`] itself. -//! -//! A `WindowRule` is what will apply to a window if a condition is true. -//! -//! ## Building `WindowRule`s -//! -//! Create a new window rule with [`WindowRule::new`]: -//! -//! ``` -//! let rule = WindowRule::new(); -//! ``` -//! -//! There are several rules you can set currently. -//! -//! ### [`WindowRule::output`] -//! This will cause the window to open on the specified output. -//! -//! ### [`WindowRule::tags`] -//! This will cause the window to open with the given tags. -//! -//! ### [`WindowRule::floating`] -//! This will cause the window to open either floating or tiled. -//! -//! ### [`WindowRule::fullscreen_or_maximized`] -//! This will cause the window to open either fullscreen, maximized, or neither. -//! -//! ### [`WindowRule::x`] -//! This will cause the window to open at the given x-coordinate. -//! -//! Note: this only applies to floating windows; tiled windows' geometry will be overridden by -//! layouting. -//! -//! ### [`WindowRule::y`] -//! This will cause the window to open at the given y-coordinate. -//! -//! Note: this only applies to floating windows; tiled windows' geometry will be overridden by -//! layouting. -//! -//! ### [`WindowRule::width`] -//! This will cause the window to open with the given width in pixels. -//! -//! Note: this only applies to floating windows; tiled windows' geometry will be overridden by -//! layouting. -//! -//! ### [`WindowRule::height`] -//! This will cause the window to open with the given height in pixels. -//! -//! Note: this only applies to floating windows; tiled windows' geometry will be overridden by -//! layouting. - -use pinnacle_api_defs::pinnacle::window; - -use crate::{output::OutputHandle, tag::TagHandle}; - -use super::FullscreenOrMaximized; - -/// A condition for a [`WindowRule`] to apply to a window. -/// -/// `WindowRuleCondition`s are built using the builder pattern. -#[derive(Default, Debug, Clone)] -pub struct WindowRuleCondition(pub(super) window::v0alpha1::WindowRuleCondition); - -impl WindowRuleCondition { - /// Create a new, empty `WindowRuleCondition`. - pub fn new() -> Self { - Default::default() - } - - /// This condition requires that at least one provided condition is true. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::window::rules::WindowRuleCondition; - /// - /// // `cond` will be true if the window opens with *either* class "Alacritty" or "firefox" - /// // *or* with title "Steam" - /// let cond = WindowRuleCondition::new() - /// .any([ - /// WindowRuleCondition::new().classes(["Alacritty", "firefox"]), - /// WindowRuleCondition::new().titles(["Steam"]). - /// ]); - /// ``` - pub fn any(mut self, conds: impl IntoIterator) -> Self { - self.0.any = conds.into_iter().map(|cond| cond.0).collect(); - self - } - - /// This condition requires that all provided conditions are true. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::window::rules::WindowRuleCondition; - /// - /// // `cond` will be true if the window opens with class "Alacritty" *and* on tag "1" - /// let cond = WindowRuleCondition::new() - /// .any([ - /// WindowRuleCondition::new().tags([tag.get("1", None)?]), - /// WindowRuleCondition::new().titles(["Alacritty"]). - /// ]); - /// ``` - pub fn all(mut self, conds: impl IntoIterator) -> Self { - self.0.all = conds.into_iter().map(|cond| cond.0).collect(); - self - } - - /// This condition requires that the window's class matches. - /// - /// When used in a top level condition or inside of [`WindowRuleCondition::all`], - /// *all* classes must match (this is impossible). - /// - /// When used in [`WindowRuleCondition::any`], at least one of the - /// provided classes must match. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::window::rules::WindowRuleCondition; - /// - /// // `cond` will be true if the window opens with class "Alacritty" - /// let cond = WindowRuleCondition::new().classes(["Alacritty"]); - /// - /// // Top level conditions need all items to be true, - /// // so the following will never be true as windows can't have two classes at once - /// let always_false = WindowRuleCondition::new().classes(["Alacritty", "firefox"]); - /// - /// // To make the above work, use [`WindowRuleCondition::any`]. - /// // The following will be true if the window is "Alacritty" or "firefox" - /// let any_class = WindowRuleCondition::new() - /// .any([ WindowRuleCondition::new().classes(["Alacritty", "firefox"]) ]); - /// ``` - pub fn classes(mut self, classes: impl IntoIterator>) -> Self { - self.0.classes = classes.into_iter().map(Into::into).collect(); - self - } - - /// This condition requires that the window's title matches. - /// - /// When used in a top level condition or inside of [`WindowRuleCondition::all`], - /// *all* titles must match (this is impossible). - /// - /// When used in [`WindowRuleCondition::any`], at least one of the - /// provided titles must match. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::window::rules::WindowRuleCondition; - /// - /// // `cond` will be true if the window opens with title "vim" - /// let cond = WindowRuleCondition::new().titles(["vim"]); - /// - /// // Top level conditions need all items to be true, - /// // so the following will never be true as windows can't have two titles at once - /// let always_false = WindowRuleCondition::new().titles(["vim", "emacs"]); - /// - /// // To make the above work, use [`WindowRuleCondition::any`]. - /// // The following will be true if the window has the title "vim" or "emacs" - /// let any_title = WindowRuleCondition::new() - /// .any([WindowRuleCondition::new().titles(["vim", "emacs"])]); - /// ``` - pub fn titles(mut self, titles: impl IntoIterator>) -> Self { - self.0.titles = titles.into_iter().map(Into::into).collect(); - self - } - - /// This condition requires that the window's is opened on the given tags. - /// - /// When used in a top level condition or inside of [`WindowRuleCondition::all`], - /// the window must open on *all* given tags. - /// - /// When used in [`WindowRuleCondition::any`], the window must open on at least - /// one of the given tags. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::window::rules::WindowRuleCondition; - /// - /// let tag1 = tag.get("1", None)?; - /// let tag2 = tag.get("2", None)?; - /// - /// // `cond` will be true if the window opens with tag "1" - /// let cond = WindowRuleCondition::new().tags([&tag1]); - /// - /// // Top level conditions need all items to be true, - /// // so the following will be true if the window opens with both tags "1" and "2" - /// let all_tags = WindowRuleCondition::new().tags([&tag1, &tag2]); - /// - /// // This does the same as the above - /// let all_tags = WindowRuleCondition::new() - /// .all([WindowRuleCondition::new().tags([&tag1, &tag2])]); - /// - /// // The following will be true if the window opens with *either* tag "1" or "2" - /// let any_tag = WindowRuleCondition::new() - /// .any([WindowRuleCondition::new().tags([&tag1, &tag2])]); - /// ``` - pub fn tags<'a>(mut self, tags: impl IntoIterator) -> Self { - self.0.tags = tags.into_iter().map(|tag| tag.id).collect(); - self - } -} - -/// A window rule. -/// -/// This is what will be applied to a window if it meets a [`WindowRuleCondition`]. -/// -/// `WindowRule`s are built using the builder pattern. -#[derive(Clone, Debug, Default)] -pub struct WindowRule(pub(super) window::v0alpha1::WindowRule); - -impl WindowRule { - /// Create a new, empty window rule. - pub fn new() -> Self { - Default::default() - } - - /// This rule will force windows to open on the provided `output`. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::window::rules::WindowRule; - /// - /// // Force the window to open on "HDMI-1" - /// let rule = WindowRule::new().output(output.get_by_name("HDMI-1")?); - /// ``` - pub fn output(mut self, output: &OutputHandle) -> Self { - self.0.output = Some(output.name.clone()); - self - } - - /// This rule will force windows to open with the provided `tags`. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::window::rules::WindowRule; - /// - /// let op = output.get_by_name("HDMI-1")?; - /// let tag1 = tag.get("1", &op)?; - /// let tag2 = tag.get("2", &op)?; - /// - /// // Force the window to open with tags "1" and "2" - /// let rule = WindowRule::new().tags([&tag1, &tag2]); - /// ``` - pub fn tags<'a>(mut self, tags: impl IntoIterator) -> Self { - self.0.tags = tags.into_iter().map(|tag| tag.id).collect(); - self - } - - /// This rule will force windows to open either floating or not. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::window::rules::WindowRule; - /// - /// // Force the window to open floating - /// let rule = WindowRule::new().floating(true); - /// - /// // Force the window to open tiled - /// let rule = WindowRule::new().floating(false); - /// ``` - pub fn floating(mut self, floating: bool) -> Self { - self.0.floating = Some(floating); - self - } - - /// This rule will force windows to open either fullscreen, maximized, or neither. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::window::rules::WindowRule; - /// use pinnacle_api::window::FullscreenOrMaximized; - /// - /// // Force the window to open fullscreen - /// let rule = WindowRule::new().fullscreen_or_maximized(FullscreenOrMaximized::Fullscreen); - /// - /// // Force the window to open maximized - /// let rule = WindowRule::new().fullscreen_or_maximized(FullscreenOrMaximized::Maximized); - /// - /// // Force the window to open not fullscreen nor maximized - /// let rule = WindowRule::new().fullscreen_or_maximized(FullscreenOrMaximized::Neither); - /// ``` - pub fn fullscreen_or_maximized( - mut self, - fullscreen_or_maximized: FullscreenOrMaximized, - ) -> Self { - self.0.fullscreen_or_maximized = Some(fullscreen_or_maximized as i32); - self - } - - /// This rule will force windows to open at a specific x-coordinate. - /// - /// This will only actually be visible if the window is also floating. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::window::rules::WindowRule; - /// - /// // Force the window to open at x = 480 - /// let rule = WindowRule::new().x(480); - /// ``` - pub fn x(mut self, x: i32) -> Self { - self.0.x = Some(x); - self - } - - /// This rule will force windows to open at a specific y-coordinate. - /// - /// This will only actually be visible if the window is also floating. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::window::rules::WindowRule; - /// - /// // Force the window to open at y = 240 - /// let rule = WindowRule::new().y(240); - /// ``` - pub fn y(mut self, y: i32) -> Self { - self.0.y = Some(y); - self - } - - /// This rule will force windows to open with a specific width. - /// - /// This will only actually be visible if the window is also floating. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::window::rules::WindowRule; - /// - /// // Force the window to open with a width of 500 pixels - /// let rule = WindowRule::new().width(500); - /// ``` - pub fn width(mut self, width: u32) -> Self { - self.0.width = Some(width as i32); - self - } - - /// This rule will force windows to open with a specific height. - /// - /// This will only actually be visible if the window is also floating. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::window::rules::WindowRule; - /// - /// // Force the window to open with a height of 250 pixels - /// let rule = WindowRule::new().height(250); - /// ``` - pub fn height(mut self, height: u32) -> Self { - self.0.height = Some(height as i32); - self - } -}