Merge pull request #131 from pinnacle-comp/rust_api_but_better

Rewrite Rust API
This commit is contained in:
Ottatop 2024-01-22 20:32:47 -06:00 committed by GitHub
commit f95c6bd47a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 5073 additions and 5345 deletions

View file

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

30
Cargo.lock generated
View file

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

View file

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

View file

@ -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. <sup>And by that I mean other people can do the adding,
I'm already maintaining Lua and Rust lol</sup>
> ### More video examples below!
> <details>
@ -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

View file

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

View file

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

1
api/rust/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
Cargo.lock

View file

@ -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 <ottatop1227@gmail.com>"]
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"]

View file

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

View file

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

View file

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

View file

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

View file

@ -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<Punctuated<MetaNameValue, Token![,]>>,
}
impl Parse for MacroInput {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let ident = input.parse()?;
let comma = input.parse::<Token![,]>();
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 })
}
}

View file

@ -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<KeyIntOrString>` 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<KeyIntOrString>,
mut action: F,
callback_vec: &mut CallbackVec<'a>,
) where
F: FnMut(&mut CallbackVec) + 'a,
{
let args_callback = move |_: Option<Args>, 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<Args>, 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<char> 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<u32> 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<BoxFuture<'static, ()>>,
}
impl From<Keysym> for KeyIntOrString {
fn from(value: Keysym) -> Self {
Self::Int(value.raw())
impl Input {
pub(crate) fn new(
channel: Channel,
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
) -> 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<Channel> {
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<Item = Mod>,
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<Item = Mod>,
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)
}
}

View file

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

View file

@ -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<Pinnacle> = OnceLock::new();
static PROCESS: OnceLock<Process> = OnceLock::new();
static WINDOW: OnceLock<Window> = OnceLock::new();
static INPUT: OnceLock<Input> = OnceLock::new();
static OUTPUT: OnceLock<Output> = OnceLock::new();
static TAG: OnceLock<Tag> = 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<BoxFuture<'static, ()>>), Box<dyn std::error::Error>> {
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::<BoxFuture<()>>();
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<Mutex<UnixStream>> = OnceLock::new();
lazy_static::lazy_static! {
static ref UNREAD_CALLBACK_MSGS: Mutex<HashMap<CallbackId, IncomingMsg>> = Mutex::new(HashMap::new());
static ref UNREAD_REQUEST_MSGS: Mutex<HashMap<RequestId, IncomingMsg>> = 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<RequestId>) -> 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<BoxFuture<'static, ()>>) {
let mut future_set = FuturesUnordered::<
BoxFuture<(
Option<BoxFuture<()>>,
Option<UnboundedReceiver<BoxFuture<()>>>,
)>,
>::new();
for cb_id in unread_callback_msgs.keys().copied().collect::<Vec<_>>() {
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<Box<dyn FnMut(Option<Args>, &mut CallbackVec) + 'a>>,
}
impl<'a> CallbackVec<'a> {
/// Create a new, empty `CallbackVec`.
pub fn new() -> Self {
Default::default()
}
}

View file

@ -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<Vec<WindowRuleCondition>>,
/// This condition is met when all of the conditions provided are met.
#[serde(default)]
pub cond_all: Option<Vec<WindowRuleCondition>>,
/// This condition is met when the class matches.
#[serde(default)]
pub class: Option<Vec<String>>,
/// This condition is met when the title matches.
#[serde(default)]
pub title: Option<Vec<String>>,
/// This condition is met when the tag matches.
#[serde(default)]
pub tag: Option<Vec<TagId>>,
}
#[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<OutputName>,
/// Set the tags the output will have on open.
#[serde(default)]
pub tags: Option<Vec<TagId>>,
/// Set the window to floating or tiled on open.
#[serde(default)]
pub floating_or_tiled: Option<FloatingOrTiled>,
/// Set the window to fullscreen, maximized, or force it to neither.
#[serde(default)]
pub fullscreen_or_maximized: Option<FullscreenOrMaximized>,
/// 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<Modifier>,
callback_id: CallbackId,
},
SetMousebind {
modifiers: Vec<Modifier>,
button: u32,
edge: MouseEdge,
callback_id: CallbackId,
},
// Window management
CloseWindow {
window_id: WindowId,
},
SetWindowSize {
window_id: WindowId,
#[serde(default)]
width: Option<i32>,
#[serde(default)]
height: Option<i32>,
},
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<String>,
},
// TODO:
RemoveTags {
/// The name of the output you want these tags removed from.
tag_ids: Vec<TagId>,
},
SetLayout {
tag_id: TagId,
layout: Layout,
},
// Output management
ConnectForAllOutputs {
callback_id: CallbackId,
},
SetOutputLocation {
output_name: OutputName,
#[serde(default)]
x: Option<i32>,
#[serde(default)]
y: Option<i32>,
},
// Process management
/// Spawn a program with an optional callback.
Spawn {
command: Vec<String>,
#[serde(default)]
callback_id: Option<CallbackId>,
},
/// Spawn a program with an optional callback only if it isn't running.
SpawnOnce {
command: Vec<String>,
#[serde(default)]
callback_id: Option<CallbackId>,
},
SetEnv {
key: String,
value: String,
},
// Pinnacle management
/// Quit the compositor.
Quit,
// Input management
SetXkbConfig {
#[serde(default)]
rules: Option<String>,
#[serde(default)]
variant: Option<String>,
#[serde(default)]
layout: Option<String>,
#[serde(default)]
model: Option<String>,
#[serde(default)]
options: Option<String>,
},
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<String>,
#[serde(default)]
stderr: Option<String>,
#[serde(default)]
exit_code: Option<i32>,
#[serde(default)]
exit_msg: Option<String>,
},
ConnectForAllOutputs {
output_name: String,
},
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub(crate) enum IncomingMsg {
CallCallback {
callback_id: CallbackId,
#[serde(default)]
args: Option<Args>,
},
RequestResponse {
request_id: RequestId,
response: RequestResponse,
},
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub(crate) enum RequestResponse {
Window {
window_id: Option<WindowId>,
},
Windows {
window_ids: Vec<WindowId>,
},
WindowProps {
size: Option<(i32, i32)>,
loc: Option<(i32, i32)>,
class: Option<String>,
title: Option<String>,
focused: Option<bool>,
floating: Option<bool>,
fullscreen_or_maximized: Option<FullscreenOrMaximized>,
},
Output {
output_name: Option<String>,
},
Outputs {
output_names: Vec<String>,
},
OutputProps {
/// The make of the output.
make: Option<String>,
/// The model of the output.
model: Option<String>,
/// 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<i32>,
/// The size of the output, in millimeters.
physical_size: Option<(i32, i32)>,
/// Whether the output is focused or not.
focused: Option<bool>,
tag_ids: Option<Vec<TagId>>,
},
Tags {
tag_ids: Vec<TagId>,
},
TagProps {
active: Option<bool>,
name: Option<String>,
output_name: Option<String>,
},
}

View file

@ -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<OutputHandle> {
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<BoxFuture<'static, ()>>,
}
/// Get a handle to all connected outputs.
pub fn get_all() -> impl Iterator<Item = OutputHandle> {
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<OutputHandle> {
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<Args>, 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<BoxFuture<'static, ()>>,
) -> 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<Channel> {
OutputServiceClient::new(self.channel.clone())
}
let msg = Msg::ConnectForAllOutputs {
callback_id: CallbackId(len as u32),
};
fn create_tag_client(&self) -> TagServiceClient<Channel> {
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<Item = OutputHandle> {
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<String>) -> Option<OutputHandle> {
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<OutputHandle> {
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<Channel>,
pub(crate) tag_client: TagServiceClient<Channel>,
pub(crate) name: String,
}
/// Properties of an output.
pub struct OutputProperties {
/// The make.
pub make: Option<String>,
/// The model.
///
/// This is something like `27GL850` or whatever gibberish monitor manufacturers name their
/// displays.
pub model: Option<String>,
/// 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<i32>,
/// The physical size of the output in millimeters.
pub physical_size: Option<(i32, i32)>,
/// Whether or not the output is focused.
pub focused: Option<bool>,
/// The tags on this output.
pub tags: Vec<TagHandle>,
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<H: std::hash::Hasher>(&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<Option<i32>>, y: impl Into<Option<i32>>) {
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<i32>, y: Option<i32>) {
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<String> {
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<String> {
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<i32> {
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<i32> {
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<u32> {
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<u32> {
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<u32> {
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<u32> {
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<u32> {
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<bool> {
self.props().focused
}
/// Get the tags this output has.
///
/// Shorthand for `self.props().tags`
pub fn tags(&self) -> Vec<TagHandle> {
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<String>,
/// The model of the output
///
/// This is something like "27GL83A" or whatever crap monitor manufacturers name their monitors
/// these days.
pub model: Option<String>,
/// The x position of the output in the global space
pub x: Option<i32>,
/// The y position of the output in the global space
pub y: Option<i32>,
/// The output's screen width in pixels
pub pixel_width: Option<u32>,
/// The output's screen height in pixels
pub pixel_height: Option<u32>,
/// The output's refresh rate in millihertz
pub refresh_rate: Option<u32>,
/// The output's physical width in millimeters
pub physical_width: Option<u32>,
/// The output's physical height in millimeters
pub physical_height: Option<u32>,
/// Whether this output is focused or not
///
/// This is currently implemented as the output with the most recent pointer motion.
pub focused: Option<bool>,
/// The tags this output has
pub tags: Vec<TagHandle>,
}

38
api/rust/src/pinnacle.rs Normal file
View file

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

View file

@ -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<BoxFuture<'static, ()>>,
}
/// 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<Box<dyn FnMut(String) + Send>>,
/// A callback that will be run when a process prints to stderr with a line
pub stderr: Option<Box<dyn FnMut(String) + Send>>,
/// A callback that will be run when a process exits with a status code and message
#[allow(clippy::type_complexity)]
pub exit: Option<Box<dyn FnMut(Option<i32>, 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<String>, Option<String>, Option<i32>, Option<String>, &mut CallbackVec) + 'a,
{
let args_callback = move |args: Option<Args>, 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<BoxFuture<'static, ()>>,
) -> 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<Channel> {
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<Item = impl Into<String>>) {
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<String>, Option<String>, Option<i32>, Option<String>, &mut CallbackVec) + 'a,
{
let args_callback = move |args: Option<Args>, 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<Item = impl Into<String>>,
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<Item = impl Into<String>>) {
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<Item = impl Into<String>>,
callbacks: SpawnCallbacks,
) {
self.spawn_inner(args, true, Some(callbacks));
}
fn spawn_inner(
&self,
args: impl IntoIterator<Item = impl Into<String>>,
once: bool,
callbacks: Option<SpawnCallbacks>,
) {
let mut client = self.create_process_client();
let args = args.into_iter().map(Into::into).collect::<Vec<_>>();
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<String>, value: impl Into<String>) {
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();
}
}

View file

@ -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<TagHandle> {
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<BoxFuture<'static, ()>>,
}
impl Tag {
pub(crate) fn new(
channel: Channel,
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
) -> Self {
Self {
channel,
fut_sender,
}
}
fn create_tag_client(&self) -> TagServiceClient<Channel> {
TagServiceClient::new(self.channel.clone())
}
fn create_output_client(&self) -> OutputServiceClient<Channel> {
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<Item = impl Into<String>>,
) -> impl Iterator<Item = TagHandle> {
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<Item = TagHandle> {
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<Item = TagHandle> {
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<String>) -> Option<TagHandle> {
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::<TagId, usize>::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<String>,
output: &OutputHandle,
) -> Option<TagHandle> {
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<Item = TagHandle>) {
let tag_ids = tags.into_iter().map(|handle| handle.id).collect::<Vec<_>>();
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<Item = Layout>) -> LayoutCycler {
let indices = Arc::new(Mutex::new(HashMap::<u32, usize>::new()));
let indices_clone = indices.clone();
let layouts = layouts.into_iter().collect::<Vec<_>>();
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<dyn FnMut(Option<&OutputHandle>)>,
pub prev: Box<dyn Fn(Option<&OutputHandle>) + Send + Sync + 'static>,
/// Cycle to the previous layout on the given output, or the focused output if `None`.
pub prev: Box<dyn FnMut(Option<&OutputHandle>)>,
}
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub(crate) enum TagId {
None,
#[serde(untagged)]
Some(u32),
pub next: Box<dyn Fn(Option<&OutputHandle>) + 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<bool>,
/// The tag's name.
pub name: Option<String>,
/// The output the tag is on.
pub output: Option<OutputHandle>,
///
/// 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<Channel>,
pub(crate) output_client: OutputServiceClient<Channel>,
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<H: std::hash::Hasher>(&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::<Vec<_>>;
///
/// 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<bool> {
self.props().active
}
/// Get this tag's name.
///
/// Shorthand for `self.props().name`.
pub fn name(&self) -> Option<String> {
self.props().name
}
/// Get a handle to the output this tag is on.
///
/// Shorthand for `self.props().output`.
pub fn output(&self) -> Option<OutputHandle> {
self.props().output
}
}
/// Properties of a tag.
pub struct TagProperties {
/// Whether the tag is active or not
pub active: Option<bool>,
/// The name of the tag
pub name: Option<String>,
/// The output the tag is on
pub output: Option<OutputHandle>,
}

14
api/rust/src/util.rs Normal file
View file

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

View file

@ -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<Item = WindowHandle> + '_ {
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<WindowHandle> {
get_all().find(|win| win.properties().focused.is_some_and(|focused| focused))
}
/// Get all windows.
pub fn get_all() -> impl Iterator<Item = WindowHandle> {
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<String>,
/// The window's title.
pub title: Option<String>,
/// Whether or not the window is focused.
pub focused: Option<bool>,
/// Whether or not the window is floating.
pub floating: Option<bool>,
/// Whether the window is fullscreen, maximized, or neither.
pub fullscreen_or_maximized: Option<FullscreenOrMaximized>,
}
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<Channel> {
WindowServiceClient::new(self.channel.clone())
}
fn create_tag_client(&self) -> TagServiceClient<Channel> {
TagServiceClient::new(self.channel.clone())
}
fn create_output_client(&self) -> OutputServiceClient<Channel> {
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<i32>, height: Option<i32>) {
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<Item = WindowHandle> {
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<WindowHandle> {
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<Channel>,
pub(crate) id: u32,
pub(crate) tag_client: TagServiceClient<Channel>,
pub(crate) output_client: OutputServiceClient<Channel>,
}
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<H: std::hash::Hasher>(&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<Geometry>,
/// The window's class
pub class: Option<String>,
/// The window's title
pub title: Option<String>,
/// Whether the window is focused or not
pub focused: Option<bool>,
/// 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<bool>,
/// Whether the window is fullscreen, maximized, or neither
pub fullscreen_or_maximized: Option<FullscreenOrMaximized>,
/// All the tags on the window
pub tags: Vec<TagHandle>,
}
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<Geometry> {
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<String> {
self.props().class
}
send_msg(msg).unwrap();
/// Get this window's title.
///
/// Shorthand for `self.props().title`.
pub fn title(&self) -> Option<String> {
self.props().title
}
/// Get whether or not this window is focused.
///
/// Shorthand for `self.props().focused`.
pub fn focused(&self) -> Option<bool> {
self.props().focused
}
/// Get whether or not this window is floating.
///
/// Shorthand for `self.props().floating`.
pub fn floating(&self) -> Option<bool> {
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<FullscreenOrMaximized> {
self.props().fullscreen_or_maximized
}
/// Get all the tags on this window.
///
/// Shorthand for `self.props().tags`.
pub fn tags(&self) -> Vec<TagHandle> {
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,
}

View file

@ -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<Item = impl Into<String>>`. 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<Item = WindowRuleCondition>) -> 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<Item = WindowRuleCondition>) -> 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<Item = impl Into<String>>) -> 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<Item = impl Into<String>>) -> 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<Item = &'a TagHandle>) -> 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<Item = &'a TagHandle>) -> 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
}
}

2030
src/api.rs

File diff suppressed because it is too large Load diff

View file

@ -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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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::<XdgToplevelSurfaceData>()
.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::<Vec<_>>();
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::<Vec<_>>()
})
});
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::<Vec<_>>();
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<String>, callback_id: Option<CallbackId>) {
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);
}
}
}

View file

@ -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<Modifier>,
callback_id: CallbackId,
},
SetMousebind {
modifiers: Vec<Modifier>,
button: u32,
edge: MouseEdge,
callback_id: CallbackId,
},
// Window management
CloseWindow {
window_id: WindowId,
},
SetWindowSize {
window_id: WindowId,
#[serde(default)]
width: Option<i32>,
#[serde(default)]
height: Option<i32>,
},
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<String>,
},
RemoveTags {
/// The name of the output you want these tags removed from.
tag_ids: Vec<TagId>,
},
SetLayout {
tag_id: TagId,
layout: Layout,
},
// Output management
ConnectForAllOutputs {
callback_id: CallbackId,
},
SetOutputLocation {
output_name: OutputName,
#[serde(default)]
x: Option<i32>,
#[serde(default)]
y: Option<i32>,
},
// Process management
/// Spawn a program with an optional callback.
Spawn {
command: Vec<String>,
#[serde(default)]
callback_id: Option<CallbackId>,
},
SpawnOnce {
command: Vec<String>,
#[serde(default)]
callback_id: Option<CallbackId>,
},
SetEnv {
key: String,
value: String,
},
// Pinnacle management
/// Quit the compositor.
Quit,
// Input management
SetXkbConfig {
#[serde(default)]
rules: Option<String>,
#[serde(default)]
variant: Option<String>,
#[serde(default)]
layout: Option<String>,
#[serde(default)]
model: Option<String>,
#[serde(default)]
options: Option<String>,
},
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<Vec<Modifier>> for ModifierMask {
fn from(value: Vec<Modifier>) -> 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<ModifiersState> 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<Modifier> {
let mut res = Vec::<Modifier>::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<Args>,
},
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<String>,
#[serde(default)]
stderr: Option<String>,
#[serde(default)]
exit_code: Option<i32>,
#[serde(default)]
exit_msg: Option<String>,
},
ConnectForAllOutputs {
output_name: String,
},
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub enum RequestResponse {
Window {
window_id: Option<WindowId>,
},
Windows {
window_ids: Vec<WindowId>,
},
WindowProps {
size: Option<(i32, i32)>,
loc: Option<(i32, i32)>,
class: Option<String>,
title: Option<String>,
focused: Option<bool>,
floating: Option<bool>,
fullscreen_or_maximized: Option<FullscreenOrMaximized>,
},
Output {
output_name: Option<String>,
},
Outputs {
output_names: Vec<String>,
},
OutputProps {
/// The make of the output.
make: Option<String>,
/// The model of the output.
model: Option<String>,
/// 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<i32>,
/// The size of the output, in millimeters.
physical_size: Option<(i32, i32)>,
/// Whether the output is focused or not.
focused: Option<bool>,
tag_ids: Option<Vec<TagId>>,
},
Tags {
tag_ids: Vec<TagId>,
},
TagProps {
active: Option<bool>,
name: Option<String>,
output_name: Option<String>,
},
}

File diff suppressed because it is too large Load diff

View file

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

View file

@ -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<Modifier>,
pub key: Key,
modifiers: Vec<Modifier>,
key: Key,
}
#[derive(serde::Deserialize, Debug, Clone, Copy)]
enum Modifier {
Shift,
Ctrl,
Alt,
Super,
}
// TODO: refactor metaconfig input
impl From<Vec<self::Modifier>> for ModifierMask {
fn from(mods: Vec<self::Modifier>) -> 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<CallbackId>,
pub grpc_output_callback_senders:
Vec<tokio::sync::mpsc::UnboundedSender<Result<ConnectForAllResponse, tonic::Status>>>,
pub output_callback_senders: Vec<UnboundedSender<Result<ConnectForAllResponse, tonic::Status>>>,
/// Saved states when outputs are disconnected
pub connector_saved_states: HashMap<OutputName, ConnectorSavedState>,
}
@ -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;
}));

View file

@ -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<LibinputSetting>,
pub reload_keybind: Option<(ModifierMask, Keysym)>,
pub kill_keybind: Option<(ModifierMask, Keysym)>,
/// All libinput devices that have been connected
pub libinput_devices: Vec<input::Device>,
pub grpc_keybinds:
pub keybinds:
HashMap<(ModifierMask, Keysym), UnboundedSender<Result<SetKeybindResponse, tonic::Status>>>,
pub grpc_mousebinds: HashMap<
pub mousebinds: HashMap<
(ModifierMask, u32, set_mousebind_request::MouseEdge),
UnboundedSender<Result<SetMousebindResponse, tonic::Status>>,
>,
#[allow(clippy::type_complexity)]
pub grpc_libinput_settings:
HashMap<Discriminant<Setting>, Box<dyn Fn(&mut input::Device) + Send>>,
pub libinput_settings: HashMap<Discriminant<Setting>, Box<dyn Fn(&mut input::Device) + Send>>,
}
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<Result<SetKeybindResponse, tonic::Status>>),
CallCallback(UnboundedSender<Result<SetKeybindResponse, tonic::Status>>),
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::<Modifier>::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 {}));
}

View file

@ -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<H: std::hash::Hasher>(&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<LibinputInputBackend>) {
@ -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);
}

View file

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

View file

@ -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::<Msg>();
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::<Self>(&display_handle),
input_state: InputState::new(),
api_state: ApiState {
stream: None,
socket_token: None,
tx_channel,
},
focus_state: FocusState::new(),
config: Config::default(),