diff --git a/.github/workflows/rustdoc.yml b/.github/workflows/rustdoc.yml index 3216823..a02769e 100644 --- a/.github/workflows/rustdoc.yml +++ b/.github/workflows/rustdoc.yml @@ -22,6 +22,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + - name: Get protoc + run: sudo apt install protobuf-compiler - name: Build docs run: cd ./api/rust && cargo doc - name: Create index.html diff --git a/Cargo.lock b/Cargo.lock index 8109427..cde96be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1489,12 +1489,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" -[[package]] -name = "paste" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" - [[package]] name = "percent-encoding" version = "2.3.1" @@ -1556,8 +1550,6 @@ dependencies = [ "pinnacle-api-defs", "prost", "prost-types", - "rmp", - "rmp-serde", "serde", "shellexpand", "smithay", @@ -1868,28 +1860,6 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" -[[package]] -name = "rmp" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9860a6cc38ed1da53456442089b4dfa35e7cedaa326df63017af88385e6b20" -dependencies = [ - "byteorder", - "num-traits", - "paste", -] - -[[package]] -name = "rmp-serde" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bffea85eea980d8a74453e5d02a8d93028f3c34725de143085a844ebe953258a" -dependencies = [ - "byteorder", - "rmp", - "serde", -] - [[package]] name = "rustc-demangle" version = "0.1.23" diff --git a/Cargo.toml b/Cargo.toml index c3054ca..bba02d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,8 +19,6 @@ thiserror = "1" xcursor = { version = "0.3", optional = true } image = { version = "0.24", default-features = false, optional = true } serde = { version = "1.0", features = ["derive"] } -rmp = { version = "0.8.12" } -rmp-serde = { version = "1.1.2" } x11rb = { version = "0.13", default-features = false, features = ["composite"], optional = true } shellexpand = "3.1.0" toml = "0.8" @@ -65,3 +63,4 @@ xwayland = ["smithay/xwayland", "x11rb", "smithay/x11rb_event_source", "xcursor" [workspace] members = ["pinnacle-api-defs"] +exclude = ["api/rust_grpc", "api/rust"] diff --git a/README.md b/README.md index 18fd23c..66f32d2 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,9 @@ Pinnacle is a Wayland compositor built in Rust using [Smithay](https://github.co It's my attempt at creating something like [AwesomeWM](https://github.com/awesomeWM/awesome) for Wayland. -It sports extensive configurability through either Lua or Rust, with the ability to add more languages in the future. +It sports extensive configurability through either Lua or Rust, with the ability to add more languages +in the future. And by that I mean other people can do the adding, + I'm already maintaining Lua and Rust lol > ### More video examples below! >
@@ -61,7 +63,7 @@ You will need: - [Rust](https://www.rust-lang.org/) 1.72 or newer, to build the project and use the Rust API - [Lua](https://www.lua.org/) 5.4 or newer, to use the Lua API - Packages for [Smithay](https://github.com/Smithay/smithay): -`libwayland libxkbcommon libudev libinput libgdm libseat`, as well as `xwayland` + `libwayland libxkbcommon libudev libinput libgdm libseat`, as well as `xwayland` - Arch: ```sh sudo pacman -S wayland wayland-protocols libxkbcommon systemd-libs libinput mesa seatd xorg-xwayland @@ -118,10 +120,6 @@ See flags you can pass in by running `cargo run -- --help` (or `-h`). # Configuration Pinnacle is configured in your choice of Lua or Rust. -> [!NOTE] -> Pinnacle is currently in the process of migrating the configuration backend from MessagePack to gRPC. -> The Lua library has already been rewritten, and the Rust API will be rewritten soon. - ## Out-of-the-box configurations If you just want to test Pinnacle out without copying stuff to your config directory, run one of the following in the crate root: @@ -132,13 +130,13 @@ PINNACLE_CONFIG_DIR="./api/lua/examples/default" cargo run PINNACLE_CONFIG_DIR="~/.local/share/pinnacle/default_config" cargo run # For a Rust configuration -PINNACLE_CONFIG_DIR="./api/rust" cargo run +PINNACLE_CONFIG_DIR="./api/rust/examples/default_config" cargo run ``` ## Custom configuration > [!IMPORTANT] -> Pinnacle is under heavy development, and there *will* be major breaking changes to these APIs +> Pinnacle is under development, and there *will* be major breaking changes to these APIs > until I release version 0.1, at which point there will be an API stability spec in place. > > Until then, I recommend you either use the out-of-the-box configs above or prepare for @@ -180,7 +178,7 @@ If you want to use Rust to configure Pinnacle, follow these steps: 1. In `~/.config/pinnacle`, run `cargo init`. 2. In the `Cargo.toml` file, add the following under `[dependencies]`: ```toml -pinnacle_api = { git = "http://github.com/pinnacle-comp/pinnacle" } +pinnacle-api = { git = "http://github.com/pinnacle-comp/pinnacle" } ``` 3. Create the file `metaconfig.toml` at the root. Add the following to the file: ```toml @@ -188,7 +186,7 @@ command = ["cargo", "run"] reload_keybind = { modifiers = ["Ctrl", "Alt"], key = "r" } kill_keybind = { modifiers = ["Ctrl", "Alt", "Shift"], key = "escape" } ``` -4. Copy the contents from [`example_config.rs`](api/rust/examples/example_config.rs) to `src/main.rs`. +4. Copy the [default config](api/rust/examples/default_config/main.rs) to `src/main.rs`. 5. Run Pinnacle! (You may want to run `cargo build` beforehand so you don't have to wait for your config to compile.) ### API Documentation diff --git a/api/lua/pinnacle/output.lua b/api/lua/pinnacle/output.lua index 8e5415c..3139c9d 100644 --- a/api/lua/pinnacle/output.lua +++ b/api/lua/pinnacle/output.lua @@ -191,7 +191,7 @@ end --- -- ┌─────┤ │ --- -- │DP-1 │HDMI-1 │ --- -- └─────┴───────┘ ---- -- Notice that x = 0 aligns with the top of "DP-1", and the top of "HDMI-1" is at x = -360. +--- -- Notice that y = 0 aligns with the top of "DP-1", and the top of "HDMI-1" is at y = -360. ---``` --- ---@param loc { x: integer?, y: integer? } diff --git a/api/lua/pinnacle/process.lua b/api/lua/pinnacle/process.lua index 28c70f2..da9d195 100644 --- a/api/lua/pinnacle/process.lua +++ b/api/lua/pinnacle/process.lua @@ -108,6 +108,18 @@ function Process:spawn_once(args, callbacks) spawn_inner(self.config_client, args, callbacks, true) end +---Set an environment variable for the compositor. +---This will cause any future spawned processes to have this environment variable. +--- +---@param key string The environment variable key +---@param value string The environment variable value +function Process:set_env(key, value) + self.config_client:unary_request(build_grpc_request_params("SetEnv", { + key = key, + value = value, + })) +end + function process.new(config_client) ---@type Process local self = { config_client = config_client } diff --git a/api/rust/.gitignore b/api/rust/.gitignore new file mode 100644 index 0000000..03314f7 --- /dev/null +++ b/api/rust/.gitignore @@ -0,0 +1 @@ +Cargo.lock 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/examples/default_config/main.rs b/api/rust/examples/default_config/main.rs new file mode 100644 index 0000000..7462eb5 --- /dev/null +++ b/api/rust/examples/default_config/main.rs @@ -0,0 +1,151 @@ +use pinnacle_api::xkbcommon::xkb::Keysym; +use pinnacle_api::{ + input::{Mod, MouseButton, MouseEdge}, + tag::{Layout, LayoutCycler}, + ApiModules, +}; + +#[pinnacle_api::config(modules)] +async fn main() { + let ApiModules { + pinnacle, + process, + window, + input, + output, + tag, + } = modules; + + let mod_key = Mod::Ctrl; + + let terminal = "alacritty"; + + // Mousebinds + + // `mod_key + left click` starts moving a window + input.mousebind([mod_key], MouseButton::Left, MouseEdge::Press, || { + window.begin_move(MouseButton::Left); + }); + + // `mod_key + right click` starts resizing a window + input.mousebind([mod_key], MouseButton::Right, MouseEdge::Press, || { + window.begin_resize(MouseButton::Right); + }); + + // Keybinds + + // `mod_key + alt + q` quits Pinnacle + input.keybind([mod_key, Mod::Alt], 'q', || { + pinnacle.quit(); + }); + + // `mod_key + alt + c` closes the focused window + input.keybind([mod_key, Mod::Alt], 'c', || { + if let Some(window) = window.get_focused() { + window.close(); + } + }); + + // `mod_key + Return` spawns a terminal + input.keybind([mod_key], Keysym::Return, move || { + process.spawn([terminal]); + }); + + // `mod_key + alt + space` toggles floating + input.keybind([mod_key, Mod::Alt], Keysym::space, || { + if let Some(window) = window.get_focused() { + window.toggle_floating(); + } + }); + + // `mod_key + f` toggles fullscreen + input.keybind([mod_key], 'f', || { + if let Some(window) = window.get_focused() { + window.toggle_fullscreen(); + } + }); + + // `mod_key + m` toggles maximized + input.keybind([mod_key], 'm', || { + if let Some(window) = window.get_focused() { + window.toggle_maximized(); + } + }); + + // Window rules + // + // You can define window rules to get windows to open with desired properties. + // See `pinnacle_api::window::rules` in the docs for more information. + + // Tags + + let tag_names = ["1", "2", "3", "4", "5"]; + + // Setup all monitors with tags "1" through "5" + output.connect_for_all(move |op| { + let mut tags = tag.add(&op, tag_names); + + // Be sure to set a tag to active or windows won't display + tags.next().unwrap().set_active(true); + }); + + process.spawn_once([terminal]); + + // Create a layout cycler to cycle through the given 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, + ]); + + // `mod_key + space` cycles to the next layout + input.keybind([mod_key], Keysym::space, move || { + layout_next(None); + }); + + // `mod_key + shift + space` cycles to the previous layout + input.keybind([mod_key, Mod::Shift], Keysym::space, move || { + layout_prev(None); + }); + + for tag_name in tag_names { + // `mod_key + 1-5` switches to tag "1" to "5" + input.keybind([mod_key], tag_name, move || { + if let Some(tg) = tag.get(tag_name) { + tg.switch_to(); + } + }); + + // `mod_key + shift + 1-5` toggles tag "1" to "5" + input.keybind([mod_key, Mod::Shift], tag_name, move || { + if let Some(tg) = tag.get(tag_name) { + tg.toggle_active(); + } + }); + + // `mod_key + alt + 1-5` moves the focused window to tag "1" to "5" + input.keybind([mod_key, Mod::Alt], tag_name, move || { + if let Some(tg) = tag.get(tag_name) { + if let Some(win) = window.get_focused() { + win.move_to_tag(&tg); + } + } + }); + + // `mod_key + shift + alt + 1-5` toggles tag "1" to "5" on the focused window + input.keybind([mod_key, Mod::Shift, Mod::Alt], tag_name, move || { + if let Some(tg) = tag.get(tag_name) { + if let Some(win) = window.get_focused() { + win.toggle_tag(&tg); + } + } + }); + } +} diff --git a/api/rust/metaconfig.toml b/api/rust/examples/default_config/metaconfig.toml similarity index 65% rename from api/rust/metaconfig.toml rename to api/rust/examples/default_config/metaconfig.toml index aa70e7d..414a25a 100644 --- a/api/rust/metaconfig.toml +++ b/api/rust/examples/default_config/metaconfig.toml @@ -7,10 +7,11 @@ # ~/.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`. +# To use a Rust config, this should be changed to something like ["cargo", "run"]. # # Because configuration is done using an external process, if it ever crashes, you lose all of your keybinds. +# The compositor will load the default config if that happens, but in the event that you don't have +# the necessary dependencies for it to run, you may get softlocked. # In order prevent you from getting stuck in the compositor, you must define keybinds to reload your config # and kill Pinnacle. # @@ -19,7 +20,7 @@ # 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"] +command = ["cargo", "run", "--example", "default_config"] ### Keybinds ### # Each keybind takes in a table with two fields: `modifiers` and `key`. @@ -40,13 +41,6 @@ kill_keybind = { modifiers = ["Ctrl", "Alt", "Shift"], key = "escape" } # 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. +# If you need to spawn your config with any environment variables, list them here. [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" +# key = "value" 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/pinnacle-api-macros/Cargo.toml b/api/rust/pinnacle-api-macros/Cargo.toml new file mode 100644 index 0000000..ba39c76 --- /dev/null +++ b/api/rust/pinnacle-api-macros/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "pinnacle-api-macros" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +quote = "1.0.35" +syn = { version = "2.0.48", features = ["full", "parsing"] } +proc-macro2 = "1.0.76" + +[lib] +proc-macro = true diff --git a/api/rust/pinnacle-api-macros/src/lib.rs b/api/rust/pinnacle-api-macros/src/lib.rs new file mode 100644 index 0000000..7838045 --- /dev/null +++ b/api/rust/pinnacle-api-macros/src/lib.rs @@ -0,0 +1,166 @@ +use proc_macro2::{Ident, Span}; +use quote::{quote, quote_spanned}; +use syn::{ + parse::Parse, parse_macro_input, punctuated::Punctuated, spanned::Spanned, Expr, Lit, + MetaNameValue, ReturnType, Stmt, Token, +}; + +/// Transform the annotated function into one used to configure the Pinnacle compositor. +/// +/// This will cause the function to connect to Pinnacle's gRPC server, run your configuration code, +/// then await all necessary futures needed to call callbacks. +/// +/// This function will not return unless an error occurs. +/// +/// # Usage +/// The function must be marked `async`, as this macro will insert the `#[tokio::main]` macro below +/// it. +/// +/// It takes in an ident, with which Pinnacle's `ApiModules` struct will be bound to. +/// +/// ``` +/// #[pinnacle_api::config(modules)] +/// async fn main() { +/// // `modules` is now accessible in the function body +/// let ApiModules { .. } = modules; +/// } +/// ``` +/// +/// `pinnacle_api` annotates the function with a bare `#[tokio::main]` attribute. +/// If you would like to configure Tokio's options, additionally pass in +/// `internal_tokio = false` to this macro and annotate the function +/// with your own `tokio::main` attribute. +/// +/// `pinnacle_api` provides a re-export of `tokio` that may prove useful. If you need other Tokio +/// features, you may need to bring them in with your own Cargo.toml. +/// +/// Note: the `tokio::main` attribute must be inserted *below* the `pinnacle_api::config` +/// attribute, as attributes are expanded from top to bottom. +/// +/// ``` +/// #[pinnacle_api::config(modules, internal_tokio = false)] +/// #[pinnacle_api::tokio::main(worker_threads = 8)] +/// async fn main() {} +/// ``` +#[proc_macro_attribute] +pub fn config( + args: proc_macro::TokenStream, + item: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + let item = parse_macro_input!(item as syn::ItemFn); + let macro_input = parse_macro_input!(args as MacroInput); + + let vis = item.vis; + let sig = item.sig; + + if sig.asyncness.is_none() { + return quote_spanned! {sig.fn_token.span()=> + compile_error!("this function must be marked `async` to run a Pinnacle config"); + } + .into(); + } + + if let ReturnType::Type(_, ty) = sig.output { + return quote_spanned! {ty.span()=> + compile_error!("this function must not have a return type"); + } + .into(); + } + + let attrs = item.attrs; + + let stmts = item.block.stmts; + + if let Some(ret @ Stmt::Expr(Expr::Return(_), _)) = stmts.last() { + return quote_spanned! {ret.span()=> + compile_error!("this function must not return, as it awaits futures after the end of this statement"); + }.into(); + } + + let module_ident = macro_input.ident; + + let options = macro_input.options; + + let mut has_internal_tokio = false; + + let mut internal_tokio = true; + + if let Some(options) = options { + for name_value in options.iter() { + if name_value.path.get_ident() == Some(&Ident::new("internal_tokio", Span::call_site())) + { + if has_internal_tokio { + return quote_spanned! {name_value.path.span()=> + compile_error!("`internal_tokio` defined twice, remove this one"); + } + .into(); + } + + has_internal_tokio = true; + if let Expr::Lit(lit) = &name_value.value { + if let Lit::Bool(bool) = &lit.lit { + internal_tokio = bool.value; + continue; + } + } + + return quote_spanned! {name_value.value.span()=> + compile_error!("expected `true` or `false`"); + } + .into(); + } else { + return quote_spanned! {name_value.path.span()=> + compile_error!("expected valid option (currently only `internal_tokio`)"); + } + .into(); + } + } + } + + let tokio_attr = internal_tokio.then(|| { + quote! { + #[::pinnacle_api::tokio::main(crate = "::pinnacle_api::tokio")] + } + }); + + quote! { + #(#attrs)* + #tokio_attr + #vis #sig { + let (#module_ident, __fut_receiver) = ::pinnacle_api::connect().await.unwrap(); + + #(#stmts)* + + ::pinnacle_api::listen(__fut_receiver).await; + } + } + .into() +} + +struct MacroInput { + ident: syn::Ident, + options: Option>, +} + +impl Parse for MacroInput { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let ident = input.parse()?; + + let comma = input.parse::(); + + let mut options = None; + + if comma.is_ok() { + options = Some(input.parse_terminated(MetaNameValue::parse, Token![,])?); + } + + if !input.is_empty() { + return Err(syn::Error::new( + input.span(), + "expected `,` followed by options", + )); + } + + Ok(MacroInput { ident, options }) + } +} 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..64acc50 100644 --- a/api/rust/src/lib.rs +++ b/api/rust/src/lib.rs @@ -1,234 +1,200 @@ -//! 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 you 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>) { + 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..7fca866 100644 --- a/api/rust/src/output.rs +++ b/api/rust/src/output.rs @@ -1,301 +1,529 @@ //! 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, +impl PartialEq for OutputHandle { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} + +impl Eq for OutputHandle {} + +impl std::hash::Hash for OutputHandle { + fn hash(&self, state: &mut H) { + self.name.hash(state); + } +} + +/// 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/src/pinnacle.rs b/api/rust/src/pinnacle.rs new file mode 100644 index 0000000..90a7c24 --- /dev/null +++ b/api/rust/src/pinnacle.rs @@ -0,0 +1,38 @@ +//! Compositor management. +//! +//! This module provides [`Pinnacle`], which allows you to quit the compositor. + +use futures::executor::block_on; +use pinnacle_api_defs::pinnacle::v0alpha1::{ + pinnacle_service_client::PinnacleServiceClient, QuitRequest, +}; +use tonic::transport::Channel; + +/// A struct that allows you to quit the compositor. +#[derive(Debug, Clone)] +pub struct Pinnacle { + channel: Channel, +} + +impl Pinnacle { + pub(crate) fn new(channel: Channel) -> Self { + Self { channel } + } + + fn create_pinnacle_client(&self) -> PinnacleServiceClient { + PinnacleServiceClient::new(self.channel.clone()) + } + + /// Quit Pinnacle. + /// + /// # Examples + /// + /// ``` + /// // Quits Pinnacle. What else were you expecting? + /// pinnacle.quit(); + /// ``` + pub fn quit(&self) { + let mut client = self.create_pinnacle_client(); + block_on(client.quit(QuitRequest {})).unwrap(); + } +} 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..ddf93eb 100644 --- a/api/rust/src/tag.rs +++ b/api/rust/src/tag.rs @@ -1,208 +1,555 @@ //! 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 the focused output. + /// + /// If you need to get a tag on a specific output, see [`Tag::get_on_output`]. + /// + /// # Examples + /// + /// ``` + /// // Get tag "Thing" on the focused output + /// let tg = tag.get("Thing"); + /// ``` + pub fn get(&self, name: impl Into) -> Option { + let name = name.into(); + let output_module = Output::new(self.channel.clone(), self.fut_sender.clone()); + let focused_output = output_module.get_focused(); -/// 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) == focused_output); + + same_tag_name && same_output + }) + } + + /// Get a handle to the first tag with the given name on the specified output. + /// + /// If you just need to get a tag on the focused output, see [`Tag::get`]. + /// + /// # Examples + /// + /// ``` + /// // Get tag "Thing" on "HDMI-1" + /// let tg = tag.get_on_output("Thing", output.get_by_name("HDMI-2")?); + /// ``` + pub fn get_on_output( + &self, + name: impl Into, + output: &OutputHandle, + ) -> Option { + let name = name.into(); + + 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| &op == output); + + 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() +impl PartialEq for TagHandle { + fn eq(&self, other: &Self) -> bool { + self.id == other.id } } -/// Layouts for tags. -#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)] +impl Eq for TagHandle {} + +impl std::hash::Hash for TagHandle { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +/// 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/src/util.rs b/api/rust/src/util.rs new file mode 100644 index 0000000..ca7d9de --- /dev/null +++ b/api/rust/src/util.rs @@ -0,0 +1,14 @@ +//! Utility types. + +/// The size and location of something. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Geometry { + /// The x position + pub x: i32, + /// The y position + pub y: i32, + /// The width + pub width: u32, + /// The height + pub height: u32, +} diff --git a/api/rust/src/window.rs b/api/rust/src/window.rs index 8423ebc..fb6f12e 100644 --- a/api/rust/src/window.rs +++ b/api/rust/src/window.rs @@ -1,206 +1,544 @@ //! 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, +} + +impl PartialEq for WindowHandle { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for WindowHandle {} + +impl std::hash::Hash for WindowHandle { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +/// 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/src/api.rs b/src/api.rs index adb5d96..ba6d2fe 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,227 +1,1887 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +use std::{ffi::OsString, num::NonZeroU32, pin::Pin, process::Stdio}; -pub mod handlers; -pub mod msg; -pub mod protocol; +use pinnacle_api_defs::pinnacle::{ + input::v0alpha1::{ + set_libinput_setting_request::{AccelProfile, ClickMethod, ScrollMethod, TapButtonMap}, + set_mousebind_request::MouseEdge, + SetKeybindRequest, SetKeybindResponse, SetLibinputSettingRequest, SetMousebindRequest, + SetMousebindResponse, SetRepeatRateRequest, SetXkbConfigRequest, + }, + output::v0alpha1::{ConnectForAllRequest, ConnectForAllResponse, SetLocationRequest}, + process::v0alpha1::{SetEnvRequest, SpawnRequest, SpawnResponse}, + tag::v0alpha1::{ + AddRequest, AddResponse, RemoveRequest, SetActiveRequest, SetLayoutRequest, SwitchToRequest, + }, + v0alpha1::{Geometry, QuitRequest}, + window::v0alpha1::{ + AddWindowRuleRequest, CloseRequest, FullscreenOrMaximized, MoveGrabRequest, + MoveToTagRequest, ResizeGrabRequest, SetFloatingRequest, SetFullscreenRequest, + SetGeometryRequest, SetMaximizedRequest, SetTagRequest, WindowRule, WindowRuleCondition, + }, +}; +use smithay::{ + desktop::space::SpaceElement, + input::keyboard::XkbConfig, + reexports::{calloop, input as libinput, wayland_protocols::xdg::shell::server}, + utils::{Point, Rectangle, SERIAL_COUNTER}, + wayland::{compositor, shell::xdg::XdgToplevelSurfaceData}, +}; +use sysinfo::ProcessRefreshKind; +use tokio::io::AsyncBufReadExt; +use tokio_stream::Stream; +use tonic::{Request, Response, Status}; -use std::{ - io::{self, Read, Write}, - os::unix::net::{UnixListener, UnixStream}, - path::Path, - sync::{Arc, Mutex}, +use crate::{ + config::ConnectorSavedState, + focus::FocusTarget, + input::ModifierMask, + output::OutputName, + state::{State, WithState}, + tag::{Tag, TagId}, + window::{window_state::WindowId, WindowElement}, }; -use self::msg::{Msg, OutgoingMsg}; -use anyhow::Context; -use calloop::RegistrationToken; -use smithay::reexports::calloop::{ - self, channel::Sender, generic::Generic, EventSource, Interest, Mode, PostAction, -}; +type ResponseStream = Pin> + Send>>; +pub type StateFnSender = calloop::channel::Sender>; -pub const SOCKET_NAME: &str = "pinnacle_socket"; +pub struct PinnacleService { + pub sender: StateFnSender, +} -/// Handle a config process. -/// -/// `stream` is the incoming stream where messages will be received, -/// and `sender` sends decoded messages to the main state's handler. -fn handle_client( - mut stream: UnixStream, - sender: Sender, -) -> Result<(), Box> { - loop { - let mut len_marker_bytes = [0u8; 4]; - if let Err(err) = stream.read_exact(&mut len_marker_bytes) { - if err.kind() == io::ErrorKind::UnexpectedEof { - tracing::warn!("stream closed: {}", err); - stream.shutdown(std::net::Shutdown::Both)?; - break Ok(()); - } - }; +#[tonic::async_trait] +impl pinnacle_api_defs::pinnacle::v0alpha1::pinnacle_service_server::PinnacleService + for PinnacleService +{ + async fn quit(&self, _request: Request) -> Result, Status> { + tracing::trace!("PinnacleService.quit"); + let f = Box::new(|state: &mut State| { + state.shutdown(); + }); + // Expect is ok here, if it panics then the state was dropped beforehand + self.sender.send(f).expect("failed to send f"); - let len_marker = u32::from_ne_bytes(len_marker_bytes); - let mut msg_bytes = vec![0u8; len_marker as usize]; - - if let Err(err) = stream.read_exact(msg_bytes.as_mut_slice()) { - if err.kind() == io::ErrorKind::UnexpectedEof { - tracing::warn!("stream closed: {}", err); - stream.shutdown(std::net::Shutdown::Both)?; - break Ok(()); - } - }; - let msg: Msg = rmp_serde::from_slice(msg_bytes.as_slice())?; // TODO: handle error - - sender.send(msg)?; + Ok(Response::new(())) } } -/// A socket source for an event loop that will listen for config processes. -pub struct PinnacleSocketSource { - /// The socket listener - socket: Generic, - /// The sender that will send messages from clients to the main event loop. - sender: Sender, +pub struct InputService { + pub sender: StateFnSender, } -impl PinnacleSocketSource { - /// Create a loop source that listens for connections to the provided `socket_dir`. - /// This will also set PINNACLE_SOCKET for use in API implementations. - pub fn new( - sender: Sender, - socket_dir: &Path, - multiple_instances: bool, - ) -> anyhow::Result { - tracing::debug!("Creating socket source for dir {socket_dir:?}"); +#[tonic::async_trait] +impl pinnacle_api_defs::pinnacle::input::v0alpha1::input_service_server::InputService + for InputService +{ + type SetKeybindStream = ResponseStream; + type SetMousebindStream = ResponseStream; - // Test if you are running multiple instances of Pinnacle - // let multiple_instances = system.processes_by_exact_name("pinnacle").count() > 1; + async fn set_keybind( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); - // If you are, append a suffix to the socket name - let socket_name = if multiple_instances { - let mut suffix: u8 = 1; - while let Ok(true) = socket_dir - .join(format!("{SOCKET_NAME}_{suffix}")) - .try_exists() - { - suffix += 1; + tracing::debug!(request = ?request); + + // TODO: impl From<&[Modifier]> for ModifierMask + let modifiers = request + .modifiers() + .fold(ModifierMask::empty(), |acc, modifier| match modifier { + pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Unspecified => acc, + pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Shift => { + acc | ModifierMask::SHIFT + } + pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Ctrl => { + acc | ModifierMask::CTRL + } + pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Alt => { + acc | ModifierMask::ALT + } + pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Super => { + acc | ModifierMask::SUPER + } + }); + let key = request + .key + .ok_or_else(|| Status::invalid_argument("no key specified"))?; + + use pinnacle_api_defs::pinnacle::input::v0alpha1::set_keybind_request::Key; + let keysym = match key { + Key::RawCode(num) => { + tracing::info!("set keybind: {:?}, raw {}", modifiers, num); + xkbcommon::xkb::Keysym::new(num) } - format!("{SOCKET_NAME}_{suffix}") - } else { - SOCKET_NAME.to_string() - }; - - let socket_path = socket_dir.join(socket_name); - - // If there are multiple instances, don't touch other sockets - if multiple_instances { - if let Ok(exists) = socket_path.try_exists() { - if exists { - std::fs::remove_file(&socket_path) - .context(format!("Failed to remove old socket at {socket_path:?}",))?; + Key::XkbName(s) => { + if s.chars().count() == 1 { + let Some(ch) = s.chars().next() else { unreachable!() }; + let keysym = xkbcommon::xkb::Keysym::from_char(ch); + tracing::info!("set keybind: {:?}, {:?}", modifiers, keysym); + keysym + } else { + let keysym = + xkbcommon::xkb::keysym_from_name(&s, xkbcommon::xkb::KEYSYM_NO_FLAGS); + tracing::info!("set keybind: {:?}, {:?}", modifiers, keysym); + keysym } } - } else { - // If there aren't, remove them all - for file in std::fs::read_dir(socket_dir)? - .filter_map(|entry| entry.ok()) - .filter(|entry| entry.file_name().to_string_lossy().starts_with(SOCKET_NAME)) - { - tracing::debug!("Removing socket at {:?}", file.path()); - std::fs::remove_file(file.path()) - .context(format!("Failed to remove old socket at {:?}", file.path()))?; + }; + + let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); + + self.sender + .send(Box::new(move |state| { + state + .input_state + .keybinds + .insert((modifiers, keysym), sender); + })) + .map_err(|_| Status::internal("internal state was not running"))?; + + let receiver_stream = tokio_stream::wrappers::UnboundedReceiverStream::new(receiver); + + Ok(Response::new(Box::pin(receiver_stream))) + } + + async fn set_mousebind( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + tracing::debug!(request = ?request); + + let modifiers = request + .modifiers() + .fold(ModifierMask::empty(), |acc, modifier| match modifier { + pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Unspecified => acc, + pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Shift => { + acc | ModifierMask::SHIFT + } + pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Ctrl => { + acc | ModifierMask::CTRL + } + pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Alt => { + acc | ModifierMask::ALT + } + pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Super => { + acc | ModifierMask::SUPER + } + }); + let button = request + .button + .ok_or_else(|| Status::invalid_argument("no key specified"))?; + + let edge = request.edge(); + + if let MouseEdge::Unspecified = edge { + return Err(Status::invalid_argument("press or release not specified")); + } + + let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); + + self.sender + .send(Box::new(move |state| { + state + .input_state + .mousebinds + .insert((modifiers, button, edge), sender); + })) + .map_err(|_| Status::internal("internal state was not running"))?; + + let receiver_stream = tokio_stream::wrappers::UnboundedReceiverStream::new(receiver); + + Ok(Response::new(Box::pin(receiver_stream))) + } + + async fn set_xkb_config( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let f = Box::new(move |state: &mut State| { + let new_config = XkbConfig { + rules: request.rules(), + variant: request.variant(), + model: request.model(), + layout: request.layout(), + options: request.options.clone(), + }; + if let Some(kb) = state.seat.get_keyboard() { + if let Err(err) = kb.set_xkb_config(state, new_config) { + tracing::error!("Failed to set xkbconfig: {err}"); + } } - } + }); - let listener = UnixListener::bind(&socket_path) - .with_context(|| format!("Failed to bind to socket at {socket_path:?}"))?; - tracing::info!("Bound to socket at {socket_path:?}"); + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; - listener - .set_nonblocking(true) - .context("Failed to set socket to nonblocking")?; + Ok(Response::new(())) + } - let socket = Generic::new(listener, Interest::READ, Mode::Level); + async fn set_repeat_rate( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); - std::env::set_var("PINNACLE_SOCKET", socket_path); + let rate = request + .rate + .ok_or_else(|| Status::invalid_argument("no rate specified"))?; + let delay = request + .delay + .ok_or_else(|| Status::invalid_argument("no rate specified"))?; - Ok(Self { socket, sender }) + let f = Box::new(move |state: &mut State| { + if let Some(kb) = state.seat.get_keyboard() { + kb.change_repeat_info(rate, delay); + } + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } + + async fn set_libinput_setting( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let setting = request + .setting + .ok_or_else(|| Status::invalid_argument("no setting specified"))?; + + let discriminant = std::mem::discriminant(&setting); + + use pinnacle_api_defs::pinnacle::input::v0alpha1::set_libinput_setting_request::Setting; + let apply_setting: Box = match setting { + Setting::AccelProfile(profile) => { + let profile = AccelProfile::try_from(profile).unwrap_or(AccelProfile::Unspecified); + + match profile { + AccelProfile::Unspecified => { + return Err(Status::invalid_argument("unspecified accel profile")); + } + AccelProfile::Flat => Box::new(|device| { + let _ = device.config_accel_set_profile(libinput::AccelProfile::Flat); + }), + AccelProfile::Adaptive => Box::new(|device| { + let _ = device.config_accel_set_profile(libinput::AccelProfile::Adaptive); + }), + } + } + Setting::AccelSpeed(speed) => Box::new(move |device| { + let _ = device.config_accel_set_speed(speed); + }), + Setting::CalibrationMatrix(matrix) => { + let matrix = <[f32; 6]>::try_from(matrix.matrix).map_err(|vec| { + Status::invalid_argument(format!( + "matrix requires exactly 6 floats but {} were specified", + vec.len() + )) + })?; + + Box::new(move |device| { + let _ = device.config_calibration_set_matrix(matrix); + }) + } + Setting::ClickMethod(method) => { + let method = ClickMethod::try_from(method).unwrap_or(ClickMethod::Unspecified); + + match method { + ClickMethod::Unspecified => { + return Err(Status::invalid_argument("unspecified click method")) + } + ClickMethod::ButtonAreas => Box::new(|device| { + let _ = device.config_click_set_method(libinput::ClickMethod::ButtonAreas); + }), + ClickMethod::ClickFinger => Box::new(|device| { + let _ = device.config_click_set_method(libinput::ClickMethod::Clickfinger); + }), + } + } + Setting::DisableWhileTyping(disable) => Box::new(move |device| { + let _ = device.config_dwt_set_enabled(disable); + }), + Setting::LeftHanded(enable) => Box::new(move |device| { + let _ = device.config_left_handed_set(enable); + }), + Setting::MiddleEmulation(enable) => Box::new(move |device| { + let _ = device.config_middle_emulation_set_enabled(enable); + }), + Setting::RotationAngle(angle) => Box::new(move |device| { + let _ = device.config_rotation_set_angle(angle % 360); + }), + Setting::ScrollButton(button) => Box::new(move |device| { + let _ = device.config_scroll_set_button(button); + }), + Setting::ScrollButtonLock(enable) => Box::new(move |device| { + let _ = device.config_scroll_set_button_lock(match enable { + true => libinput::ScrollButtonLockState::Enabled, + false => libinput::ScrollButtonLockState::Disabled, + }); + }), + Setting::ScrollMethod(method) => { + let method = ScrollMethod::try_from(method).unwrap_or(ScrollMethod::Unspecified); + + match method { + ScrollMethod::Unspecified => { + return Err(Status::invalid_argument("unspecified scroll method")); + } + ScrollMethod::NoScroll => Box::new(|device| { + let _ = device.config_scroll_set_method(libinput::ScrollMethod::NoScroll); + }), + ScrollMethod::TwoFinger => Box::new(|device| { + let _ = device.config_scroll_set_method(libinput::ScrollMethod::TwoFinger); + }), + ScrollMethod::Edge => Box::new(|device| { + let _ = device.config_scroll_set_method(libinput::ScrollMethod::Edge); + }), + ScrollMethod::OnButtonDown => Box::new(|device| { + let _ = + device.config_scroll_set_method(libinput::ScrollMethod::OnButtonDown); + }), + } + } + Setting::NaturalScroll(enable) => Box::new(move |device| { + let _ = device.config_scroll_set_natural_scroll_enabled(enable); + }), + Setting::TapButtonMap(map) => { + let map = TapButtonMap::try_from(map).unwrap_or(TapButtonMap::Unspecified); + + match map { + TapButtonMap::Unspecified => { + return Err(Status::invalid_argument("unspecified tap button map")); + } + TapButtonMap::LeftRightMiddle => Box::new(|device| { + let _ = device + .config_tap_set_button_map(libinput::TapButtonMap::LeftRightMiddle); + }), + TapButtonMap::LeftMiddleRight => Box::new(|device| { + let _ = device + .config_tap_set_button_map(libinput::TapButtonMap::LeftMiddleRight); + }), + } + } + Setting::TapDrag(enable) => Box::new(move |device| { + let _ = device.config_tap_set_drag_enabled(enable); + }), + Setting::TapDragLock(enable) => Box::new(move |device| { + let _ = device.config_tap_set_drag_lock_enabled(enable); + }), + Setting::Tap(enable) => Box::new(move |device| { + let _ = device.config_tap_set_enabled(enable); + }), + }; + + let f = Box::new(move |state: &mut State| { + for device in state.input_state.libinput_devices.iter_mut() { + apply_setting(device); + } + + state + .input_state + .libinput_settings + .insert(discriminant, apply_setting); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) } } -/// Send a message to a client. -pub fn send_to_client( - stream: &mut UnixStream, - msg: &OutgoingMsg, -) -> Result<(), rmp_serde::encode::Error> { - tracing::trace!("Sending {msg:?}"); - - let msg = rmp_serde::to_vec_named(msg)?; - let msg_len = msg.len() as u32; - let bytes = msg_len.to_ne_bytes(); - - if let Err(err) = stream.write_all(&bytes) { - if err.kind() == io::ErrorKind::BrokenPipe { - // TODO: notify user that config daemon is ded - return Ok(()); // TODO: - } - } - - if let Err(err) = stream.write_all(msg.as_slice()) { - if err.kind() == io::ErrorKind::BrokenPipe { - // TODO: something - return Ok(()); // TODO: - } - } - - Ok(()) +pub struct ProcessService { + pub sender: StateFnSender, } -impl EventSource for PinnacleSocketSource { - type Event = UnixStream; +#[tonic::async_trait] +impl pinnacle_api_defs::pinnacle::process::v0alpha1::process_service_server::ProcessService + for ProcessService +{ + type SpawnStream = ResponseStream; - type Metadata = (); + async fn spawn( + &self, + request: Request, + ) -> Result, Status> { + tracing::debug!("ProcessService.spawn"); + let request = request.into_inner(); - type Ret = (); + let once = request.once(); + let has_callback = request.has_callback(); + let mut command = request.args.into_iter(); + let arg0 = command + .next() + .ok_or_else(|| Status::invalid_argument("no args specified"))?; - type Error = io::Error; + let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); - fn process_events( - &mut self, - readiness: calloop::Readiness, - token: calloop::Token, - mut callback: F, - ) -> Result - where - F: FnMut(Self::Event, &mut Self::Metadata) -> Self::Ret, - { - self.socket - .process_events(readiness, token, |_readiness, listener| { - while let Ok((stream, _sock_addr)) = listener.accept() { - let sender = self.sender.clone(); - let callback_stream = stream.try_clone()?; + let f = Box::new(move |state: &mut State| { + if once { + state + .system_processes + .refresh_processes_specifics(ProcessRefreshKind::new()); - callback(callback_stream, &mut ()); + let compositor_pid = std::process::id(); + let already_running = + state + .system_processes + .processes_by_exact_name(&arg0) + .any(|proc| { + proc.parent() + .is_some_and(|parent_pid| parent_pid.as_u32() == compositor_pid) + }); - // Handle the client in another thread as to not block the main one. - // - // No idea if this is even needed or if it's premature optimization. - std::thread::spawn(move || { - if let Err(err) = handle_client(stream, sender) { - tracing::error!("handle_client errored: {err}"); + if already_running { + return; + } + } + + let Ok(mut child) = tokio::process::Command::new(OsString::from(arg0.clone())) + .envs( + [("WAYLAND_DISPLAY", state.socket_name.clone())] + .into_iter() + .chain(state.xdisplay.map(|xdisp| ("DISPLAY", format!(":{xdisp}")))), + ) + .stdin(match has_callback { + true => Stdio::piped(), + false => Stdio::null(), + }) + .stdout(match has_callback { + true => Stdio::piped(), + false => Stdio::null(), + }) + .stderr(match has_callback { + true => Stdio::piped(), + false => Stdio::null(), + }) + .args(command) + .spawn() + else { + tracing::warn!("Tried to run {arg0}, but it doesn't exist",); + return; + }; + + if !has_callback { + return; + } + + let stdout = child.stdout.take(); + let stderr = child.stderr.take(); + + if let Some(stdout) = stdout { + let sender = sender.clone(); + + let mut reader = tokio::io::BufReader::new(stdout).lines(); + + tokio::spawn(async move { + while let Ok(Some(line)) = reader.next_line().await { + let response: Result<_, Status> = Ok(SpawnResponse { + stdout: Some(line), + ..Default::default() + }); + + // TODO: handle error + match sender.send(response) { + Ok(_) => (), + Err(err) => { + tracing::error!(err = ?err); + break; + } + } + } + }); + } + + if let Some(stderr) = stderr { + let sender = sender.clone(); + + let mut reader = tokio::io::BufReader::new(stderr).lines(); + + tokio::spawn(async move { + while let Ok(Some(line)) = reader.next_line().await { + let response: Result<_, Status> = Ok(SpawnResponse { + stderr: Some(line), + ..Default::default() + }); + + // TODO: handle error + match sender.send(response) { + Ok(_) => (), + Err(err) => { + tracing::error!(err = ?err); + break; + } + } + } + }); + } + + tokio::spawn(async move { + match child.wait().await { + Ok(exit_status) => { + let response = Ok(SpawnResponse { + exit_code: exit_status.code(), + exit_message: Some(exit_status.to_string()), + ..Default::default() + }); + // TODO: handle error + let _ = sender.send(response); + } + Err(err) => tracing::warn!("child wait() err: {err}"), + } + }); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + let receiver_stream = tokio_stream::wrappers::UnboundedReceiverStream::new(receiver); + + Ok(Response::new(Box::pin(receiver_stream))) + } + + async fn set_env(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + + let key = request + .key + .ok_or_else(|| Status::invalid_argument("no key specified"))?; + let value = request + .value + .ok_or_else(|| Status::invalid_argument("no value specified"))?; + + if key.is_empty() { + return Err(Status::invalid_argument("key was empty")); + } + + if key.contains(['\0', '=']) { + return Err(Status::invalid_argument("key contained NUL or =")); + } + + if value.contains('\0') { + return Err(Status::invalid_argument("value contained NUL")); + } + + std::env::set_var(key, value); + + Ok(Response::new(())) + } +} + +pub struct TagService { + pub sender: StateFnSender, +} + +#[tonic::async_trait] +impl pinnacle_api_defs::pinnacle::tag::v0alpha1::tag_service_server::TagService for TagService { + async fn set_active(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + + let tag_id = TagId::Some( + request + .tag_id + .ok_or_else(|| Status::invalid_argument("no tag specified"))?, + ); + + let set_or_toggle = match request.set_or_toggle { + Some( + pinnacle_api_defs::pinnacle::tag::v0alpha1::set_active_request::SetOrToggle::Set( + set, + ), + ) => Some(set), + Some( + pinnacle_api_defs::pinnacle::tag::v0alpha1::set_active_request::SetOrToggle::Toggle( + _, + ), + ) => None, + None => return Err(Status::invalid_argument("unspecified set or toggle")), + }; + + let f = Box::new(move |state: &mut State| { + let Some(tag) = tag_id.tag(state) else { + return; + }; + match set_or_toggle { + Some(set) => tag.set_active(set), + None => tag.set_active(!tag.active()), + } + + let Some(output) = tag.output(state) else { + return; + }; + + state.update_windows(&output); + state.update_focus(&output); + state.schedule_render(&output); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } + + async fn switch_to(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + + let tag_id = TagId::Some( + request + .tag_id + .ok_or_else(|| Status::invalid_argument("no tag specified"))?, + ); + + let f = Box::new(move |state: &mut State| { + let Some(tag) = tag_id.tag(state) else { return }; + let Some(output) = tag.output(state) else { return }; + + output.with_state(|state| { + for op_tag in state.tags.iter_mut() { + op_tag.set_active(false); + } + tag.set_active(true); + }); + + state.update_windows(&output); + state.update_focus(&output); + state.schedule_render(&output); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } + + async fn add(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + + let output_name = OutputName( + request + .output_name + .ok_or_else(|| Status::invalid_argument("no output specified"))?, + ); + + let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::(); + + let f = Box::new(move |state: &mut State| { + let new_tags = request + .tag_names + .into_iter() + .map(Tag::new) + .collect::>(); + + let tag_ids = new_tags + .iter() + .map(|tag| tag.id()) + .map(|id| match id { + TagId::None => unreachable!(), + TagId::Some(id) => id, + }) + .collect::>(); + + let _ = sender.send(AddResponse { tag_ids }); + + if let Some(saved_state) = state.config.connector_saved_states.get_mut(&output_name) { + let mut tags = saved_state.tags.clone(); + tags.extend(new_tags.clone()); + saved_state.tags = tags; + } else { + state.config.connector_saved_states.insert( + output_name.clone(), + crate::config::ConnectorSavedState { + tags: new_tags.clone(), + ..Default::default() + }, + ); + } + + let Some(output) = state + .space + .outputs() + .find(|output| output.name() == output_name.0) + else { + return; + }; + + output.with_state(|state| { + state.tags.extend(new_tags.clone()); + tracing::debug!("tags added, are now {:?}", state.tags); + }); + + for tag in new_tags { + for window in state.windows.iter() { + window.with_state(|state| { + for win_tag in state.tags.iter_mut() { + if win_tag.id() == tag.id() { + *win_tag = tag.clone(); + } } }); } + } + }); - Ok(PostAction::Continue) + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + let response = receiver + .recv() + .await + .ok_or_else(|| Status::internal("internal state was not running"))?; + + Ok(Response::new(response)) + } + + // TODO: test + async fn remove(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + + let tag_ids = request.tag_ids.into_iter().map(TagId::Some); + + let f = Box::new(move |state: &mut State| { + let tags_to_remove = tag_ids.flat_map(|id| id.tag(state)).collect::>(); + + for output in state.space.outputs().cloned().collect::>() { + // TODO: seriously, convert state.tags into a hashset + output.with_state(|state| { + for tag_to_remove in tags_to_remove.iter() { + state.tags.retain(|tag| tag != tag_to_remove); + } + }); + + state.update_windows(&output); + state.schedule_render(&output); + } + + for conn_saved_state in state.config.connector_saved_states.values_mut() { + for tag_to_remove in tags_to_remove.iter() { + conn_saved_state.tags.retain(|tag| tag != tag_to_remove); + } + } + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } + + async fn set_layout(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + + let tag_id = TagId::Some( + request + .tag_id + .ok_or_else(|| Status::invalid_argument("no tag specified"))?, + ); + + use pinnacle_api_defs::pinnacle::tag::v0alpha1::set_layout_request::Layout; + + // TODO: from impl + let layout = match request.layout() { + Layout::Unspecified => return Err(Status::invalid_argument("unspecified layout")), + Layout::MasterStack => crate::layout::Layout::MasterStack, + Layout::Dwindle => crate::layout::Layout::Dwindle, + Layout::Spiral => crate::layout::Layout::Spiral, + Layout::CornerTopLeft => crate::layout::Layout::CornerTopLeft, + Layout::CornerTopRight => crate::layout::Layout::CornerTopRight, + Layout::CornerBottomLeft => crate::layout::Layout::CornerBottomLeft, + Layout::CornerBottomRight => crate::layout::Layout::CornerBottomRight, + }; + + let f = Box::new(move |state: &mut State| { + let Some(tag) = tag_id.tag(state) else { return }; + + tag.set_layout(layout); + + let Some(output) = tag.output(state) else { return }; + + state.update_windows(&output); + state.schedule_render(&output); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } + + async fn get( + &self, + _request: Request, + ) -> Result, Status> { + let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::< + pinnacle_api_defs::pinnacle::tag::v0alpha1::GetResponse, + >(); + + let f = Box::new(move |state: &mut State| { + let tag_ids = state + .space + .outputs() + .flat_map(|op| op.with_state(|state| state.tags.clone())) + .map(|tag| tag.id()) + .map(|id| match id { + TagId::None => unreachable!(), + TagId::Some(id) => id, + }) + .collect::>(); + + let _ = + sender.send(pinnacle_api_defs::pinnacle::tag::v0alpha1::GetResponse { tag_ids }); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + let response = receiver + .recv() + .await + .ok_or_else(|| Status::internal("internal state was not running"))?; + + Ok(Response::new(response)) + } + + async fn get_properties( + &self, + request: Request, + ) -> Result, Status> + { + let request = request.into_inner(); + + let tag_id = TagId::Some( + request + .tag_id + .ok_or_else(|| Status::invalid_argument("no tag specified"))?, + ); + + let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::< + pinnacle_api_defs::pinnacle::tag::v0alpha1::GetPropertiesResponse, + >(); + + let f = Box::new(move |state: &mut State| { + let tag = tag_id.tag(state); + + let output_name = tag + .as_ref() + .and_then(|tag| tag.output(state)) + .map(|output| output.name()); + let active = tag.as_ref().map(|tag| tag.active()); + let name = tag.as_ref().map(|tag| tag.name()); + + let _ = sender.send( + pinnacle_api_defs::pinnacle::tag::v0alpha1::GetPropertiesResponse { + active, + name, + output_name, + }, + ); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + let response = receiver + .recv() + .await + .ok_or_else(|| Status::internal("internal state was not running"))?; + + Ok(Response::new(response)) + } +} + +pub struct OutputService { + pub sender: StateFnSender, +} + +#[tonic::async_trait] +impl pinnacle_api_defs::pinnacle::output::v0alpha1::output_service_server::OutputService + for OutputService +{ + type ConnectForAllStream = ResponseStream; + + async fn set_location( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let output_name = OutputName( + request + .output_name + .ok_or_else(|| Status::invalid_argument("no output specified"))?, + ); + + let x = request.x; + let y = request.y; + + let f = Box::new(move |state: &mut State| { + if let Some(saved_state) = state.config.connector_saved_states.get_mut(&output_name) { + if let Some(x) = x { + saved_state.loc.x = x; + } + if let Some(y) = y { + saved_state.loc.y = y; + } + } else { + state.config.connector_saved_states.insert( + output_name.clone(), + ConnectorSavedState { + loc: (x.unwrap_or_default(), y.unwrap_or_default()).into(), + ..Default::default() + }, + ); + } + + let Some(output) = output_name.output(state) else { + return; + }; + let mut loc = output.current_location(); + if let Some(x) = x { + loc.x = x; + } + if let Some(y) = y { + loc.y = y; + } + output.change_current_state(None, None, None, Some(loc)); + state.space.map_output(&output, loc); + tracing::debug!("Mapping output {} to {loc:?}", output.name()); + state.update_windows(&output); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } + + // TODO: remove this and integrate it into a signal/event system + async fn connect_for_all( + &self, + _request: Request, + ) -> Result, Status> { + tracing::trace!("OutputService.connect_for_all"); + let (sender, receiver) = + tokio::sync::mpsc::unbounded_channel::>(); + + let f = Box::new(move |state: &mut State| { + // for output in state.space.outputs() { + // let _ = sender.send(Ok(ConnectForAllResponse { + // output_name: Some(output.name()), + // })); + // tracing::debug!(name = output.name(), "sent connect_for_all"); + // } + + state.config.output_callback_senders.push(sender); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + let receiver_stream = tokio_stream::wrappers::UnboundedReceiverStream::new(receiver); + + Ok(Response::new(Box::pin(receiver_stream))) + } + + async fn get( + &self, + _request: Request, + ) -> Result, Status> { + let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::< + pinnacle_api_defs::pinnacle::output::v0alpha1::GetResponse, + >(); + + let f = Box::new(move |state: &mut State| { + let output_names = state + .space + .outputs() + .map(|output| output.name()) + .collect::>(); + + let _ = sender + .send(pinnacle_api_defs::pinnacle::output::v0alpha1::GetResponse { output_names }); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + let response = receiver + .recv() + .await + .ok_or_else(|| Status::internal("internal state was not running"))?; + + Ok(Response::new(response)) + } + + async fn get_properties( + &self, + request: Request, + ) -> Result< + Response, + Status, + > { + let request = request.into_inner(); + + let output_name = OutputName( + request + .output_name + .ok_or_else(|| Status::invalid_argument("no output specified"))?, + ); + + let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::< + pinnacle_api_defs::pinnacle::output::v0alpha1::GetPropertiesResponse, + >(); + + let f = Box::new(move |state: &mut State| { + let output = output_name.output(state); + + let pixel_width = output + .as_ref() + .and_then(|output| output.current_mode().map(|mode| mode.size.w as u32)); + + let pixel_height = output + .as_ref() + .and_then(|output| output.current_mode().map(|mode| mode.size.h as u32)); + + let refresh_rate = output + .as_ref() + .and_then(|output| output.current_mode().map(|mode| mode.refresh as u32)); + + let model = output + .as_ref() + .map(|output| output.physical_properties().model); + + let physical_width = output + .as_ref() + .map(|output| output.physical_properties().size.w as u32); + + let physical_height = output + .as_ref() + .map(|output| output.physical_properties().size.h as u32); + + let make = output + .as_ref() + .map(|output| output.physical_properties().make); + + let x = output.as_ref().map(|output| output.current_location().x); + + let y = output.as_ref().map(|output| output.current_location().y); + + let focused = state + .focus_state + .focused_output + .as_ref() + .and_then(|foc_op| output.as_ref().map(|op| op == foc_op)); + + let tag_ids = output + .as_ref() + .map(|output| { + output.with_state(|state| { + state + .tags + .iter() + .map(|tag| match tag.id() { + TagId::None => unreachable!(), + TagId::Some(id) => id, + }) + .collect::>() + }) + }) + .unwrap_or_default(); + + let _ = sender.send( + pinnacle_api_defs::pinnacle::output::v0alpha1::GetPropertiesResponse { + make, + model, + x, + y, + pixel_width, + pixel_height, + refresh_rate, + physical_width, + physical_height, + focused, + tag_ids, + }, + ); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + let response = receiver + .recv() + .await + .ok_or_else(|| Status::internal("internal state was not running"))?; + + Ok(Response::new(response)) + } +} + +pub struct WindowService { + pub sender: StateFnSender, +} + +#[tonic::async_trait] +impl pinnacle_api_defs::pinnacle::window::v0alpha1::window_service_server::WindowService + for WindowService +{ + async fn close(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + + let window_id = WindowId::Some( + request + .window_id + .ok_or_else(|| Status::invalid_argument("no window specified"))?, + ); + + let f = Box::new(move |state: &mut State| { + let Some(window) = window_id.window(state) else { return }; + + match window { + WindowElement::Wayland(window) => window.toplevel().send_close(), + WindowElement::X11(surface) => surface.close().expect("failed to close x11 win"), + WindowElement::X11OverrideRedirect(_) => { + tracing::warn!("tried to close override redirect window"); + } + _ => unreachable!(), + } + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } + + async fn set_geometry( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let window_id = WindowId::Some( + request + .window_id + .ok_or_else(|| Status::invalid_argument("no window specified"))?, + ); + + let geometry = request.geometry.unwrap_or_default(); + let x = geometry.x; + let y = geometry.y; + let width = geometry.width; + let height = geometry.height; + + let f = Box::new(move |state: &mut State| { + let Some(window) = window_id.window(state) else { return }; + + // TODO: with no x or y, defaults unmapped windows to 0, 0 + let mut window_loc = state + .space + .element_location(&window) + .unwrap_or((x.unwrap_or_default(), y.unwrap_or_default()).into()); + window_loc.x = x.unwrap_or(window_loc.x); + window_loc.y = y.unwrap_or(window_loc.y); + + let mut window_size = window.geometry().size; + window_size.w = width.unwrap_or(window_size.w); + window_size.h = height.unwrap_or(window_size.h); + + let rect = Rectangle::from_loc_and_size(window_loc, window_size); + // window.change_geometry(rect); + window.with_state(|state| { + use crate::window::window_state::FloatingOrTiled; + state.floating_or_tiled = match state.floating_or_tiled { + FloatingOrTiled::Floating(_) => FloatingOrTiled::Floating(rect), + FloatingOrTiled::Tiled(_) => FloatingOrTiled::Tiled(Some(rect)), + } + }); + + for output in state.space.outputs_for_element(&window) { + state.update_windows(&output); + state.schedule_render(&output); + } + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } + + async fn set_fullscreen( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let window_id = WindowId::Some( + request + .window_id + .ok_or_else(|| Status::invalid_argument("no window specified"))?, + ); + + let set_or_toggle = match request.set_or_toggle { + Some(pinnacle_api_defs::pinnacle::window::v0alpha1::set_fullscreen_request::SetOrToggle::Set(set)) => { + Some(set) + } + Some(pinnacle_api_defs::pinnacle::window::v0alpha1::set_fullscreen_request::SetOrToggle::Toggle(_)) => { + None + } + None => return Err(Status::invalid_argument("unspecified set or toggle")), + }; + + let f = Box::new(move |state: &mut State| { + let Some(window) = window_id.window(state) else { + return; + }; + match set_or_toggle { + Some(set) => { + let is_fullscreen = + window.with_state(|state| state.fullscreen_or_maximized.is_fullscreen()); + if set != is_fullscreen { + window.toggle_fullscreen(); + } + } + None => window.toggle_fullscreen(), + } + + let Some(output) = window.output(state) else { + return; + }; + + state.update_windows(&output); + state.schedule_render(&output); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } + + async fn set_maximized( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let window_id = WindowId::Some( + request + .window_id + .ok_or_else(|| Status::invalid_argument("no window specified"))?, + ); + + let set_or_toggle = match request.set_or_toggle { + Some(pinnacle_api_defs::pinnacle::window::v0alpha1::set_maximized_request::SetOrToggle::Set(set)) => { + Some(set) + } + Some(pinnacle_api_defs::pinnacle::window::v0alpha1::set_maximized_request::SetOrToggle::Toggle(_)) => None, + None => return Err(Status::invalid_argument("unspecified set or toggle")), + }; + + let f = Box::new(move |state: &mut State| { + let Some(window) = window_id.window(state) else { + return; + }; + match set_or_toggle { + Some(set) => { + let is_maximized = + window.with_state(|state| state.fullscreen_or_maximized.is_maximized()); + if set != is_maximized { + window.toggle_maximized(); + } + } + None => window.toggle_maximized(), + } + + let Some(output) = window.output(state) else { + return; + }; + + state.update_windows(&output); + state.schedule_render(&output); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } + + async fn set_floating( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let window_id = WindowId::Some( + request + .window_id + .ok_or_else(|| Status::invalid_argument("no window specified"))?, + ); + + let set_or_toggle = match request.set_or_toggle { + Some(pinnacle_api_defs::pinnacle::window::v0alpha1::set_floating_request::SetOrToggle::Set(set)) => { + Some(set) + } + Some(pinnacle_api_defs::pinnacle::window::v0alpha1::set_floating_request::SetOrToggle::Toggle(_)) => None, + None => return Err(Status::invalid_argument("unspecified set or toggle")), + }; + + let f = Box::new(move |state: &mut State| { + let Some(window) = window_id.window(state) else { + return; + }; + match set_or_toggle { + Some(set) => { + let is_floating = + window.with_state(|state| state.floating_or_tiled.is_floating()); + if set != is_floating { + window.toggle_floating(); + } + } + None => window.toggle_floating(), + } + + let Some(output) = window.output(state) else { + return; + }; + + state.update_windows(&output); + state.schedule_render(&output); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } + + async fn move_to_tag( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let window_id = WindowId::Some( + request + .window_id + .ok_or_else(|| Status::invalid_argument("no window specified"))?, + ); + + let tag_id = TagId::Some( + request + .tag_id + .ok_or_else(|| Status::invalid_argument("no tag specified"))?, + ); + + let f = Box::new(move |state: &mut State| { + let Some(window) = window_id.window(state) else { return }; + let Some(tag) = tag_id.tag(state) else { return }; + window.with_state(|state| { + state.tags = vec![tag.clone()]; + }); + let Some(output) = tag.output(state) else { return }; + state.update_windows(&output); + state.schedule_render(&output); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } + + async fn set_tag(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + + let window_id = WindowId::Some( + request + .window_id + .ok_or_else(|| Status::invalid_argument("no window specified"))?, + ); + + let tag_id = TagId::Some( + request + .tag_id + .ok_or_else(|| Status::invalid_argument("no tag specified"))?, + ); + + let set_or_toggle = match request.set_or_toggle { + Some( + pinnacle_api_defs::pinnacle::window::v0alpha1::set_tag_request::SetOrToggle::Set( + set, + ), + ) => Some(set), + Some( + pinnacle_api_defs::pinnacle::window::v0alpha1::set_tag_request::SetOrToggle::Toggle( + _, + ), + ) => None, + None => return Err(Status::invalid_argument("unspecified set or toggle")), + }; + + let f = Box::new(move |state: &mut State| { + let Some(window) = window_id.window(state) else { return }; + let Some(tag) = tag_id.tag(state) else { return }; + + // TODO: turn state.tags into a hashset + match set_or_toggle { + Some(set) => { + if set { + window.with_state(|state| { + state.tags.retain(|tg| tg != &tag); + state.tags.push(tag.clone()); + }) + } else { + window.with_state(|state| { + state.tags.retain(|tg| tg != &tag); + }) + } + } + None => window.with_state(|state| { + if !state.tags.contains(&tag) { + state.tags.push(tag.clone()); + } else { + state.tags.retain(|tg| tg != &tag); + } + }), + } + + let Some(output) = tag.output(state) else { return }; + state.update_windows(&output); + state.schedule_render(&output); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } + + async fn move_grab(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + + let button = request + .button + .ok_or_else(|| Status::invalid_argument("no button specified"))?; + + let f = Box::new(move |state: &mut State| { + let Some((FocusTarget::Window(window), _)) = + state.focus_target_under(state.pointer_location) + else { + return; + }; + let Some(wl_surf) = window.wl_surface() else { return }; + let seat = state.seat.clone(); + + // We use the server one and not the client because windows like Steam don't provide + // GrabStartData, so we need to create it ourselves. + crate::grab::move_grab::move_request_server( + state, + &wl_surf, + &seat, + SERIAL_COUNTER.next_serial(), + button, + ); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } + + async fn resize_grab( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let button = request + .button + .ok_or_else(|| Status::invalid_argument("no button specified"))?; + + let f = Box::new(move |state: &mut State| { + let pointer_loc = state.pointer_location; + let Some((FocusTarget::Window(window), window_loc)) = + state.focus_target_under(pointer_loc) + else { + return; + }; + let Some(wl_surf) = window.wl_surface() else { return }; + + let window_geometry = window.geometry(); + let window_x = window_loc.x as f64; + let window_y = window_loc.y as f64; + let window_width = window_geometry.size.w as f64; + let window_height = window_geometry.size.h as f64; + let half_width = window_x + window_width / 2.0; + let half_height = window_y + window_height / 2.0; + let full_width = window_x + window_width; + let full_height = window_y + window_height; + + let edges = match pointer_loc { + Point { x, y, .. } + if (window_x..=half_width).contains(&x) + && (window_y..=half_height).contains(&y) => + { + server::xdg_toplevel::ResizeEdge::TopLeft + } + Point { x, y, .. } + if (half_width..=full_width).contains(&x) + && (window_y..=half_height).contains(&y) => + { + server::xdg_toplevel::ResizeEdge::TopRight + } + Point { x, y, .. } + if (window_x..=half_width).contains(&x) + && (half_height..=full_height).contains(&y) => + { + server::xdg_toplevel::ResizeEdge::BottomLeft + } + Point { x, y, .. } + if (half_width..=full_width).contains(&x) + && (half_height..=full_height).contains(&y) => + { + server::xdg_toplevel::ResizeEdge::BottomRight + } + _ => server::xdg_toplevel::ResizeEdge::None, + }; + + crate::grab::resize_grab::resize_request_server( + state, + &wl_surf, + &state.seat.clone(), + SERIAL_COUNTER.next_serial(), + edges.into(), + button, + ); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } + + async fn get( + &self, + _request: Request, + ) -> Result, Status> { + let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::< + pinnacle_api_defs::pinnacle::window::v0alpha1::GetResponse, + >(); + + let f = Box::new(move |state: &mut State| { + let window_ids = state + .windows + .iter() + .map(|win| { + win.with_state(|state| match state.id { + WindowId::None => unreachable!(), + WindowId::Some(id) => id, + }) + }) + .collect::>(); + + let _ = sender + .send(pinnacle_api_defs::pinnacle::window::v0alpha1::GetResponse { window_ids }); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + let response = receiver + .recv() + .await + .ok_or_else(|| Status::internal("internal state was not running"))?; + + Ok(Response::new(response)) + } + + async fn get_properties( + &self, + request: Request, + ) -> Result< + Response, + Status, + > { + let request = request.into_inner(); + + let window_id = WindowId::Some( + request + .window_id + .ok_or_else(|| Status::invalid_argument("no window specified"))?, + ); + + let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::< + pinnacle_api_defs::pinnacle::window::v0alpha1::GetPropertiesResponse, + >(); + + let f = Box::new(move |state: &mut State| { + let window = window_id.window(state); + + let width = window.as_ref().map(|win| win.geometry().size.w); + + let height = window.as_ref().map(|win| win.geometry().size.h); + + let x = window + .as_ref() + .and_then(|win| state.space.element_location(win)) + .map(|loc| loc.x); + + let y = window + .as_ref() + .and_then(|win| state.space.element_location(win)) + .map(|loc| loc.y); + + let geometry = if width.is_none() && height.is_none() && x.is_none() && y.is_none() { + None + } else { + Some(Geometry { + x, + y, + width, + height, + }) + }; + + let (class, title) = window.as_ref().map_or((None, None), |win| match &win { + WindowElement::Wayland(_) => { + if let Some(wl_surf) = win.wl_surface() { + compositor::with_states(&wl_surf, |states| { + let lock = states + .data_map + .get::() + .expect("XdgToplevelSurfaceData wasn't in surface's data map") + .lock() + .expect("failed to acquire lock"); + (lock.app_id.clone(), lock.title.clone()) + }) + } else { + (None, None) + } + } + WindowElement::X11(surface) | WindowElement::X11OverrideRedirect(surface) => { + (Some(surface.class()), Some(surface.title())) + } + _ => unreachable!(), + }); + + let focused = window.as_ref().and_then(|win| { + let output = win.output(state)?; + state.focused_window(&output).map(|foc_win| win == &foc_win) + }); + + let floating = window + .as_ref() + .map(|win| win.with_state(|state| state.floating_or_tiled.is_floating())); + + let fullscreen_or_maximized = window + .as_ref() + .map(|win| win.with_state(|state| state.fullscreen_or_maximized)) + .map(|fs_or_max| match fs_or_max { + // TODO: from impl + crate::window::window_state::FullscreenOrMaximized::Neither => { + FullscreenOrMaximized::Neither + } + crate::window::window_state::FullscreenOrMaximized::Fullscreen => { + FullscreenOrMaximized::Fullscreen + } + crate::window::window_state::FullscreenOrMaximized::Maximized => { + FullscreenOrMaximized::Maximized + } + } as i32); + + let tag_ids = window + .as_ref() + .map(|win| { + win.with_state(|state| { + state + .tags + .iter() + .map(|tag| match tag.id() { + TagId::Some(id) => id, + TagId::None => unreachable!(), + }) + .collect::>() + }) + }) + .unwrap_or_default(); + + let _ = sender.send( + pinnacle_api_defs::pinnacle::window::v0alpha1::GetPropertiesResponse { + geometry, + class, + title, + focused, + floating, + fullscreen_or_maximized, + tag_ids, + }, + ); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + let response = receiver + .recv() + .await + .ok_or_else(|| Status::internal("internal state was not running"))?; + + Ok(Response::new(response)) + } + + async fn add_window_rule( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let cond = request + .cond + .ok_or_else(|| Status::invalid_argument("no condition specified"))? + .into(); + + let rule = request + .rule + .ok_or_else(|| Status::invalid_argument("no rule specified"))? + .into(); + + let f = Box::new(move |state: &mut State| { + state.config.window_rules.push((cond, rule)); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } +} + +impl From for crate::window::rules::WindowRuleCondition { + fn from(cond: WindowRuleCondition) -> Self { + let cond_any = match cond.any.is_empty() { + true => None, + false => Some( + cond.any + .into_iter() + .map(crate::window::rules::WindowRuleCondition::from) + .collect::>(), + ), + }; + + let cond_all = match cond.all.is_empty() { + true => None, + false => Some( + cond.all + .into_iter() + .map(crate::window::rules::WindowRuleCondition::from) + .collect::>(), + ), + }; + + let class = match cond.classes.is_empty() { + true => None, + false => Some(cond.classes), + }; + + let title = match cond.titles.is_empty() { + true => None, + false => Some(cond.titles), + }; + + let tag = match cond.tags.is_empty() { + true => None, + false => Some(cond.tags.into_iter().map(TagId::Some).collect::>()), + }; + + crate::window::rules::WindowRuleCondition { + cond_any, + cond_all, + class, + title, + tag, + } + } +} + +impl From for crate::window::rules::WindowRule { + fn from(rule: WindowRule) -> Self { + let fullscreen_or_maximized = match rule.fullscreen_or_maximized() { + FullscreenOrMaximized::Unspecified => None, + FullscreenOrMaximized::Neither => { + Some(crate::window::window_state::FullscreenOrMaximized::Neither) + } + FullscreenOrMaximized::Fullscreen => { + Some(crate::window::window_state::FullscreenOrMaximized::Fullscreen) + } + FullscreenOrMaximized::Maximized => { + Some(crate::window::window_state::FullscreenOrMaximized::Maximized) + } + }; + let output = rule.output.map(OutputName); + let tags = match rule.tags.is_empty() { + true => None, + false => Some(rule.tags.into_iter().map(TagId::Some).collect::>()), + }; + let floating_or_tiled = rule.floating.map(|floating| match floating { + true => crate::window::rules::FloatingOrTiled::Floating, + false => crate::window::rules::FloatingOrTiled::Tiled, + }); + let size = rule.width.and_then(|w| { + rule.height.and_then(|h| { + Some(( + NonZeroU32::try_from(w as u32).ok()?, + NonZeroU32::try_from(h as u32).ok()?, + )) }) - } + }); + let location = rule.x.and_then(|x| rule.y.map(|y| (x, y))); - fn register( - &mut self, - poll: &mut calloop::Poll, - token_factory: &mut calloop::TokenFactory, - ) -> calloop::Result<()> { - self.socket.register(poll, token_factory) - } - - fn reregister( - &mut self, - poll: &mut calloop::Poll, - token_factory: &mut calloop::TokenFactory, - ) -> calloop::Result<()> { - self.socket.reregister(poll, token_factory) - } - - fn unregister(&mut self, poll: &mut calloop::Poll) -> calloop::Result<()> { - self.socket.unregister(poll) + crate::window::rules::WindowRule { + output, + tags, + floating_or_tiled, + fullscreen_or_maximized, + size, + location, + } } } - -pub struct ApiState { - // TODO: this may not need to be in an arc mutex because of the move to async - /// The stream API messages are being sent through. - pub stream: Option>>, - /// A token used to remove the socket source from the event loop on config restart. - pub socket_token: Option, - /// The sending channel used to send API messages received from the socket source to a handler. - pub tx_channel: Sender, -} diff --git a/src/api/handlers.rs b/src/api/handlers.rs deleted file mode 100644 index 0373f82..0000000 --- a/src/api/handlers.rs +++ /dev/null @@ -1,824 +0,0 @@ -use std::{ffi::OsString, process::Stdio}; - -use smithay::{ - desktop::space::SpaceElement, - input::keyboard::XkbConfig, - reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::ResizeEdge, - utils::{Point, Rectangle, SERIAL_COUNTER}, - wayland::{compositor, shell::xdg::XdgToplevelSurfaceData}, -}; -use sysinfo::ProcessRefreshKind; -use tokio::io::AsyncBufReadExt; - -use crate::{ - api::msg::{ - Args, CallbackId, KeyIntOrString, Msg, OutgoingMsg, Request, RequestId, RequestResponse, - }, - config::ConnectorSavedState, - focus::FocusTarget, - tag::Tag, - window::WindowElement, -}; - -use crate::state::{State, WithState}; - -impl State { - /// Handle a client message. - pub fn handle_msg(&mut self, msg: Msg) { - tracing::trace!("Got {msg:?}"); - - match msg { - Msg::SetKeybind { - key, - modifiers, - callback_id, - } => { - let key = match key { - KeyIntOrString::Int(num) => { - tracing::info!("set keybind: {:?}, raw {}", modifiers, num); - num - } - KeyIntOrString::String(s) => { - if s.chars().count() == 1 { - let Some(ch) = s.chars().next() else { unreachable!() }; - let raw = xkbcommon::xkb::Keysym::from_char(ch).raw(); - tracing::info!("set keybind: {:?}, {:?} (raw {})", modifiers, ch, raw); - raw - } else { - let raw = xkbcommon::xkb::keysym_from_name( - &s, - xkbcommon::xkb::KEYSYM_NO_FLAGS, - ) - .raw(); - tracing::info!("set keybind: {:?}, {:?}", modifiers, raw); - raw - } - } - }; - - self.input_state - .keybinds - .insert((modifiers.into(), key.into()), callback_id); - } - Msg::SetMousebind { - modifiers, - button, - edge, - callback_id, - } => { - // TODO: maybe validate/parse valid codes? - self.input_state - .mousebinds - .insert((modifiers.into(), button, edge), callback_id); - } - Msg::CloseWindow { window_id } => { - if let Some(window) = window_id.window(self) { - match window { - WindowElement::Wayland(window) => window.toplevel().send_close(), - WindowElement::X11(surface) => { - surface.close().expect("failed to close x11 win"); - } - WindowElement::X11OverrideRedirect(_) => (), - _ => unreachable!(), - } - } - } - - Msg::Spawn { - command, - callback_id, - } => { - self.handle_spawn(command, callback_id); - } - Msg::SpawnOnce { - command, - callback_id, - } => { - self.system_processes - .refresh_processes_specifics(ProcessRefreshKind::new()); - - let Some(arg0) = command.first() else { - tracing::warn!("No command specified for `SpawnOnce`"); - return; - }; - - let compositor_pid = std::process::id(); - let already_running = - self.system_processes - .processes_by_exact_name(arg0) - .any(|proc| { - proc.parent() - .is_some_and(|parent_pid| parent_pid.as_u32() == compositor_pid) - }); - - if !already_running { - self.handle_spawn(command, callback_id); - } - } - Msg::SetEnv { key, value } => std::env::set_var(key, value), - - Msg::SetWindowSize { - window_id, - width, - height, - } => { - let Some(window) = window_id.window(self) else { return }; - - // TODO: tiled vs floating - // FIXME: this will map unmapped windows at 0,0 - let window_loc = self - .space - .element_location(&window) - .unwrap_or((0, 0).into()); - let mut window_size = window.geometry().size; - if let Some(width) = width { - window_size.w = width; - } - if let Some(height) = height { - window_size.h = height; - } - use crate::window::window_state::FloatingOrTiled; - - let rect = Rectangle::from_loc_and_size(window_loc, window_size); - window.change_geometry(rect); - window.with_state(|state| { - state.floating_or_tiled = match state.floating_or_tiled { - FloatingOrTiled::Floating(_) => FloatingOrTiled::Floating(rect), - FloatingOrTiled::Tiled(_) => FloatingOrTiled::Tiled(Some(rect)), - } - }); - - for output in self.space.outputs_for_element(&window) { - self.update_windows(&output); - self.schedule_render(&output); - } - } - Msg::MoveWindowToTag { window_id, tag_id } => { - let Some(window) = window_id.window(self) else { return }; - let Some(tag) = tag_id.tag(self) else { return }; - window.with_state(|state| { - state.tags = vec![tag.clone()]; - }); - let Some(output) = tag.output(self) else { return }; - self.update_windows(&output); - self.schedule_render(&output); - } - Msg::ToggleTagOnWindow { window_id, tag_id } => { - let Some(window) = window_id.window(self) else { return }; - let Some(tag) = tag_id.tag(self) else { return }; - - window.with_state(|state| { - if state.tags.contains(&tag) { - state.tags.retain(|tg| tg != &tag); - } else { - state.tags.push(tag.clone()); - } - }); - - let Some(output) = tag.output(self) else { return }; - self.update_windows(&output); - self.schedule_render(&output); - } - Msg::ToggleFloating { window_id } => { - let Some(window) = window_id.window(self) else { return }; - window.toggle_floating(); - - let Some(output) = window.output(self) else { return }; - self.update_windows(&output); - self.schedule_render(&output); - } - Msg::ToggleFullscreen { window_id } => { - let Some(window) = window_id.window(self) else { return }; - window.toggle_fullscreen(); - - let Some(output) = window.output(self) else { return }; - self.update_windows(&output); - self.schedule_render(&output); - } - Msg::ToggleMaximized { window_id } => { - let Some(window) = window_id.window(self) else { return }; - window.toggle_maximized(); - - let Some(output) = window.output(self) else { return }; - self.update_windows(&output); - self.schedule_render(&output); - } - Msg::AddWindowRule { cond, rule } => { - self.config.window_rules.push((cond, rule)); - } - Msg::WindowMoveGrab { button } => { - let Some((FocusTarget::Window(window), _)) = - self.focus_target_under(self.pointer_location) - else { - return; - }; - let Some(wl_surf) = window.wl_surface() else { return }; - let seat = self.seat.clone(); - - // We use the server one and not the client because windows like Steam don't provide - // GrabStartData, so we need to create it ourselves. - crate::grab::move_grab::move_request_server( - self, - &wl_surf, - &seat, - SERIAL_COUNTER.next_serial(), - button, - ); - } - Msg::WindowResizeGrab { button } => { - // TODO: in the future, there may be movable layer surfaces - let pointer_loc = self.pointer_location; - let Some((FocusTarget::Window(window), window_loc)) = - self.focus_target_under(pointer_loc) - else { - return; - }; - let Some(wl_surf) = window.wl_surface() else { return }; - - let window_geometry = window.geometry(); - let window_x = window_loc.x as f64; - let window_y = window_loc.y as f64; - let window_width = window_geometry.size.w as f64; - let window_height = window_geometry.size.h as f64; - let half_width = window_x + window_width / 2.0; - let half_height = window_y + window_height / 2.0; - let full_width = window_x + window_width; - let full_height = window_y + window_height; - - let edges = match pointer_loc { - Point { x, y, .. } - if (window_x..=half_width).contains(&x) - && (window_y..=half_height).contains(&y) => - { - ResizeEdge::TopLeft - } - Point { x, y, .. } - if (half_width..=full_width).contains(&x) - && (window_y..=half_height).contains(&y) => - { - ResizeEdge::TopRight - } - Point { x, y, .. } - if (window_x..=half_width).contains(&x) - && (half_height..=full_height).contains(&y) => - { - ResizeEdge::BottomLeft - } - Point { x, y, .. } - if (half_width..=full_width).contains(&x) - && (half_height..=full_height).contains(&y) => - { - ResizeEdge::BottomRight - } - _ => ResizeEdge::None, - }; - - crate::grab::resize_grab::resize_request_server( - self, - &wl_surf, - &self.seat.clone(), - SERIAL_COUNTER.next_serial(), - edges.into(), - button, - ); - } - - // Tags ---------------------------------------- - Msg::ToggleTag { tag_id } => { - tracing::debug!("ToggleTag"); - if let Some(tag) = tag_id.tag(self) { - tag.set_active(!tag.active()); - if let Some(output) = tag.output(self) { - self.update_windows(&output); - self.update_focus(&output); - self.schedule_render(&output); - } - } - } - Msg::SwitchToTag { tag_id } => { - let Some(tag) = tag_id.tag(self) else { return }; - let Some(output) = tag.output(self) else { return }; - output.with_state(|state| { - for op_tag in state.tags.iter_mut() { - op_tag.set_active(false); - } - tag.set_active(true); - }); - self.update_windows(&output); - self.update_focus(&output); - self.schedule_render(&output); - } - Msg::AddTags { - output_name, - tag_names, - } => { - let new_tags = tag_names.into_iter().map(Tag::new).collect::>(); - if let Some(saved_state) = self.config.connector_saved_states.get_mut(&output_name) - { - let mut tags = saved_state.tags.clone(); - tags.extend(new_tags.clone()); - saved_state.tags = tags; - } else { - self.config.connector_saved_states.insert( - output_name.clone(), - ConnectorSavedState { - tags: new_tags.clone(), - ..Default::default() - }, - ); - } - - if let Some(output) = self - .space - .outputs() - .find(|output| output.name() == output_name.0) - { - output.with_state(|state| { - state.tags.extend(new_tags.clone()); - tracing::debug!("tags added, are now {:?}", state.tags); - }); - - // replace tags that windows have that are the same id - // (this should only happen on config reload) - for tag in new_tags { - for window in self.windows.iter() { - window.with_state(|state| { - for win_tag in state.tags.iter_mut() { - if win_tag.id() == tag.id() { - *win_tag = tag.clone(); - } - } - }); - } - } - } - } - Msg::RemoveTags { tag_ids } => { - let tags = tag_ids - .into_iter() - .filter_map(|tag_id| tag_id.tag(self)) - .collect::>(); - - for tag in tags { - for saved_state in self.config.connector_saved_states.values_mut() { - saved_state.tags.retain(|tg| tg != &tag); - } - let Some(output) = tag.output(self) else { continue }; - output.with_state(|state| { - state.tags.retain(|tg| tg != &tag); - }); - } - } - Msg::SetLayout { tag_id, layout } => { - let Some(tag) = tag_id.tag(self) else { return }; - tag.set_layout(layout); - let Some(output) = tag.output(self) else { return }; - self.update_windows(&output); - - self.schedule_render(&output); - } - - Msg::ConnectForAllOutputs { callback_id } => { - let stream = self - .api_state - .stream - .as_ref() - .expect("stream doesn't exist"); - - for output in self.space.outputs() { - crate::api::send_to_client( - &mut stream.lock().expect("couldn't lock stream"), - &OutgoingMsg::CallCallback { - callback_id, - args: Some(Args::ConnectForAllOutputs { - output_name: output.name(), - }), - }, - ) - .expect("Send to client failed"); - } - - self.config.output_callback_ids.push(callback_id); - } - Msg::SetOutputLocation { output_name, x, y } => { - if let Some(saved_state) = self.config.connector_saved_states.get_mut(&output_name) - { - if let Some(x) = x { - saved_state.loc.x = x; - } - if let Some(y) = y { - saved_state.loc.y = y; - } - } else { - self.config.connector_saved_states.insert( - output_name.clone(), - ConnectorSavedState { - loc: (x.unwrap_or_default(), y.unwrap_or_default()).into(), - ..Default::default() - }, - ); - } - - let Some(output) = output_name.output(self) else { return }; - let mut loc = output.current_location(); - if let Some(x) = x { - loc.x = x; - } - if let Some(y) = y { - loc.y = y; - } - output.change_current_state(None, None, None, Some(loc)); - self.space.map_output(&output, loc); - tracing::debug!("Mapping output {} to {loc:?}", output.name()); - self.update_windows(&output); - } - - Msg::Quit => { - tracing::info!("Quitting Pinnacle"); - self.shutdown(); - } - - Msg::SetXkbConfig { - rules, - variant, - layout, - model, - options, - } => { - let new_config = XkbConfig { - rules: &rules.unwrap_or_default(), - model: &model.unwrap_or_default(), - layout: &layout.unwrap_or_default(), - variant: &variant.unwrap_or_default(), - options, - }; - if let Some(kb) = self.seat.get_keyboard() { - if let Err(err) = kb.set_xkb_config(self, new_config) { - tracing::error!("Failed to set xkbconfig: {err}"); - } - } - } - - Msg::SetLibinputSetting(setting) => { - for device in self.input_state.libinput_devices.iter_mut() { - // We're just gonna indiscriminately apply everything and ignore errors - setting.apply_to_device(device); - } - - self.input_state.libinput_settings.push(setting); - } - - Msg::Request { - request_id, - request, - } => { - self.handle_request(request_id, request); - } - } - } - - /// Handle a client request. - fn handle_request(&mut self, request_id: RequestId, request: Request) { - let stream = self - .api_state - .stream - .clone() // clone due to use of self below - .expect("Stream doesn't exist"); - let mut stream = stream.lock().expect("Couldn't lock stream"); - - match request { - Request::GetWindows => { - let window_ids = self - .windows - .iter() - .map(|win| win.with_state(|state| state.id)) - .collect::>(); - - crate::api::send_to_client( - &mut stream, - &OutgoingMsg::RequestResponse { - request_id, - response: RequestResponse::Windows { window_ids }, - }, - ) - .expect("Couldn't send to client"); - } - Request::GetWindowProps { window_id } => { - let window = window_id.window(self); - - let size = window - .as_ref() - .map(|win| (win.geometry().size.w, win.geometry().size.h)); - - let loc = window - .as_ref() - .and_then(|win| self.space.element_location(win)) - .map(|loc| (loc.x, loc.y)); - - let (class, title) = window.as_ref().map_or((None, None), |win| match &win { - WindowElement::Wayland(_) => { - if let Some(wl_surf) = win.wl_surface() { - compositor::with_states(&wl_surf, |states| { - let lock = states - .data_map - .get::() - .expect("XdgToplevelSurfaceData wasn't in surface's data map") - .lock() - .expect("failed to acquire lock"); - (lock.app_id.clone(), lock.title.clone()) - }) - } else { - (None, None) - } - } - WindowElement::X11(surface) | WindowElement::X11OverrideRedirect(surface) => { - (Some(surface.class()), Some(surface.title())) - } - _ => unreachable!(), - }); - - let focused = window.as_ref().and_then(|win| { - let output = win.output(self)?; - self.focused_window(&output).map(|foc_win| win == &foc_win) - }); - - let floating = window - .as_ref() - .map(|win| win.with_state(|state| state.floating_or_tiled.is_floating())); - - let fullscreen_or_maximized = window - .as_ref() - .map(|win| win.with_state(|state| state.fullscreen_or_maximized)); - - crate::api::send_to_client( - &mut stream, - &OutgoingMsg::RequestResponse { - request_id, - response: RequestResponse::WindowProps { - size, - loc, - class, - title, - focused, - floating, - fullscreen_or_maximized, - }, - }, - ) - .expect("failed to send to client"); - } - Request::GetOutputs => { - let output_names = self - .space - .outputs() - .map(|output| output.name()) - .collect::>(); - - crate::api::send_to_client( - &mut stream, - &OutgoingMsg::RequestResponse { - request_id, - response: RequestResponse::Outputs { output_names }, - }, - ) - .expect("failed to send to client"); - } - Request::GetOutputProps { output_name } => { - let output = self - .space - .outputs() - .find(|output| output.name() == output_name); - - let res = output.as_ref().and_then(|output| { - output.current_mode().map(|mode| (mode.size.w, mode.size.h)) - }); - - let refresh_rate = output - .as_ref() - .and_then(|output| output.current_mode().map(|mode| mode.refresh)); - - let model = output - .as_ref() - .map(|output| output.physical_properties().model); - - let physical_size = output.as_ref().map(|output| { - ( - output.physical_properties().size.w, - output.physical_properties().size.h, - ) - }); - - let make = output - .as_ref() - .map(|output| output.physical_properties().make); - - let loc = output - .as_ref() - .map(|output| (output.current_location().x, output.current_location().y)); - - let focused = self - .focus_state - .focused_output - .as_ref() - .and_then(|foc_op| output.map(|op| op == foc_op)); - - let tag_ids = output.as_ref().map(|output| { - output.with_state(|state| { - state.tags.iter().map(|tag| tag.id()).collect::>() - }) - }); - - crate::api::send_to_client( - &mut stream, - &OutgoingMsg::RequestResponse { - request_id, - response: RequestResponse::OutputProps { - make, - model, - loc, - res, - refresh_rate, - physical_size, - focused, - tag_ids, - }, - }, - ) - .expect("failed to send to client"); - } - Request::GetTags => { - let tag_ids = self - .space - .outputs() - .flat_map(|op| op.with_state(|state| state.tags.clone())) - .map(|tag| tag.id()) - .collect::>(); - - crate::api::send_to_client( - &mut stream, - &OutgoingMsg::RequestResponse { - request_id, - response: RequestResponse::Tags { tag_ids }, - }, - ) - .expect("failed to send to client"); - } - Request::GetTagProps { tag_id } => { - let tag = tag_id.tag(self); - - let output_name = tag - .as_ref() - .and_then(|tag| tag.output(self)) - .map(|output| output.name()); - - let active = tag.as_ref().map(|tag| tag.active()); - let name = tag.as_ref().map(|tag| tag.name()); - - crate::api::send_to_client( - &mut stream, - &OutgoingMsg::RequestResponse { - request_id, - response: RequestResponse::TagProps { - active, - name, - output_name, - }, - }, - ) - .expect("failed to send to client"); - } - } - } - - // Welcome to indentation hell - /// Handle a received spawn command by spawning the command and hooking up any callbacks. - pub fn handle_spawn(&self, command: Vec, callback_id: Option) { - let mut command = command.into_iter(); - let Some(program) = command.next() else { - // TODO: notify that command was nothing - tracing::warn!("got an empty command"); - return; - }; - - let program = OsString::from(program); - let Ok(mut child) = tokio::process::Command::new(&program) - .envs( - [("WAYLAND_DISPLAY", self.socket_name.clone())] - .into_iter() - .chain(self.xdisplay.map(|xdisp| ("DISPLAY", format!(":{xdisp}")))), - ) - .stdin(if callback_id.is_some() { - Stdio::piped() - } else { - // piping to null because foot won't open without a callback_id - // otherwise - Stdio::null() - }) - .stdout(if callback_id.is_some() { - Stdio::piped() - } else { - Stdio::null() - }) - .stderr(if callback_id.is_some() { - Stdio::piped() - } else { - Stdio::null() - }) - .args(command) - .spawn() - else { - // TODO: notify user that program doesn't exist - tracing::warn!( - "Tried to run {}, but it doesn't exist", - program.to_string_lossy() - ); - return; - }; - - if let Some(callback_id) = callback_id { - let stdout = child.stdout.take(); - let stderr = child.stderr.take(); - - let stream_out = self.api_state.stream.clone().expect("Stream doesn't exist"); - let stream_err = stream_out.clone(); - let stream_exit = stream_out.clone(); - - if let Some(stdout) = stdout { - let future = async move { - let mut reader = tokio::io::BufReader::new(stdout).lines(); - while let Ok(Some(line)) = reader.next_line().await { - let msg = OutgoingMsg::CallCallback { - callback_id, - args: Some(Args::Spawn { - stdout: Some(line), - stderr: None, - exit_code: None, - exit_msg: None, - }), - }; - - crate::api::send_to_client( - &mut stream_out.lock().expect("Couldn't lock stream"), - &msg, - ) - .expect("Send to client failed"); // TODO: notify instead of crash - } - }; - - tokio::spawn(future); - } - - if let Some(stderr) = stderr { - let future = async move { - let mut reader = tokio::io::BufReader::new(stderr).lines(); - while let Ok(Some(line)) = reader.next_line().await { - let msg = OutgoingMsg::CallCallback { - callback_id, - args: Some(Args::Spawn { - stdout: None, - stderr: Some(line), - exit_code: None, - exit_msg: None, - }), - }; - - crate::api::send_to_client( - &mut stream_err.lock().expect("Couldn't lock stream"), - &msg, - ) - .expect("Send to client failed"); // TODO: notify instead of crash - } - }; - - tokio::spawn(future); - } - - let future = async move { - match child.wait().await { - Ok(exit_status) => { - let msg = OutgoingMsg::CallCallback { - callback_id, - args: Some(Args::Spawn { - stdout: None, - stderr: None, - exit_code: exit_status.code(), - exit_msg: Some(exit_status.to_string()), - }), - }; - - crate::api::send_to_client( - &mut stream_exit.lock().expect("Couldn't lock stream"), - &msg, - ) - .expect("Send to client failed"); // TODO: notify instead of crash - } - Err(err) => { - tracing::warn!("child wait() err: {err}"); - } - } - }; - - tokio::spawn(future); - } - } -} diff --git a/src/api/msg.rs b/src/api/msg.rs deleted file mode 100644 index 588177b..0000000 --- a/src/api/msg.rs +++ /dev/null @@ -1,335 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -// The MessagePack format for these is a one-element map where the element's key is the enum name and its -// value is a map of the enum's values - -use smithay::input::keyboard::ModifiersState; - -use crate::{ - input::libinput::LibinputSetting, - layout::Layout, - output::OutputName, - tag::TagId, - window::{ - rules::{WindowRule, WindowRuleCondition}, - window_state::{FullscreenOrMaximized, WindowId}, - }, -}; - -#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Copy)] -pub struct CallbackId(pub u32); - -#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)] -pub enum KeyIntOrString { - Int(u32), - String(String), -} - -#[derive(Debug, Hash, serde::Serialize, serde::Deserialize, Clone, Copy, PartialEq, Eq)] -pub enum MouseEdge { - Press, - Release, -} - -#[derive(Debug, serde::Deserialize)] -pub 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, - }, - 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, - }, - 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, - }, -} - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub struct RequestId(u32); - -#[allow(clippy::enum_variant_names)] -#[derive(Debug, serde::Serialize, serde::Deserialize)] -/// Messages that require a server response, usually to provide some data. -pub enum Request { - // Windows - GetWindows, - GetWindowProps { window_id: WindowId }, - // Outputs - GetOutputs, - GetOutputProps { output_name: String }, - // Tags - GetTags, - GetTagProps { tag_id: TagId }, -} - -#[derive(Debug, PartialEq, Eq, Copy, Clone, serde::Serialize, serde::Deserialize)] -pub enum Modifier { - Shift = 0b0000_0001, - Ctrl = 0b0000_0010, - Alt = 0b0000_0100, - Super = 0b0000_1000, -} - -/// A bitmask of [`Modifier`]s for the purpose of hashing. -#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] -pub struct ModifierMask(u8); - -impl From> for ModifierMask { - fn from(value: Vec) -> Self { - let value = value.into_iter(); - let mut mask: u8 = 0b0000_0000; - for modifier in value { - mask |= modifier as u8; - } - Self(mask) - } -} - -impl From<&[Modifier]> for ModifierMask { - fn from(value: &[Modifier]) -> Self { - let value = value.iter(); - let mut mask: u8 = 0b0000_0000; - for modifier in value { - mask |= *modifier as u8; - } - Self(mask) - } -} - -impl From for ModifierMask { - fn from(state: ModifiersState) -> Self { - let mut mask: u8 = 0b0000_0000; - if state.shift { - mask |= Modifier::Shift as u8; - } - if state.ctrl { - mask |= Modifier::Ctrl as u8; - } - if state.alt { - mask |= Modifier::Alt as u8; - } - if state.logo { - mask |= Modifier::Super as u8; - } - Self(mask) - } -} - -impl ModifierMask { - #[allow(dead_code)] - pub fn values(self) -> Vec { - let mut res = Vec::::new(); - if self.0 & Modifier::Shift as u8 == Modifier::Shift as u8 { - res.push(Modifier::Shift); - } - if self.0 & Modifier::Ctrl as u8 == Modifier::Ctrl as u8 { - res.push(Modifier::Ctrl); - } - if self.0 & Modifier::Alt as u8 == Modifier::Alt as u8 { - res.push(Modifier::Alt); - } - if self.0 & Modifier::Super as u8 == Modifier::Super as u8 { - res.push(Modifier::Super); - } - res - } -} - -/// Messages sent from the server to the client. -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub enum OutgoingMsg { - CallCallback { - callback_id: CallbackId, - #[serde(default)] - args: Option, - }, - RequestResponse { - request_id: RequestId, - response: RequestResponse, - }, -} - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -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 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/src/api/protocol.rs b/src/api/protocol.rs deleted file mode 100644 index 23b0fee..0000000 --- a/src/api/protocol.rs +++ /dev/null @@ -1,1887 +0,0 @@ -use std::{ffi::OsString, num::NonZeroU32, pin::Pin, process::Stdio}; - -use pinnacle_api_defs::pinnacle::{ - input::v0alpha1::{ - set_libinput_setting_request::{AccelProfile, ClickMethod, ScrollMethod, TapButtonMap}, - set_mousebind_request::MouseEdge, - SetKeybindRequest, SetKeybindResponse, SetLibinputSettingRequest, SetMousebindRequest, - SetMousebindResponse, SetRepeatRateRequest, SetXkbConfigRequest, - }, - output::v0alpha1::{ConnectForAllRequest, ConnectForAllResponse, SetLocationRequest}, - process::v0alpha1::{SetEnvRequest, SpawnRequest, SpawnResponse}, - tag::v0alpha1::{ - AddRequest, AddResponse, RemoveRequest, SetActiveRequest, SetLayoutRequest, SwitchToRequest, - }, - v0alpha1::{Geometry, QuitRequest}, - window::v0alpha1::{ - AddWindowRuleRequest, CloseRequest, FullscreenOrMaximized, MoveGrabRequest, - MoveToTagRequest, ResizeGrabRequest, SetFloatingRequest, SetFullscreenRequest, - SetGeometryRequest, SetMaximizedRequest, SetTagRequest, WindowRule, WindowRuleCondition, - }, -}; -use smithay::{ - desktop::space::SpaceElement, - input::keyboard::XkbConfig, - reexports::{calloop, input as libinput, wayland_protocols::xdg::shell::server}, - utils::{Point, Rectangle, SERIAL_COUNTER}, - wayland::{compositor, shell::xdg::XdgToplevelSurfaceData}, -}; -use sysinfo::ProcessRefreshKind; -use tokio::io::AsyncBufReadExt; -use tokio_stream::Stream; -use tonic::{Request, Response, Status}; - -use crate::{ - config::ConnectorSavedState, - focus::FocusTarget, - input::ModifierMask, - output::OutputName, - state::{State, WithState}, - tag::{Tag, TagId}, - window::{window_state::WindowId, WindowElement}, -}; - -type ResponseStream = Pin> + Send>>; -pub type StateFnSender = calloop::channel::Sender>; - -pub struct PinnacleService { - pub sender: StateFnSender, -} - -#[tonic::async_trait] -impl pinnacle_api_defs::pinnacle::v0alpha1::pinnacle_service_server::PinnacleService - for PinnacleService -{ - async fn quit(&self, _request: Request) -> Result, Status> { - tracing::trace!("PinnacleService.quit"); - let f = Box::new(|state: &mut State| { - state.shutdown(); - }); - // Expect is ok here, if it panics then the state was dropped beforehand - self.sender.send(f).expect("failed to send f"); - - Ok(Response::new(())) - } -} - -pub struct InputService { - pub sender: StateFnSender, -} - -#[tonic::async_trait] -impl pinnacle_api_defs::pinnacle::input::v0alpha1::input_service_server::InputService - for InputService -{ - type SetKeybindStream = ResponseStream; - type SetMousebindStream = ResponseStream; - - async fn set_keybind( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - - tracing::debug!(request = ?request); - - // TODO: impl From<&[Modifier]> for ModifierMask - let modifiers = request - .modifiers() - .fold(ModifierMask::empty(), |acc, modifier| match modifier { - pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Unspecified => acc, - pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Shift => { - acc | ModifierMask::SHIFT - } - pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Ctrl => { - acc | ModifierMask::CTRL - } - pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Alt => { - acc | ModifierMask::ALT - } - pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Super => { - acc | ModifierMask::SUPER - } - }); - let key = request - .key - .ok_or_else(|| Status::invalid_argument("no key specified"))?; - - use pinnacle_api_defs::pinnacle::input::v0alpha1::set_keybind_request::Key; - let keysym = match key { - Key::RawCode(num) => { - tracing::info!("set keybind: {:?}, raw {}", modifiers, num); - xkbcommon::xkb::Keysym::new(num) - } - Key::XkbName(s) => { - if s.chars().count() == 1 { - let Some(ch) = s.chars().next() else { unreachable!() }; - let keysym = xkbcommon::xkb::Keysym::from_char(ch); - tracing::info!("set keybind: {:?}, {:?}", modifiers, keysym); - keysym - } else { - let keysym = - xkbcommon::xkb::keysym_from_name(&s, xkbcommon::xkb::KEYSYM_NO_FLAGS); - tracing::info!("set keybind: {:?}, {:?}", modifiers, keysym); - keysym - } - } - }; - - let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); - - self.sender - .send(Box::new(move |state| { - state - .input_state - .grpc_keybinds - .insert((modifiers, keysym), sender); - })) - .map_err(|_| Status::internal("internal state was not running"))?; - - let receiver_stream = tokio_stream::wrappers::UnboundedReceiverStream::new(receiver); - - Ok(Response::new(Box::pin(receiver_stream))) - } - - async fn set_mousebind( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - - tracing::debug!(request = ?request); - - let modifiers = request - .modifiers() - .fold(ModifierMask::empty(), |acc, modifier| match modifier { - pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Unspecified => acc, - pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Shift => { - acc | ModifierMask::SHIFT - } - pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Ctrl => { - acc | ModifierMask::CTRL - } - pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Alt => { - acc | ModifierMask::ALT - } - pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Super => { - acc | ModifierMask::SUPER - } - }); - let button = request - .button - .ok_or_else(|| Status::invalid_argument("no key specified"))?; - - let edge = request.edge(); - - if let MouseEdge::Unspecified = edge { - return Err(Status::invalid_argument("press or release not specified")); - } - - let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); - - self.sender - .send(Box::new(move |state| { - state - .input_state - .grpc_mousebinds - .insert((modifiers, button, edge), sender); - })) - .map_err(|_| Status::internal("internal state was not running"))?; - - let receiver_stream = tokio_stream::wrappers::UnboundedReceiverStream::new(receiver); - - Ok(Response::new(Box::pin(receiver_stream))) - } - - async fn set_xkb_config( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - - let f = Box::new(move |state: &mut State| { - let new_config = XkbConfig { - rules: request.rules(), - variant: request.variant(), - model: request.model(), - layout: request.layout(), - options: request.options.clone(), - }; - if let Some(kb) = state.seat.get_keyboard() { - if let Err(err) = kb.set_xkb_config(state, new_config) { - tracing::error!("Failed to set xkbconfig: {err}"); - } - } - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - async fn set_repeat_rate( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - - let rate = request - .rate - .ok_or_else(|| Status::invalid_argument("no rate specified"))?; - let delay = request - .delay - .ok_or_else(|| Status::invalid_argument("no rate specified"))?; - - let f = Box::new(move |state: &mut State| { - if let Some(kb) = state.seat.get_keyboard() { - kb.change_repeat_info(rate, delay); - } - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - async fn set_libinput_setting( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - - let setting = request - .setting - .ok_or_else(|| Status::invalid_argument("no setting specified"))?; - - let discriminant = std::mem::discriminant(&setting); - - use pinnacle_api_defs::pinnacle::input::v0alpha1::set_libinput_setting_request::Setting; - let apply_setting: Box = match setting { - Setting::AccelProfile(profile) => { - let profile = AccelProfile::try_from(profile).unwrap_or(AccelProfile::Unspecified); - - match profile { - AccelProfile::Unspecified => { - return Err(Status::invalid_argument("unspecified accel profile")); - } - AccelProfile::Flat => Box::new(|device| { - let _ = device.config_accel_set_profile(libinput::AccelProfile::Flat); - }), - AccelProfile::Adaptive => Box::new(|device| { - let _ = device.config_accel_set_profile(libinput::AccelProfile::Adaptive); - }), - } - } - Setting::AccelSpeed(speed) => Box::new(move |device| { - let _ = device.config_accel_set_speed(speed); - }), - Setting::CalibrationMatrix(matrix) => { - let matrix = <[f32; 6]>::try_from(matrix.matrix).map_err(|vec| { - Status::invalid_argument(format!( - "matrix requires exactly 6 floats but {} were specified", - vec.len() - )) - })?; - - Box::new(move |device| { - let _ = device.config_calibration_set_matrix(matrix); - }) - } - Setting::ClickMethod(method) => { - let method = ClickMethod::try_from(method).unwrap_or(ClickMethod::Unspecified); - - match method { - ClickMethod::Unspecified => { - return Err(Status::invalid_argument("unspecified click method")) - } - ClickMethod::ButtonAreas => Box::new(|device| { - let _ = device.config_click_set_method(libinput::ClickMethod::ButtonAreas); - }), - ClickMethod::ClickFinger => Box::new(|device| { - let _ = device.config_click_set_method(libinput::ClickMethod::Clickfinger); - }), - } - } - Setting::DisableWhileTyping(disable) => Box::new(move |device| { - let _ = device.config_dwt_set_enabled(disable); - }), - Setting::LeftHanded(enable) => Box::new(move |device| { - let _ = device.config_left_handed_set(enable); - }), - Setting::MiddleEmulation(enable) => Box::new(move |device| { - let _ = device.config_middle_emulation_set_enabled(enable); - }), - Setting::RotationAngle(angle) => Box::new(move |device| { - let _ = device.config_rotation_set_angle(angle % 360); - }), - Setting::ScrollButton(button) => Box::new(move |device| { - let _ = device.config_scroll_set_button(button); - }), - Setting::ScrollButtonLock(enable) => Box::new(move |device| { - let _ = device.config_scroll_set_button_lock(match enable { - true => libinput::ScrollButtonLockState::Enabled, - false => libinput::ScrollButtonLockState::Disabled, - }); - }), - Setting::ScrollMethod(method) => { - let method = ScrollMethod::try_from(method).unwrap_or(ScrollMethod::Unspecified); - - match method { - ScrollMethod::Unspecified => { - return Err(Status::invalid_argument("unspecified scroll method")); - } - ScrollMethod::NoScroll => Box::new(|device| { - let _ = device.config_scroll_set_method(libinput::ScrollMethod::NoScroll); - }), - ScrollMethod::TwoFinger => Box::new(|device| { - let _ = device.config_scroll_set_method(libinput::ScrollMethod::TwoFinger); - }), - ScrollMethod::Edge => Box::new(|device| { - let _ = device.config_scroll_set_method(libinput::ScrollMethod::Edge); - }), - ScrollMethod::OnButtonDown => Box::new(|device| { - let _ = - device.config_scroll_set_method(libinput::ScrollMethod::OnButtonDown); - }), - } - } - Setting::NaturalScroll(enable) => Box::new(move |device| { - let _ = device.config_scroll_set_natural_scroll_enabled(enable); - }), - Setting::TapButtonMap(map) => { - let map = TapButtonMap::try_from(map).unwrap_or(TapButtonMap::Unspecified); - - match map { - TapButtonMap::Unspecified => { - return Err(Status::invalid_argument("unspecified tap button map")); - } - TapButtonMap::LeftRightMiddle => Box::new(|device| { - let _ = device - .config_tap_set_button_map(libinput::TapButtonMap::LeftRightMiddle); - }), - TapButtonMap::LeftMiddleRight => Box::new(|device| { - let _ = device - .config_tap_set_button_map(libinput::TapButtonMap::LeftMiddleRight); - }), - } - } - Setting::TapDrag(enable) => Box::new(move |device| { - let _ = device.config_tap_set_drag_enabled(enable); - }), - Setting::TapDragLock(enable) => Box::new(move |device| { - let _ = device.config_tap_set_drag_lock_enabled(enable); - }), - Setting::Tap(enable) => Box::new(move |device| { - let _ = device.config_tap_set_enabled(enable); - }), - }; - - let f = Box::new(move |state: &mut State| { - for device in state.input_state.libinput_devices.iter_mut() { - apply_setting(device); - } - - state - .input_state - .grpc_libinput_settings - .insert(discriminant, apply_setting); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } -} - -pub struct ProcessService { - pub sender: StateFnSender, -} - -#[tonic::async_trait] -impl pinnacle_api_defs::pinnacle::process::v0alpha1::process_service_server::ProcessService - for ProcessService -{ - type SpawnStream = ResponseStream; - - async fn spawn( - &self, - request: Request, - ) -> Result, Status> { - tracing::debug!("ProcessService.spawn"); - let request = request.into_inner(); - - let once = request.once(); - let has_callback = request.has_callback(); - let mut command = request.args.into_iter(); - let arg0 = command - .next() - .ok_or_else(|| Status::invalid_argument("no args specified"))?; - - let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); - - let f = Box::new(move |state: &mut State| { - if once { - state - .system_processes - .refresh_processes_specifics(ProcessRefreshKind::new()); - - let compositor_pid = std::process::id(); - let already_running = - state - .system_processes - .processes_by_exact_name(&arg0) - .any(|proc| { - proc.parent() - .is_some_and(|parent_pid| parent_pid.as_u32() == compositor_pid) - }); - - if already_running { - return; - } - } - - let Ok(mut child) = tokio::process::Command::new(OsString::from(arg0.clone())) - .envs( - [("WAYLAND_DISPLAY", state.socket_name.clone())] - .into_iter() - .chain(state.xdisplay.map(|xdisp| ("DISPLAY", format!(":{xdisp}")))), - ) - .stdin(match has_callback { - true => Stdio::piped(), - false => Stdio::null(), - }) - .stdout(match has_callback { - true => Stdio::piped(), - false => Stdio::null(), - }) - .stderr(match has_callback { - true => Stdio::piped(), - false => Stdio::null(), - }) - .args(command) - .spawn() - else { - tracing::warn!("Tried to run {arg0}, but it doesn't exist",); - return; - }; - - if !has_callback { - return; - } - - let stdout = child.stdout.take(); - let stderr = child.stderr.take(); - - if let Some(stdout) = stdout { - let sender = sender.clone(); - - let mut reader = tokio::io::BufReader::new(stdout).lines(); - - tokio::spawn(async move { - while let Ok(Some(line)) = reader.next_line().await { - let response: Result<_, Status> = Ok(SpawnResponse { - stdout: Some(line), - ..Default::default() - }); - - // TODO: handle error - match sender.send(response) { - Ok(_) => (), - Err(err) => { - tracing::error!(err = ?err); - break; - } - } - } - }); - } - - if let Some(stderr) = stderr { - let sender = sender.clone(); - - let mut reader = tokio::io::BufReader::new(stderr).lines(); - - tokio::spawn(async move { - while let Ok(Some(line)) = reader.next_line().await { - let response: Result<_, Status> = Ok(SpawnResponse { - stderr: Some(line), - ..Default::default() - }); - - // TODO: handle error - match sender.send(response) { - Ok(_) => (), - Err(err) => { - tracing::error!(err = ?err); - break; - } - } - } - }); - } - - tokio::spawn(async move { - match child.wait().await { - Ok(exit_status) => { - let response = Ok(SpawnResponse { - exit_code: exit_status.code(), - exit_message: Some(exit_status.to_string()), - ..Default::default() - }); - // TODO: handle error - let _ = sender.send(response); - } - Err(err) => tracing::warn!("child wait() err: {err}"), - } - }); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - let receiver_stream = tokio_stream::wrappers::UnboundedReceiverStream::new(receiver); - - Ok(Response::new(Box::pin(receiver_stream))) - } - - async fn set_env(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - - let key = request - .key - .ok_or_else(|| Status::invalid_argument("no key specified"))?; - let value = request - .value - .ok_or_else(|| Status::invalid_argument("no value specified"))?; - - if key.is_empty() { - return Err(Status::invalid_argument("key was empty")); - } - - if key.contains(['\0', '=']) { - return Err(Status::invalid_argument("key contained NUL or =")); - } - - if value.contains('\0') { - return Err(Status::invalid_argument("value contained NUL")); - } - - std::env::set_var(key, value); - - Ok(Response::new(())) - } -} - -pub struct TagService { - pub sender: StateFnSender, -} - -#[tonic::async_trait] -impl pinnacle_api_defs::pinnacle::tag::v0alpha1::tag_service_server::TagService for TagService { - async fn set_active(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - - let tag_id = TagId::Some( - request - .tag_id - .ok_or_else(|| Status::invalid_argument("no tag specified"))?, - ); - - let set_or_toggle = match request.set_or_toggle { - Some( - pinnacle_api_defs::pinnacle::tag::v0alpha1::set_active_request::SetOrToggle::Set( - set, - ), - ) => Some(set), - Some( - pinnacle_api_defs::pinnacle::tag::v0alpha1::set_active_request::SetOrToggle::Toggle( - _, - ), - ) => None, - None => return Err(Status::invalid_argument("unspecified set or toggle")), - }; - - let f = Box::new(move |state: &mut State| { - let Some(tag) = tag_id.tag(state) else { - return; - }; - match set_or_toggle { - Some(set) => tag.set_active(set), - None => tag.set_active(!tag.active()), - } - - let Some(output) = tag.output(state) else { - return; - }; - - state.update_windows(&output); - state.update_focus(&output); - state.schedule_render(&output); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - async fn switch_to(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - - let tag_id = TagId::Some( - request - .tag_id - .ok_or_else(|| Status::invalid_argument("no tag specified"))?, - ); - - let f = Box::new(move |state: &mut State| { - let Some(tag) = tag_id.tag(state) else { return }; - let Some(output) = tag.output(state) else { return }; - - output.with_state(|state| { - for op_tag in state.tags.iter_mut() { - op_tag.set_active(false); - } - tag.set_active(true); - }); - - state.update_windows(&output); - state.update_focus(&output); - state.schedule_render(&output); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - async fn add(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - - let output_name = OutputName( - request - .output_name - .ok_or_else(|| Status::invalid_argument("no output specified"))?, - ); - - let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::(); - - let f = Box::new(move |state: &mut State| { - let new_tags = request - .tag_names - .into_iter() - .map(Tag::new) - .collect::>(); - - let tag_ids = new_tags - .iter() - .map(|tag| tag.id()) - .map(|id| match id { - TagId::None => unreachable!(), - TagId::Some(id) => id, - }) - .collect::>(); - - let _ = sender.send(AddResponse { tag_ids }); - - if let Some(saved_state) = state.config.connector_saved_states.get_mut(&output_name) { - let mut tags = saved_state.tags.clone(); - tags.extend(new_tags.clone()); - saved_state.tags = tags; - } else { - state.config.connector_saved_states.insert( - output_name.clone(), - crate::config::ConnectorSavedState { - tags: new_tags.clone(), - ..Default::default() - }, - ); - } - - let Some(output) = state - .space - .outputs() - .find(|output| output.name() == output_name.0) - else { - return; - }; - - output.with_state(|state| { - state.tags.extend(new_tags.clone()); - tracing::debug!("tags added, are now {:?}", state.tags); - }); - - for tag in new_tags { - for window in state.windows.iter() { - window.with_state(|state| { - for win_tag in state.tags.iter_mut() { - if win_tag.id() == tag.id() { - *win_tag = tag.clone(); - } - } - }); - } - } - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - let response = receiver - .recv() - .await - .ok_or_else(|| Status::internal("internal state was not running"))?; - - Ok(Response::new(response)) - } - - // TODO: test - async fn remove(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - - let tag_ids = request.tag_ids.into_iter().map(TagId::Some); - - let f = Box::new(move |state: &mut State| { - let tags_to_remove = tag_ids.flat_map(|id| id.tag(state)).collect::>(); - - for output in state.space.outputs().cloned().collect::>() { - // TODO: seriously, convert state.tags into a hashset - output.with_state(|state| { - for tag_to_remove in tags_to_remove.iter() { - state.tags.retain(|tag| tag != tag_to_remove); - } - }); - - state.update_windows(&output); - state.schedule_render(&output); - } - - for conn_saved_state in state.config.connector_saved_states.values_mut() { - for tag_to_remove in tags_to_remove.iter() { - conn_saved_state.tags.retain(|tag| tag != tag_to_remove); - } - } - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - async fn set_layout(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - - let tag_id = TagId::Some( - request - .tag_id - .ok_or_else(|| Status::invalid_argument("no tag specified"))?, - ); - - use pinnacle_api_defs::pinnacle::tag::v0alpha1::set_layout_request::Layout; - - // TODO: from impl - let layout = match request.layout() { - Layout::Unspecified => return Err(Status::invalid_argument("unspecified layout")), - Layout::MasterStack => crate::layout::Layout::MasterStack, - Layout::Dwindle => crate::layout::Layout::Dwindle, - Layout::Spiral => crate::layout::Layout::Spiral, - Layout::CornerTopLeft => crate::layout::Layout::CornerTopLeft, - Layout::CornerTopRight => crate::layout::Layout::CornerTopRight, - Layout::CornerBottomLeft => crate::layout::Layout::CornerBottomLeft, - Layout::CornerBottomRight => crate::layout::Layout::CornerBottomRight, - }; - - let f = Box::new(move |state: &mut State| { - let Some(tag) = tag_id.tag(state) else { return }; - - tag.set_layout(layout); - - let Some(output) = tag.output(state) else { return }; - - state.update_windows(&output); - state.schedule_render(&output); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - async fn get( - &self, - _request: Request, - ) -> Result, Status> { - let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::< - pinnacle_api_defs::pinnacle::tag::v0alpha1::GetResponse, - >(); - - let f = Box::new(move |state: &mut State| { - let tag_ids = state - .space - .outputs() - .flat_map(|op| op.with_state(|state| state.tags.clone())) - .map(|tag| tag.id()) - .map(|id| match id { - TagId::None => unreachable!(), - TagId::Some(id) => id, - }) - .collect::>(); - - let _ = - sender.send(pinnacle_api_defs::pinnacle::tag::v0alpha1::GetResponse { tag_ids }); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - let response = receiver - .recv() - .await - .ok_or_else(|| Status::internal("internal state was not running"))?; - - Ok(Response::new(response)) - } - - async fn get_properties( - &self, - request: Request, - ) -> Result, Status> - { - let request = request.into_inner(); - - let tag_id = TagId::Some( - request - .tag_id - .ok_or_else(|| Status::invalid_argument("no tag specified"))?, - ); - - let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::< - pinnacle_api_defs::pinnacle::tag::v0alpha1::GetPropertiesResponse, - >(); - - let f = Box::new(move |state: &mut State| { - let tag = tag_id.tag(state); - - let output_name = tag - .as_ref() - .and_then(|tag| tag.output(state)) - .map(|output| output.name()); - let active = tag.as_ref().map(|tag| tag.active()); - let name = tag.as_ref().map(|tag| tag.name()); - - let _ = sender.send( - pinnacle_api_defs::pinnacle::tag::v0alpha1::GetPropertiesResponse { - active, - name, - output_name, - }, - ); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - let response = receiver - .recv() - .await - .ok_or_else(|| Status::internal("internal state was not running"))?; - - Ok(Response::new(response)) - } -} - -pub struct OutputService { - pub sender: StateFnSender, -} - -#[tonic::async_trait] -impl pinnacle_api_defs::pinnacle::output::v0alpha1::output_service_server::OutputService - for OutputService -{ - type ConnectForAllStream = ResponseStream; - - async fn set_location( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - - let output_name = OutputName( - request - .output_name - .ok_or_else(|| Status::invalid_argument("no output specified"))?, - ); - - let x = request.x; - let y = request.y; - - let f = Box::new(move |state: &mut State| { - if let Some(saved_state) = state.config.connector_saved_states.get_mut(&output_name) { - if let Some(x) = x { - saved_state.loc.x = x; - } - if let Some(y) = y { - saved_state.loc.y = y; - } - } else { - state.config.connector_saved_states.insert( - output_name.clone(), - ConnectorSavedState { - loc: (x.unwrap_or_default(), y.unwrap_or_default()).into(), - ..Default::default() - }, - ); - } - - let Some(output) = output_name.output(state) else { - return; - }; - let mut loc = output.current_location(); - if let Some(x) = x { - loc.x = x; - } - if let Some(y) = y { - loc.y = y; - } - output.change_current_state(None, None, None, Some(loc)); - state.space.map_output(&output, loc); - tracing::debug!("Mapping output {} to {loc:?}", output.name()); - state.update_windows(&output); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - // TODO: remove this and integrate it into a signal/event system - async fn connect_for_all( - &self, - _request: Request, - ) -> Result, Status> { - tracing::trace!("OutputService.connect_for_all"); - let (sender, receiver) = - tokio::sync::mpsc::unbounded_channel::>(); - - let f = Box::new(move |state: &mut State| { - // for output in state.space.outputs() { - // let _ = sender.send(Ok(ConnectForAllResponse { - // output_name: Some(output.name()), - // })); - // tracing::debug!(name = output.name(), "sent connect_for_all"); - // } - - state.config.grpc_output_callback_senders.push(sender); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - let receiver_stream = tokio_stream::wrappers::UnboundedReceiverStream::new(receiver); - - Ok(Response::new(Box::pin(receiver_stream))) - } - - async fn get( - &self, - _request: Request, - ) -> Result, Status> { - let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::< - pinnacle_api_defs::pinnacle::output::v0alpha1::GetResponse, - >(); - - let f = Box::new(move |state: &mut State| { - let output_names = state - .space - .outputs() - .map(|output| output.name()) - .collect::>(); - - let _ = sender - .send(pinnacle_api_defs::pinnacle::output::v0alpha1::GetResponse { output_names }); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - let response = receiver - .recv() - .await - .ok_or_else(|| Status::internal("internal state was not running"))?; - - Ok(Response::new(response)) - } - - async fn get_properties( - &self, - request: Request, - ) -> Result< - Response, - Status, - > { - let request = request.into_inner(); - - let output_name = OutputName( - request - .output_name - .ok_or_else(|| Status::invalid_argument("no output specified"))?, - ); - - let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::< - pinnacle_api_defs::pinnacle::output::v0alpha1::GetPropertiesResponse, - >(); - - let f = Box::new(move |state: &mut State| { - let output = output_name.output(state); - - let pixel_width = output - .as_ref() - .and_then(|output| output.current_mode().map(|mode| mode.size.w as u32)); - - let pixel_height = output - .as_ref() - .and_then(|output| output.current_mode().map(|mode| mode.size.h as u32)); - - let refresh_rate = output - .as_ref() - .and_then(|output| output.current_mode().map(|mode| mode.refresh as u32)); - - let model = output - .as_ref() - .map(|output| output.physical_properties().model); - - let physical_width = output - .as_ref() - .map(|output| output.physical_properties().size.w as u32); - - let physical_height = output - .as_ref() - .map(|output| output.physical_properties().size.h as u32); - - let make = output - .as_ref() - .map(|output| output.physical_properties().make); - - let x = output.as_ref().map(|output| output.current_location().x); - - let y = output.as_ref().map(|output| output.current_location().y); - - let focused = state - .focus_state - .focused_output - .as_ref() - .and_then(|foc_op| output.as_ref().map(|op| op == foc_op)); - - let tag_ids = output - .as_ref() - .map(|output| { - output.with_state(|state| { - state - .tags - .iter() - .map(|tag| match tag.id() { - TagId::None => unreachable!(), - TagId::Some(id) => id, - }) - .collect::>() - }) - }) - .unwrap_or_default(); - - let _ = sender.send( - pinnacle_api_defs::pinnacle::output::v0alpha1::GetPropertiesResponse { - make, - model, - x, - y, - pixel_width, - pixel_height, - refresh_rate, - physical_width, - physical_height, - focused, - tag_ids, - }, - ); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - let response = receiver - .recv() - .await - .ok_or_else(|| Status::internal("internal state was not running"))?; - - Ok(Response::new(response)) - } -} - -pub struct WindowService { - pub sender: StateFnSender, -} - -#[tonic::async_trait] -impl pinnacle_api_defs::pinnacle::window::v0alpha1::window_service_server::WindowService - for WindowService -{ - async fn close(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - - let window_id = WindowId::Some( - request - .window_id - .ok_or_else(|| Status::invalid_argument("no window specified"))?, - ); - - let f = Box::new(move |state: &mut State| { - let Some(window) = window_id.window(state) else { return }; - - match window { - WindowElement::Wayland(window) => window.toplevel().send_close(), - WindowElement::X11(surface) => surface.close().expect("failed to close x11 win"), - WindowElement::X11OverrideRedirect(_) => { - tracing::warn!("tried to close override redirect window"); - } - _ => unreachable!(), - } - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - async fn set_geometry( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - - let window_id = WindowId::Some( - request - .window_id - .ok_or_else(|| Status::invalid_argument("no window specified"))?, - ); - - let geometry = request.geometry.unwrap_or_default(); - let x = geometry.x; - let y = geometry.y; - let width = geometry.width; - let height = geometry.height; - - let f = Box::new(move |state: &mut State| { - let Some(window) = window_id.window(state) else { return }; - - // TODO: with no x or y, defaults unmapped windows to 0, 0 - let mut window_loc = state - .space - .element_location(&window) - .unwrap_or((x.unwrap_or_default(), y.unwrap_or_default()).into()); - window_loc.x = x.unwrap_or(window_loc.x); - window_loc.y = y.unwrap_or(window_loc.y); - - let mut window_size = window.geometry().size; - window_size.w = width.unwrap_or(window_size.w); - window_size.h = height.unwrap_or(window_size.h); - - let rect = Rectangle::from_loc_and_size(window_loc, window_size); - // window.change_geometry(rect); - window.with_state(|state| { - use crate::window::window_state::FloatingOrTiled; - state.floating_or_tiled = match state.floating_or_tiled { - FloatingOrTiled::Floating(_) => FloatingOrTiled::Floating(rect), - FloatingOrTiled::Tiled(_) => FloatingOrTiled::Tiled(Some(rect)), - } - }); - - for output in state.space.outputs_for_element(&window) { - state.update_windows(&output); - state.schedule_render(&output); - } - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - async fn set_fullscreen( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - - let window_id = WindowId::Some( - request - .window_id - .ok_or_else(|| Status::invalid_argument("no window specified"))?, - ); - - let set_or_toggle = match request.set_or_toggle { - Some(pinnacle_api_defs::pinnacle::window::v0alpha1::set_fullscreen_request::SetOrToggle::Set(set)) => { - Some(set) - } - Some(pinnacle_api_defs::pinnacle::window::v0alpha1::set_fullscreen_request::SetOrToggle::Toggle(_)) => { - None - } - None => return Err(Status::invalid_argument("unspecified set or toggle")), - }; - - let f = Box::new(move |state: &mut State| { - let Some(window) = window_id.window(state) else { - return; - }; - match set_or_toggle { - Some(set) => { - let is_fullscreen = - window.with_state(|state| state.fullscreen_or_maximized.is_fullscreen()); - if set != is_fullscreen { - window.toggle_fullscreen(); - } - } - None => window.toggle_fullscreen(), - } - - let Some(output) = window.output(state) else { - return; - }; - - state.update_windows(&output); - state.schedule_render(&output); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - async fn set_maximized( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - - let window_id = WindowId::Some( - request - .window_id - .ok_or_else(|| Status::invalid_argument("no window specified"))?, - ); - - let set_or_toggle = match request.set_or_toggle { - Some(pinnacle_api_defs::pinnacle::window::v0alpha1::set_maximized_request::SetOrToggle::Set(set)) => { - Some(set) - } - Some(pinnacle_api_defs::pinnacle::window::v0alpha1::set_maximized_request::SetOrToggle::Toggle(_)) => None, - None => return Err(Status::invalid_argument("unspecified set or toggle")), - }; - - let f = Box::new(move |state: &mut State| { - let Some(window) = window_id.window(state) else { - return; - }; - match set_or_toggle { - Some(set) => { - let is_maximized = - window.with_state(|state| state.fullscreen_or_maximized.is_maximized()); - if set != is_maximized { - window.toggle_maximized(); - } - } - None => window.toggle_maximized(), - } - - let Some(output) = window.output(state) else { - return; - }; - - state.update_windows(&output); - state.schedule_render(&output); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - async fn set_floating( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - - let window_id = WindowId::Some( - request - .window_id - .ok_or_else(|| Status::invalid_argument("no window specified"))?, - ); - - let set_or_toggle = match request.set_or_toggle { - Some(pinnacle_api_defs::pinnacle::window::v0alpha1::set_floating_request::SetOrToggle::Set(set)) => { - Some(set) - } - Some(pinnacle_api_defs::pinnacle::window::v0alpha1::set_floating_request::SetOrToggle::Toggle(_)) => None, - None => return Err(Status::invalid_argument("unspecified set or toggle")), - }; - - let f = Box::new(move |state: &mut State| { - let Some(window) = window_id.window(state) else { - return; - }; - match set_or_toggle { - Some(set) => { - let is_floating = - window.with_state(|state| state.floating_or_tiled.is_floating()); - if set != is_floating { - window.toggle_floating(); - } - } - None => window.toggle_floating(), - } - - let Some(output) = window.output(state) else { - return; - }; - - state.update_windows(&output); - state.schedule_render(&output); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - async fn move_to_tag( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - - let window_id = WindowId::Some( - request - .window_id - .ok_or_else(|| Status::invalid_argument("no window specified"))?, - ); - - let tag_id = TagId::Some( - request - .tag_id - .ok_or_else(|| Status::invalid_argument("no tag specified"))?, - ); - - let f = Box::new(move |state: &mut State| { - let Some(window) = window_id.window(state) else { return }; - let Some(tag) = tag_id.tag(state) else { return }; - window.with_state(|state| { - state.tags = vec![tag.clone()]; - }); - let Some(output) = tag.output(state) else { return }; - state.update_windows(&output); - state.schedule_render(&output); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - async fn set_tag(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - - let window_id = WindowId::Some( - request - .window_id - .ok_or_else(|| Status::invalid_argument("no window specified"))?, - ); - - let tag_id = TagId::Some( - request - .tag_id - .ok_or_else(|| Status::invalid_argument("no tag specified"))?, - ); - - let set_or_toggle = match request.set_or_toggle { - Some( - pinnacle_api_defs::pinnacle::window::v0alpha1::set_tag_request::SetOrToggle::Set( - set, - ), - ) => Some(set), - Some( - pinnacle_api_defs::pinnacle::window::v0alpha1::set_tag_request::SetOrToggle::Toggle( - _, - ), - ) => None, - None => return Err(Status::invalid_argument("unspecified set or toggle")), - }; - - let f = Box::new(move |state: &mut State| { - let Some(window) = window_id.window(state) else { return }; - let Some(tag) = tag_id.tag(state) else { return }; - - // TODO: turn state.tags into a hashset - match set_or_toggle { - Some(set) => { - if set { - window.with_state(|state| { - state.tags.retain(|tg| tg != &tag); - state.tags.push(tag.clone()); - }) - } else { - window.with_state(|state| { - state.tags.retain(|tg| tg != &tag); - }) - } - } - None => window.with_state(|state| { - if !state.tags.contains(&tag) { - state.tags.push(tag.clone()); - } else { - state.tags.retain(|tg| tg != &tag); - } - }), - } - - let Some(output) = tag.output(state) else { return }; - state.update_windows(&output); - state.schedule_render(&output); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - async fn move_grab(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - - let button = request - .button - .ok_or_else(|| Status::invalid_argument("no button specified"))?; - - let f = Box::new(move |state: &mut State| { - let Some((FocusTarget::Window(window), _)) = - state.focus_target_under(state.pointer_location) - else { - return; - }; - let Some(wl_surf) = window.wl_surface() else { return }; - let seat = state.seat.clone(); - - // We use the server one and not the client because windows like Steam don't provide - // GrabStartData, so we need to create it ourselves. - crate::grab::move_grab::move_request_server( - state, - &wl_surf, - &seat, - SERIAL_COUNTER.next_serial(), - button, - ); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - async fn resize_grab( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - - let button = request - .button - .ok_or_else(|| Status::invalid_argument("no button specified"))?; - - let f = Box::new(move |state: &mut State| { - let pointer_loc = state.pointer_location; - let Some((FocusTarget::Window(window), window_loc)) = - state.focus_target_under(pointer_loc) - else { - return; - }; - let Some(wl_surf) = window.wl_surface() else { return }; - - let window_geometry = window.geometry(); - let window_x = window_loc.x as f64; - let window_y = window_loc.y as f64; - let window_width = window_geometry.size.w as f64; - let window_height = window_geometry.size.h as f64; - let half_width = window_x + window_width / 2.0; - let half_height = window_y + window_height / 2.0; - let full_width = window_x + window_width; - let full_height = window_y + window_height; - - let edges = match pointer_loc { - Point { x, y, .. } - if (window_x..=half_width).contains(&x) - && (window_y..=half_height).contains(&y) => - { - server::xdg_toplevel::ResizeEdge::TopLeft - } - Point { x, y, .. } - if (half_width..=full_width).contains(&x) - && (window_y..=half_height).contains(&y) => - { - server::xdg_toplevel::ResizeEdge::TopRight - } - Point { x, y, .. } - if (window_x..=half_width).contains(&x) - && (half_height..=full_height).contains(&y) => - { - server::xdg_toplevel::ResizeEdge::BottomLeft - } - Point { x, y, .. } - if (half_width..=full_width).contains(&x) - && (half_height..=full_height).contains(&y) => - { - server::xdg_toplevel::ResizeEdge::BottomRight - } - _ => server::xdg_toplevel::ResizeEdge::None, - }; - - crate::grab::resize_grab::resize_request_server( - state, - &wl_surf, - &state.seat.clone(), - SERIAL_COUNTER.next_serial(), - edges.into(), - button, - ); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - async fn get( - &self, - _request: Request, - ) -> Result, Status> { - let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::< - pinnacle_api_defs::pinnacle::window::v0alpha1::GetResponse, - >(); - - let f = Box::new(move |state: &mut State| { - let window_ids = state - .windows - .iter() - .map(|win| { - win.with_state(|state| match state.id { - WindowId::None => unreachable!(), - WindowId::Some(id) => id, - }) - }) - .collect::>(); - - let _ = sender - .send(pinnacle_api_defs::pinnacle::window::v0alpha1::GetResponse { window_ids }); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - let response = receiver - .recv() - .await - .ok_or_else(|| Status::internal("internal state was not running"))?; - - Ok(Response::new(response)) - } - - async fn get_properties( - &self, - request: Request, - ) -> Result< - Response, - Status, - > { - let request = request.into_inner(); - - let window_id = WindowId::Some( - request - .window_id - .ok_or_else(|| Status::invalid_argument("no window specified"))?, - ); - - let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::< - pinnacle_api_defs::pinnacle::window::v0alpha1::GetPropertiesResponse, - >(); - - let f = Box::new(move |state: &mut State| { - let window = window_id.window(state); - - let width = window.as_ref().map(|win| win.geometry().size.w); - - let height = window.as_ref().map(|win| win.geometry().size.h); - - let x = window - .as_ref() - .and_then(|win| state.space.element_location(win)) - .map(|loc| loc.x); - - let y = window - .as_ref() - .and_then(|win| state.space.element_location(win)) - .map(|loc| loc.y); - - let geometry = if width.is_none() && height.is_none() && x.is_none() && y.is_none() { - None - } else { - Some(Geometry { - x, - y, - width, - height, - }) - }; - - let (class, title) = window.as_ref().map_or((None, None), |win| match &win { - WindowElement::Wayland(_) => { - if let Some(wl_surf) = win.wl_surface() { - compositor::with_states(&wl_surf, |states| { - let lock = states - .data_map - .get::() - .expect("XdgToplevelSurfaceData wasn't in surface's data map") - .lock() - .expect("failed to acquire lock"); - (lock.app_id.clone(), lock.title.clone()) - }) - } else { - (None, None) - } - } - WindowElement::X11(surface) | WindowElement::X11OverrideRedirect(surface) => { - (Some(surface.class()), Some(surface.title())) - } - _ => unreachable!(), - }); - - let focused = window.as_ref().and_then(|win| { - let output = win.output(state)?; - state.focused_window(&output).map(|foc_win| win == &foc_win) - }); - - let floating = window - .as_ref() - .map(|win| win.with_state(|state| state.floating_or_tiled.is_floating())); - - let fullscreen_or_maximized = window - .as_ref() - .map(|win| win.with_state(|state| state.fullscreen_or_maximized)) - .map(|fs_or_max| match fs_or_max { - // TODO: from impl - crate::window::window_state::FullscreenOrMaximized::Neither => { - FullscreenOrMaximized::Neither - } - crate::window::window_state::FullscreenOrMaximized::Fullscreen => { - FullscreenOrMaximized::Fullscreen - } - crate::window::window_state::FullscreenOrMaximized::Maximized => { - FullscreenOrMaximized::Maximized - } - } as i32); - - let tag_ids = window - .as_ref() - .map(|win| { - win.with_state(|state| { - state - .tags - .iter() - .map(|tag| match tag.id() { - TagId::Some(id) => id, - TagId::None => unreachable!(), - }) - .collect::>() - }) - }) - .unwrap_or_default(); - - let _ = sender.send( - pinnacle_api_defs::pinnacle::window::v0alpha1::GetPropertiesResponse { - geometry, - class, - title, - focused, - floating, - fullscreen_or_maximized, - tag_ids, - }, - ); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - let response = receiver - .recv() - .await - .ok_or_else(|| Status::internal("internal state was not running"))?; - - Ok(Response::new(response)) - } - - async fn add_window_rule( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - - let cond = request - .cond - .ok_or_else(|| Status::invalid_argument("no condition specified"))? - .into(); - - let rule = request - .rule - .ok_or_else(|| Status::invalid_argument("no rule specified"))? - .into(); - - let f = Box::new(move |state: &mut State| { - state.config.window_rules.push((cond, rule)); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } -} - -impl From for crate::window::rules::WindowRuleCondition { - fn from(cond: WindowRuleCondition) -> Self { - let cond_any = match cond.any.is_empty() { - true => None, - false => Some( - cond.any - .into_iter() - .map(crate::window::rules::WindowRuleCondition::from) - .collect::>(), - ), - }; - - let cond_all = match cond.all.is_empty() { - true => None, - false => Some( - cond.all - .into_iter() - .map(crate::window::rules::WindowRuleCondition::from) - .collect::>(), - ), - }; - - let class = match cond.classes.is_empty() { - true => None, - false => Some(cond.classes), - }; - - let title = match cond.titles.is_empty() { - true => None, - false => Some(cond.titles), - }; - - let tag = match cond.tags.is_empty() { - true => None, - false => Some(cond.tags.into_iter().map(TagId::Some).collect::>()), - }; - - crate::window::rules::WindowRuleCondition { - cond_any, - cond_all, - class, - title, - tag, - } - } -} - -impl From for crate::window::rules::WindowRule { - fn from(rule: WindowRule) -> Self { - let fullscreen_or_maximized = match rule.fullscreen_or_maximized() { - FullscreenOrMaximized::Unspecified => None, - FullscreenOrMaximized::Neither => { - Some(crate::window::window_state::FullscreenOrMaximized::Neither) - } - FullscreenOrMaximized::Fullscreen => { - Some(crate::window::window_state::FullscreenOrMaximized::Fullscreen) - } - FullscreenOrMaximized::Maximized => { - Some(crate::window::window_state::FullscreenOrMaximized::Maximized) - } - }; - let output = rule.output.map(OutputName); - let tags = match rule.tags.is_empty() { - true => None, - false => Some(rule.tags.into_iter().map(TagId::Some).collect::>()), - }; - let floating_or_tiled = rule.floating.map(|floating| match floating { - true => crate::window::rules::FloatingOrTiled::Floating, - false => crate::window::rules::FloatingOrTiled::Tiled, - }); - let size = rule.width.and_then(|w| { - rule.height.and_then(|h| { - Some(( - NonZeroU32::try_from(w as u32).ok()?, - NonZeroU32::try_from(h as u32).ok()?, - )) - }) - }); - let location = rule.x.and_then(|x| rule.y.map(|y| (x, y))); - - crate::window::rules::WindowRule { - output, - tags, - floating_or_tiled, - fullscreen_or_maximized, - size, - location, - } - } -} diff --git a/src/backend/udev.rs b/src/backend/udev.rs index 32a9b76..c94a8fb 100644 --- a/src/backend/udev.rs +++ b/src/backend/udev.rs @@ -72,7 +72,6 @@ use smithay_drm_extras::{ }; use crate::{ - api::msg::{Args, OutgoingMsg}, backend::Backend, config::ConnectorSavedState, output::OutputName, @@ -985,36 +984,11 @@ impl State { output.with_state(|state| state.tags = tags.clone()); } else { // Run any output callbacks - let clone = output.clone(); - self.schedule( - |dt| dt.state.api_state.stream.is_some(), - move |dt| { - let stream = dt - .state - .api_state - .stream - .as_ref() - .expect("stream doesn't exist"); - let mut stream = stream.lock().expect("couldn't lock stream"); - for callback_id in dt.state.config.output_callback_ids.iter() { - crate::api::send_to_client( - &mut stream, - &OutgoingMsg::CallCallback { - callback_id: *callback_id, - args: Some(Args::ConnectForAllOutputs { - output_name: clone.name(), - }), - }, - ) - .expect("Send to client failed"); - } - for grpc_sender in dt.state.config.grpc_output_callback_senders.iter() { - let _ = grpc_sender.send(Ok(ConnectForAllResponse { - output_name: Some(clone.name()), - })); - } - }, - ); + for sender in self.config.output_callback_senders.iter() { + let _ = sender.send(Ok(ConnectForAllResponse { + output_name: Some(output.name()), + })); + } } } diff --git a/src/config.rs b/src/config.rs index 1f87588..50f246c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,11 +1,8 @@ use crate::{ api::{ - msg::ModifierMask, - protocol::{ - InputService, OutputService, PinnacleService, ProcessService, TagService, WindowService, - }, - PinnacleSocketSource, + InputService, OutputService, PinnacleService, ProcessService, TagService, WindowService, }, + input::ModifierMask, output::OutputName, tag::Tag, window::rules::{WindowRule, WindowRuleCondition}, @@ -14,7 +11,6 @@ use std::{ collections::HashMap, path::{Path, PathBuf}, process::Stdio, - sync::{Arc, Mutex}, }; use anyhow::Context; @@ -32,9 +28,9 @@ use smithay::{ utils::{Logical, Point}, }; use sysinfo::ProcessRefreshKind; +use tokio::sync::mpsc::UnboundedSender; use toml::Table; -use crate::api::msg::{CallbackId, Modifier}; use xkbcommon::xkb::Keysym; use crate::{ @@ -57,8 +53,34 @@ pub struct Metaconfig { #[derive(serde::Deserialize, Debug)] pub struct Keybind { - pub modifiers: Vec, - pub key: Key, + modifiers: Vec, + key: Key, +} + +#[derive(serde::Deserialize, Debug, Clone, Copy)] +enum Modifier { + Shift, + Ctrl, + Alt, + Super, +} + +// TODO: refactor metaconfig input +impl From> for ModifierMask { + fn from(mods: Vec) -> Self { + let mut mask = ModifierMask::empty(); + + for m in mods { + match m { + Modifier::Shift => mask |= ModifierMask::SHIFT, + Modifier::Ctrl => mask |= ModifierMask::CTRL, + Modifier::Alt => mask |= ModifierMask::ALT, + Modifier::Super => mask |= ModifierMask::SUPER, + } + } + + mask + } } // TODO: accept xkbcommon names instead @@ -141,10 +163,7 @@ pub enum Key { pub struct Config { /// Window rules and conditions on when those rules should apply pub window_rules: Vec<(WindowRuleCondition, WindowRule)>, - /// All callbacks that should be run when outputs are connected - pub output_callback_ids: Vec, - pub grpc_output_callback_senders: - Vec>>, + pub output_callback_senders: Vec>>, /// Saved states when outputs are disconnected pub connector_saved_states: HashMap, } @@ -214,13 +233,6 @@ impl State { config_join_handle.abort(); } - if let Some(token) = self.api_state.socket_token { - // Should only happen if parsing the metaconfig failed - self.loop_handle.remove(token); - } - - let tx_channel = self.api_state.tx_channel.clone(); - // Love that trailing slash let data_home = PathBuf::from( crate::XDG_BASE_DIRS @@ -255,19 +267,6 @@ impl State { self.start_grpc_server(socket_dir.as_path())?; - self.system_processes - .refresh_processes_specifics(ProcessRefreshKind::new()); - - let multiple_instances = self - .system_processes - .processes_by_exact_name("pinnacle") - .filter(|proc| proc.thread_kind().is_none()) - .count() - > 1; - - let socket_source = PinnacleSocketSource::new(tx_channel, &socket_dir, multiple_instances) - .context("Failed to create socket source")?; - let reload_keybind = metaconfig.reload_keybind; let kill_keybind = metaconfig.kill_keybind; @@ -324,28 +323,9 @@ impl State { let reload_keybind = (reload_mask, Keysym::from(reload_keybind.key as u32)); let kill_keybind = (kill_mask, Keysym::from(kill_keybind.key as u32)); - let socket_token = self - .loop_handle - .insert_source(socket_source, |stream, _, data| { - if let Some(old_stream) = data - .state - .api_state - .stream - .replace(Arc::new(Mutex::new(stream))) - { - old_stream - .lock() - .expect("Couldn't lock old stream") - .shutdown(std::net::Shutdown::Both) - .expect("Couldn't shutdown old stream"); - } - })?; - self.input_state.reload_keybind = Some(reload_keybind); self.input_state.kill_keybind = Some(kill_keybind); - self.api_state.socket_token = Some(socket_token); - self.config_join_handle = Some(tokio::spawn(async move { let _ = child.wait().await; })); diff --git a/src/input.rs b/src/input.rs index 7c3b2ab..b45e13b 100644 --- a/src/input.rs +++ b/src/input.rs @@ -4,12 +4,7 @@ pub mod libinput; use std::{collections::HashMap, mem::Discriminant}; -use crate::{ - api::msg::{CallbackId, Modifier, MouseEdge, OutgoingMsg}, - focus::FocusTarget, - state::WithState, - window::WindowElement, -}; +use crate::{focus::FocusTarget, state::WithState, window::WindowElement}; use pinnacle_api_defs::pinnacle::input::v0alpha1::{ set_libinput_setting_request::Setting, set_mousebind_request, SetKeybindResponse, SetMousebindResponse, @@ -33,8 +28,6 @@ use xkbcommon::xkb::Keysym; use crate::state::State; -use self::libinput::LibinputSetting; - bitflags::bitflags! { #[derive(Debug, Hash, Copy, Clone, PartialEq, Eq)] pub struct ModifierMask: u8 { @@ -85,39 +78,30 @@ impl From<&ModifiersState> for ModifierMask { #[derive(Default)] pub struct InputState { - /// A hashmap of modifier keys and keycodes to callback IDs - pub keybinds: HashMap<(crate::api::msg::ModifierMask, Keysym), CallbackId>, - /// A hashmap of modifier keys and mouse button codes to callback IDs - pub mousebinds: HashMap<(crate::api::msg::ModifierMask, u32, MouseEdge), CallbackId>, - pub reload_keybind: Option<(crate::api::msg::ModifierMask, Keysym)>, - pub kill_keybind: Option<(crate::api::msg::ModifierMask, Keysym)>, - /// User defined libinput settings that will be applied - pub libinput_settings: Vec, + pub reload_keybind: Option<(ModifierMask, Keysym)>, + pub kill_keybind: Option<(ModifierMask, Keysym)>, /// All libinput devices that have been connected pub libinput_devices: Vec, - pub grpc_keybinds: + pub keybinds: HashMap<(ModifierMask, Keysym), UnboundedSender>>, - pub grpc_mousebinds: HashMap< + pub mousebinds: HashMap< (ModifierMask, u32, set_mousebind_request::MouseEdge), UnboundedSender>, >, #[allow(clippy::type_complexity)] - pub grpc_libinput_settings: - HashMap, Box>, + pub libinput_settings: HashMap, Box>, } impl std::fmt::Debug for InputState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("InputState") - .field("keybinds", &self.keybinds) - .field("mousebinds", &self.mousebinds) .field("reload_keybind", &self.reload_keybind) .field("kill_keybind", &self.kill_keybind) - .field("libinput_settings", &self.libinput_settings) .field("libinput_devices", &self.libinput_devices) - .field("grpc_keybinds", &self.grpc_keybinds) - .field("grpc_libinput_settings", &"...") + .field("keybinds", &self.keybinds) + .field("mousebinds", &self.mousebinds) + .field("libinput_settings", &"...") .finish() } } @@ -130,9 +114,7 @@ impl InputState { #[derive(Debug)] enum KeyAction { - /// Call a callback from a config process - CallCallback(CallbackId), - CallGrpcCallback(UnboundedSender>), + CallCallback(UnboundedSender>), Quit, SwitchVt(i32), ReloadConfig, @@ -257,59 +239,23 @@ impl State { |state, modifiers, keysym| { // tracing::debug!(keysym = ?keysym, raw_keysyms = ?keysym.raw_syms(), modified_syms = ?keysym.modified_syms()); if press_state == KeyState::Pressed { - let mut modifier_mask = Vec::::new(); - if modifiers.alt { - modifier_mask.push(Modifier::Alt); - } - if modifiers.shift { - modifier_mask.push(Modifier::Shift); - } - if modifiers.ctrl { - modifier_mask.push(Modifier::Ctrl); - } - if modifiers.logo { - modifier_mask.push(Modifier::Super); - } - let modifier_mask = crate::api::msg::ModifierMask::from(modifier_mask); - - let grpc_modifiers = ModifierMask::from(modifiers); + let mod_mask = ModifierMask::from(modifiers); let raw_sym = keysym.raw_syms().iter().next(); let mod_sym = keysym.modified_sym(); if let (Some(sender), _) | (None, Some(sender)) = ( - state - .input_state - .grpc_keybinds - .get(&(grpc_modifiers, mod_sym)), + state.input_state.keybinds.get(&(mod_mask, mod_sym)), raw_sym.and_then(|raw_sym| { - state - .input_state - .grpc_keybinds - .get(&(grpc_modifiers, *raw_sym)) + state.input_state.keybinds.get(&(mod_mask, *raw_sym)) }), ) { - return FilterResult::Intercept(KeyAction::CallGrpcCallback( - sender.clone(), - )); + return FilterResult::Intercept(KeyAction::CallCallback(sender.clone())); } - let cb_id_mod = state.input_state.keybinds.get(&(modifier_mask, mod_sym)); - - let cb_id_raw = raw_sym.and_then(|raw_sym| { - state.input_state.keybinds.get(&(modifier_mask, *raw_sym)) - }); - - match (cb_id_mod, cb_id_raw) { - (Some(cb_id), _) | (None, Some(cb_id)) => { - return FilterResult::Intercept(KeyAction::CallCallback(*cb_id)); - } - (None, None) => (), - } - - if kill_keybind == Some((modifier_mask, mod_sym)) { + if kill_keybind == Some((mod_mask, mod_sym)) { return FilterResult::Intercept(KeyAction::Quit); - } else if reload_keybind == Some((modifier_mask, mod_sym)) { + } else if reload_keybind == Some((mod_mask, mod_sym)) { return FilterResult::Intercept(KeyAction::ReloadConfig); } else if let mut vt @ keysyms::KEY_XF86Switch_VT_1 ..=keysyms::KEY_XF86Switch_VT_12 = keysym.modified_sym().raw() @@ -325,20 +271,7 @@ impl State { ); match action { - Some(KeyAction::CallCallback(callback_id)) => { - if let Some(stream) = self.api_state.stream.as_ref() { - if let Err(err) = crate::api::send_to_client( - &mut stream.lock().expect("Could not lock stream mutex"), - &OutgoingMsg::CallCallback { - callback_id, - args: None, - }, - ) { - tracing::error!("error sending msg to client: {err}"); - } - } - } - Some(KeyAction::CallGrpcCallback(sender)) => { + Some(KeyAction::CallCallback(sender)) => { let _ = sender.send(Ok(SetKeybindResponse {})); } Some(KeyAction::SwitchVt(vt)) => { @@ -367,42 +300,17 @@ impl State { let pointer_loc = pointer.current_location(); + let mod_mask = ModifierMask::from(keyboard.modifier_state()); + let mouse_edge = match button_state { - ButtonState::Released => MouseEdge::Release, - ButtonState::Pressed => MouseEdge::Press, - }; - let modifier_mask = crate::api::msg::ModifierMask::from(keyboard.modifier_state()); - - let grpc_modifier_mask = ModifierMask::from(keyboard.modifier_state()); - - // If any mousebinds are detected, call the config's callback and return. - if let Some(&callback_id) = - self.input_state - .mousebinds - .get(&(modifier_mask, button, mouse_edge)) - { - if let Some(stream) = self.api_state.stream.as_ref() { - crate::api::send_to_client( - &mut stream.lock().expect("failed to lock api stream"), - &OutgoingMsg::CallCallback { - callback_id, - args: None, - }, - ) - .expect("failed to call callback"); - } - return; - } - - let grpc_mouse_edge = match button_state { ButtonState::Released => set_mousebind_request::MouseEdge::Release, ButtonState::Pressed => set_mousebind_request::MouseEdge::Press, }; - if let Some(stream) = - self.input_state - .grpc_mousebinds - .get(&(grpc_modifier_mask, button, grpc_mouse_edge)) + if let Some(stream) = self + .input_state + .mousebinds + .get(&(mod_mask, button, mouse_edge)) { let _ = stream.send(Ok(SetMousebindResponse {})); } diff --git a/src/input/libinput.rs b/src/input/libinput.rs index 325cb54..f46b4fd 100644 --- a/src/input/libinput.rs +++ b/src/input/libinput.rs @@ -1,104 +1,7 @@ -use smithay::{ - backend::{input::InputEvent, libinput::LibinputInputBackend}, - reexports::input::{self, AccelProfile, ClickMethod, ScrollMethod, TapButtonMap}, -}; +use smithay::backend::{input::InputEvent, libinput::LibinputInputBackend}; use crate::state::State; -#[derive(Debug, serde::Deserialize)] -#[serde(remote = "AccelProfile")] -enum AccelProfileDef { - Flat, - Adaptive, -} - -#[derive(Debug, serde::Deserialize)] -#[serde(remote = "ClickMethod")] -enum ClickMethodDef { - ButtonAreas, - Clickfinger, -} - -#[derive(Debug, serde::Deserialize)] -#[serde(remote = "ScrollMethod")] -enum ScrollMethodDef { - NoScroll, - TwoFinger, - Edge, - OnButtonDown, -} - -#[derive(Debug, serde::Deserialize)] -#[serde(remote = "TapButtonMap")] -enum TapButtonMapDef { - LeftRightMiddle, - LeftMiddleRight, -} - -#[derive(Debug, PartialEq, Copy, Clone, serde::Deserialize)] -pub enum LibinputSetting { - #[serde(with = "AccelProfileDef")] - AccelProfile(AccelProfile), - AccelSpeed(f64), - CalibrationMatrix([f32; 6]), - #[serde(with = "ClickMethodDef")] - ClickMethod(ClickMethod), - DisableWhileTypingEnabled(bool), - LeftHanded(bool), - MiddleEmulationEnabled(bool), - RotationAngle(u32), - #[serde(with = "ScrollMethodDef")] - ScrollMethod(ScrollMethod), - NaturalScrollEnabled(bool), - ScrollButton(u32), - #[serde(with = "TapButtonMapDef")] - TapButtonMap(TapButtonMap), - TapDragEnabled(bool), - TapDragLockEnabled(bool), - TapEnabled(bool), -} - -impl LibinputSetting { - pub fn apply_to_device(&self, device: &mut input::Device) { - let _ = match self { - LibinputSetting::AccelProfile(profile) => device.config_accel_set_profile(*profile), - LibinputSetting::AccelSpeed(speed) => device.config_accel_set_speed(*speed), - LibinputSetting::CalibrationMatrix(matrix) => { - device.config_calibration_set_matrix(*matrix) - } - LibinputSetting::ClickMethod(method) => device.config_click_set_method(*method), - LibinputSetting::DisableWhileTypingEnabled(enabled) => { - device.config_dwt_set_enabled(*enabled) - } - LibinputSetting::LeftHanded(enabled) => device.config_left_handed_set(*enabled), - LibinputSetting::MiddleEmulationEnabled(enabled) => { - device.config_middle_emulation_set_enabled(*enabled) - } - LibinputSetting::RotationAngle(angle) => device.config_rotation_set_angle(*angle), - LibinputSetting::ScrollMethod(method) => device.config_scroll_set_method(*method), - LibinputSetting::NaturalScrollEnabled(enabled) => { - device.config_scroll_set_natural_scroll_enabled(*enabled) - } - LibinputSetting::ScrollButton(button) => device.config_scroll_set_button(*button), - LibinputSetting::TapButtonMap(map) => device.config_tap_set_button_map(*map), - LibinputSetting::TapDragEnabled(enabled) => { - device.config_tap_set_drag_enabled(*enabled) - } - LibinputSetting::TapDragLockEnabled(enabled) => { - device.config_tap_set_drag_lock_enabled(*enabled) - } - LibinputSetting::TapEnabled(enabled) => device.config_tap_set_enabled(*enabled), - }; - } -} - -// We want to completely replace old settings, so we hash only the discriminant. -impl std::hash::Hash for LibinputSetting { - fn hash(&self, state: &mut H) { - core::mem::discriminant(self).hash(state); - } -} - impl State { /// Apply current libinput settings to new devices. pub fn apply_libinput_settings(&mut self, event: &InputEvent) { @@ -117,10 +20,7 @@ impl State { return; } - for setting in self.input_state.libinput_settings.iter() { - setting.apply_to_device(&mut device); - } - for setting in self.input_state.grpc_libinput_settings.values() { + for setting in self.input_state.libinput_settings.values() { setting(&mut device); } diff --git a/src/main.rs b/src/main.rs index 8edef8b..c690d90 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ //! While Pinnacle is not a library, this documentation serves to guide those who want to //! contribute or learn how building something like this works. -// #![deny(unused_imports)] // gonna force myself to keep stuff clean +// #![deny(unused_imports)] // this has remained commented out for months lol #![warn(clippy::unwrap_used)] use clap::Parser; diff --git a/src/state.rs b/src/state.rs index 224e28e..b61c23b 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,22 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-or-later use crate::{ - api::{msg::Msg, ApiState}, - backend::Backend, - config::Config, - cursor::Cursor, - focus::FocusState, - grab::resize_grab::ResizeSurfaceState, - window::WindowElement, + backend::Backend, config::Config, cursor::Cursor, focus::FocusState, + grab::resize_grab::ResizeSurfaceState, window::WindowElement, }; use smithay::{ desktop::{PopupManager, Space}, input::{keyboard::XkbConfig, pointer::CursorImageStatus, Seat, SeatState}, reexports::{ - calloop::{ - self, channel::Event, generic::Generic, Interest, LoopHandle, LoopSignal, Mode, - PostAction, - }, + calloop::{generic::Generic, Interest, LoopHandle, LoopSignal, Mode, PostAction}, wayland_server::{ backend::{ClientData, ClientId, DisconnectReason}, protocol::wl_surface::WlSurface, @@ -74,8 +66,6 @@ pub struct State { /// The state of key and mousebinds along with libinput settings pub input_state: InputState, - /// The state holding stuff dealing with the api, like the stream - pub api_state: ApiState, /// Keeps track of the focus stack and focused output pub focus_state: FocusState, @@ -159,8 +149,6 @@ impl State { }, )?; - let (tx_channel, rx_channel) = calloop::channel::channel::(); - loop_handle.insert_idle(|data| { if let Err(err) = data.state.start_config(crate::config::get_config_dir()) { panic!("failed to start config: {err}"); @@ -174,16 +162,6 @@ impl State { seat.add_keyboard(XkbConfig::default(), 500, 25)?; - loop_handle.insert_idle(|data| { - data.state - .loop_handle - .insert_source(rx_channel, |msg, _, data| match msg { - Event::Msg(msg) => data.state.handle_msg(msg), - Event::Closed => todo!(), - }) - .expect("failed to insert rx_channel into loop"); - }); - let xwayland = { let (xwayland, channel) = XWayland::new(&display_handle); let clone = display_handle.clone(); @@ -253,11 +231,6 @@ impl State { layer_shell_state: WlrLayerShellState::new::(&display_handle), input_state: InputState::new(), - api_state: ApiState { - stream: None, - socket_token: None, - tx_channel, - }, focus_state: FocusState::new(), config: Config::default(),