mirror of
https://github.com/pinnacle-comp/pinnacle.git
synced 2025-01-18 22:26:12 +01:00
Merge pull request #131 from pinnacle-comp/rust_api_but_better
Rewrite Rust API
This commit is contained in:
commit
f95c6bd47a
34 changed files with 5073 additions and 5345 deletions
2
.github/workflows/rustdoc.yml
vendored
2
.github/workflows/rustdoc.yml
vendored
|
@ -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
30
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"]
|
||||
|
|
18
README.md
18
README.md
|
@ -27,7 +27,9 @@ Pinnacle is a Wayland compositor built in Rust using [Smithay](https://github.co
|
|||
It's my attempt at creating something like [AwesomeWM](https://github.com/awesomeWM/awesome)
|
||||
for Wayland.
|
||||
|
||||
It sports extensive configurability through either Lua or Rust, with the ability to add more languages in the future.
|
||||
It sports extensive configurability through either Lua or Rust, with the ability to add more languages
|
||||
in the future. <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
|
||||
|
|
|
@ -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? }
|
||||
|
|
|
@ -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
1
api/rust/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
Cargo.lock
|
|
@ -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"]
|
||||
|
|
151
api/rust/examples/default_config/main.rs
Normal file
151
api/rust/examples/default_config/main.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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"
|
|
@ -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);
|
||||
}
|
14
api/rust/pinnacle-api-macros/Cargo.toml
Normal file
14
api/rust/pinnacle-api-macros/Cargo.toml
Normal 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
|
166
api/rust/pinnacle-api-macros/src/lib.rs
Normal file
166
api/rust/pinnacle-api-macros/src/lib.rs
Normal 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 })
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
},
|
||||
}
|
|
@ -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
38
api/rust/src/pinnacle.rs
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
14
api/rust/src/util.rs
Normal 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,
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
2030
src/api.rs
File diff suppressed because it is too large
Load diff
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
335
src/api/msg.rs
335
src/api/msg.rs
|
@ -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>,
|
||||
},
|
||||
}
|
1887
src/api/protocol.rs
1887
src/api/protocol.rs
File diff suppressed because it is too large
Load diff
|
@ -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()),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}));
|
||||
|
|
138
src/input.rs
138
src/input.rs
|
@ -4,12 +4,7 @@ pub mod libinput;
|
|||
|
||||
use std::{collections::HashMap, mem::Discriminant};
|
||||
|
||||
use crate::{
|
||||
api::msg::{CallbackId, Modifier, MouseEdge, OutgoingMsg},
|
||||
focus::FocusTarget,
|
||||
state::WithState,
|
||||
window::WindowElement,
|
||||
};
|
||||
use crate::{focus::FocusTarget, state::WithState, window::WindowElement};
|
||||
use pinnacle_api_defs::pinnacle::input::v0alpha1::{
|
||||
set_libinput_setting_request::Setting, set_mousebind_request, SetKeybindResponse,
|
||||
SetMousebindResponse,
|
||||
|
@ -33,8 +28,6 @@ use xkbcommon::xkb::Keysym;
|
|||
|
||||
use crate::state::State;
|
||||
|
||||
use self::libinput::LibinputSetting;
|
||||
|
||||
bitflags::bitflags! {
|
||||
#[derive(Debug, Hash, Copy, Clone, PartialEq, Eq)]
|
||||
pub struct ModifierMask: u8 {
|
||||
|
@ -85,39 +78,30 @@ impl From<&ModifiersState> for ModifierMask {
|
|||
|
||||
#[derive(Default)]
|
||||
pub struct InputState {
|
||||
/// A hashmap of modifier keys and keycodes to callback IDs
|
||||
pub keybinds: HashMap<(crate::api::msg::ModifierMask, Keysym), CallbackId>,
|
||||
/// A hashmap of modifier keys and mouse button codes to callback IDs
|
||||
pub mousebinds: HashMap<(crate::api::msg::ModifierMask, u32, MouseEdge), CallbackId>,
|
||||
pub reload_keybind: Option<(crate::api::msg::ModifierMask, Keysym)>,
|
||||
pub kill_keybind: Option<(crate::api::msg::ModifierMask, Keysym)>,
|
||||
/// User defined libinput settings that will be applied
|
||||
pub libinput_settings: Vec<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 {}));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
33
src/state.rs
33
src/state.rs
|
@ -1,22 +1,14 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
use crate::{
|
||||
api::{msg::Msg, ApiState},
|
||||
backend::Backend,
|
||||
config::Config,
|
||||
cursor::Cursor,
|
||||
focus::FocusState,
|
||||
grab::resize_grab::ResizeSurfaceState,
|
||||
window::WindowElement,
|
||||
backend::Backend, config::Config, cursor::Cursor, focus::FocusState,
|
||||
grab::resize_grab::ResizeSurfaceState, window::WindowElement,
|
||||
};
|
||||
use smithay::{
|
||||
desktop::{PopupManager, Space},
|
||||
input::{keyboard::XkbConfig, pointer::CursorImageStatus, Seat, SeatState},
|
||||
reexports::{
|
||||
calloop::{
|
||||
self, channel::Event, generic::Generic, Interest, LoopHandle, LoopSignal, Mode,
|
||||
PostAction,
|
||||
},
|
||||
calloop::{generic::Generic, Interest, LoopHandle, LoopSignal, Mode, PostAction},
|
||||
wayland_server::{
|
||||
backend::{ClientData, ClientId, DisconnectReason},
|
||||
protocol::wl_surface::WlSurface,
|
||||
|
@ -74,8 +66,6 @@ pub struct State {
|
|||
|
||||
/// The state of key and mousebinds along with libinput settings
|
||||
pub input_state: InputState,
|
||||
/// The state holding stuff dealing with the api, like the stream
|
||||
pub api_state: ApiState,
|
||||
/// Keeps track of the focus stack and focused output
|
||||
pub focus_state: FocusState,
|
||||
|
||||
|
@ -159,8 +149,6 @@ impl State {
|
|||
},
|
||||
)?;
|
||||
|
||||
let (tx_channel, rx_channel) = calloop::channel::channel::<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(),
|
||||
|
|
Loading…
Reference in a new issue