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:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
- name: Get protoc
|
||||||
|
run: sudo apt install protobuf-compiler
|
||||||
- name: Build docs
|
- name: Build docs
|
||||||
run: cd ./api/rust && cargo doc
|
run: cd ./api/rust && cargo doc
|
||||||
- name: Create index.html
|
- 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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "paste"
|
|
||||||
version = "1.0.14"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
|
@ -1556,8 +1550,6 @@ dependencies = [
|
||||||
"pinnacle-api-defs",
|
"pinnacle-api-defs",
|
||||||
"prost",
|
"prost",
|
||||||
"prost-types",
|
"prost-types",
|
||||||
"rmp",
|
|
||||||
"rmp-serde",
|
|
||||||
"serde",
|
"serde",
|
||||||
"shellexpand",
|
"shellexpand",
|
||||||
"smithay",
|
"smithay",
|
||||||
|
@ -1868,28 +1860,6 @@ version = "0.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
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]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.23"
|
version = "0.1.23"
|
||||||
|
|
|
@ -19,8 +19,6 @@ thiserror = "1"
|
||||||
xcursor = { version = "0.3", optional = true }
|
xcursor = { version = "0.3", optional = true }
|
||||||
image = { version = "0.24", default-features = false, optional = true }
|
image = { version = "0.24", default-features = false, optional = true }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
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 }
|
x11rb = { version = "0.13", default-features = false, features = ["composite"], optional = true }
|
||||||
shellexpand = "3.1.0"
|
shellexpand = "3.1.0"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
|
@ -65,3 +63,4 @@ xwayland = ["smithay/xwayland", "x11rb", "smithay/x11rb_event_source", "xcursor"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["pinnacle-api-defs"]
|
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)
|
It's my attempt at creating something like [AwesomeWM](https://github.com/awesomeWM/awesome)
|
||||||
for Wayland.
|
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!
|
> ### More video examples below!
|
||||||
> <details>
|
> <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
|
- [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
|
- [Lua](https://www.lua.org/) 5.4 or newer, to use the Lua API
|
||||||
- Packages for [Smithay](https://github.com/Smithay/smithay):
|
- 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:
|
- Arch:
|
||||||
```sh
|
```sh
|
||||||
sudo pacman -S wayland wayland-protocols libxkbcommon systemd-libs libinput mesa seatd xorg-xwayland
|
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
|
# Configuration
|
||||||
Pinnacle is configured in your choice of Lua or Rust.
|
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
|
## Out-of-the-box configurations
|
||||||
If you just want to test Pinnacle out without copying stuff to your config directory,
|
If you just want to test Pinnacle out without copying stuff to your config directory,
|
||||||
run one of the following in the crate root:
|
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
|
PINNACLE_CONFIG_DIR="~/.local/share/pinnacle/default_config" cargo run
|
||||||
|
|
||||||
# For a Rust configuration
|
# For a Rust configuration
|
||||||
PINNACLE_CONFIG_DIR="./api/rust" cargo run
|
PINNACLE_CONFIG_DIR="./api/rust/examples/default_config" cargo run
|
||||||
```
|
```
|
||||||
|
|
||||||
## Custom configuration
|
## Custom configuration
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!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 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
|
> 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`.
|
1. In `~/.config/pinnacle`, run `cargo init`.
|
||||||
2. In the `Cargo.toml` file, add the following under `[dependencies]`:
|
2. In the `Cargo.toml` file, add the following under `[dependencies]`:
|
||||||
```toml
|
```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:
|
3. Create the file `metaconfig.toml` at the root. Add the following to the file:
|
||||||
```toml
|
```toml
|
||||||
|
@ -188,7 +186,7 @@ command = ["cargo", "run"]
|
||||||
reload_keybind = { modifiers = ["Ctrl", "Alt"], key = "r" }
|
reload_keybind = { modifiers = ["Ctrl", "Alt"], key = "r" }
|
||||||
kill_keybind = { modifiers = ["Ctrl", "Alt", "Shift"], key = "escape" }
|
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.)
|
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
|
### API Documentation
|
||||||
|
|
|
@ -191,7 +191,7 @@ end
|
||||||
--- -- ┌─────┤ │
|
--- -- ┌─────┤ │
|
||||||
--- -- │DP-1 │HDMI-1 │
|
--- -- │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? }
|
---@param loc { x: integer?, y: integer? }
|
||||||
|
|
|
@ -108,6 +108,18 @@ function Process:spawn_once(args, callbacks)
|
||||||
spawn_inner(self.config_client, args, callbacks, true)
|
spawn_inner(self.config_client, args, callbacks, true)
|
||||||
end
|
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)
|
function process.new(config_client)
|
||||||
---@type Process
|
---@type Process
|
||||||
local self = { config_client = config_client }
|
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]
|
[package]
|
||||||
name = "pinnacle_api"
|
name = "pinnacle-api"
|
||||||
version = "0.0.1"
|
version = "0.0.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
authors = ["Ottatop <ottatop1227@gmail.com>"]
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
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]
|
[dependencies]
|
||||||
serde = { version = "1.0.188", features = ["derive"] }
|
pinnacle-api-defs = { path = "../../pinnacle-api-defs" }
|
||||||
rmp = { version = "0.8.12" }
|
pinnacle-api-macros = { path = "./pinnacle-api-macros" }
|
||||||
rmp-serde = { version = "1.1.2" }
|
tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread", "net"] }
|
||||||
anyhow = { version = "1.0.75", features = ["backtrace"] }
|
async-net = "2.0.0"
|
||||||
lazy_static = "1.4.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"
|
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/
|
# ~/.config/pinnacle/
|
||||||
#
|
#
|
||||||
# When Pinnacle finds a metaconfig.toml file, it will execute the command provided to `command`.
|
# 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.
|
# To use a Rust config, this should be changed to something like ["cargo", "run"].
|
||||||
# In the future, there will be a Rust API that can be run using `cargo run`.
|
|
||||||
#
|
#
|
||||||
# Because configuration is done using an external process, if it ever crashes, you lose all of your keybinds.
|
# 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
|
# In order prevent you from getting stuck in the compositor, you must define keybinds to reload your config
|
||||||
# and kill Pinnacle.
|
# and kill Pinnacle.
|
||||||
#
|
#
|
||||||
|
@ -19,7 +20,7 @@
|
||||||
# The command Pinnacle will run on startup and when you reload your config.
|
# 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.
|
# Paths are relative to the directory the metaconfig.toml file is in.
|
||||||
# This must be an array.
|
# This must be an array.
|
||||||
command = ["cargo", "run", "--example", "example_config"]
|
command = ["cargo", "run", "--example", "default_config"]
|
||||||
|
|
||||||
### Keybinds ###
|
### Keybinds ###
|
||||||
# Each keybind takes in a table with two fields: `modifiers` and `key`.
|
# 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/"
|
# socket_dir = "/your/dir/here/"
|
||||||
|
|
||||||
### Environment Variables ###
|
### Environment Variables ###
|
||||||
# You may need to specify to Lua where Pinnacle's Lua API library is.
|
# If you need to spawn your config with any environment variables, list them here.
|
||||||
# This is currently done using the `envs` table, with keys as the name of the environment variable and
|
|
||||||
# the value as the variable value. This supports $var expansion, and paths are relative to this metaconfig.toml file.
|
|
||||||
#
|
|
||||||
# Pinnacle will run your config with the additional PINNACLE_DIR environment variable.
|
|
||||||
#
|
|
||||||
# Here, LUA_PATH and LUA_CPATH are used to tell Lua the path to the library.
|
|
||||||
[envs]
|
[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"
|
# key = "value"
|
||||||
# LUA_CPATH = "$PINNACLE_LIB_DIR/lua/lib/?.so;$LUA_CPATH"
|
|
|
@ -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.
|
//! 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;
|
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.
|
/// A mouse button.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||||
pub enum MouseButton {
|
pub enum MouseButton {
|
||||||
/// The left mouse button.
|
/// The left mouse button
|
||||||
Left = 0x110,
|
Left = 0x110,
|
||||||
/// The right mouse button.
|
/// The right mouse button
|
||||||
Right,
|
Right = 0x111,
|
||||||
/// The middle mouse button, pressed usually by clicking the scroll wheel.
|
/// The middle mouse button
|
||||||
Middle,
|
Middle = 0x112,
|
||||||
///
|
/// The side mouse button
|
||||||
Side,
|
Side = 0x113,
|
||||||
///
|
/// The extra mouse button
|
||||||
Extra,
|
Extra = 0x114,
|
||||||
///
|
/// The forward mouse button
|
||||||
Forward,
|
Forward = 0x115,
|
||||||
///
|
/// The backward mouse button
|
||||||
Back,
|
Back = 0x116,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The edge on which you want things to trigger.
|
/// Keyboard modifiers.
|
||||||
#[derive(Debug, Hash, serde::Serialize, serde::Deserialize, Clone, Copy, PartialEq, Eq)]
|
#[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 {
|
pub enum MouseEdge {
|
||||||
/// Actions will be triggered on button press.
|
/// Perform actions on button press
|
||||||
Press,
|
Press = 1,
|
||||||
/// Actions will be triggered on button release.
|
/// Perform actions on button release
|
||||||
Release,
|
Release,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<char> for KeyIntOrString {
|
/// A struct that lets you define xkeyboard config options.
|
||||||
fn from(value: char) -> Self {
|
///
|
||||||
Self::String(value.to_string())
|
/// 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 {
|
/// The `Input` struct.
|
||||||
fn from(value: u32) -> Self {
|
///
|
||||||
Self::Int(value)
|
/// 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 {
|
impl Input {
|
||||||
fn from(value: Keysym) -> Self {
|
pub(crate) fn new(
|
||||||
Self::Int(value.raw())
|
channel: Channel,
|
||||||
|
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
channel,
|
||||||
|
fut_sender,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// A modifier key.
|
fn create_input_client(&self) -> InputServiceClient<Channel> {
|
||||||
#[derive(Debug, PartialEq, Eq, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
InputServiceClient::new(self.channel.clone())
|
||||||
pub enum Modifier {
|
}
|
||||||
/// The shift key.
|
|
||||||
Shift,
|
/// Set a keybind.
|
||||||
/// The control key.
|
|
||||||
Ctrl,
|
|
||||||
/// The alt key.
|
|
||||||
Alt,
|
|
||||||
/// The super key.
|
|
||||||
///
|
///
|
||||||
/// This is also known as the Windows key, meta, or Mod4 for those coming from Xorg.
|
/// If called with an already set keybind, it gets replaced.
|
||||||
Super,
|
///
|
||||||
|
/// 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};
|
/// Pointer acceleration profile
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
/// 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)]
|
|
||||||
pub enum AccelProfile {
|
pub enum AccelProfile {
|
||||||
/// Flat pointer acceleration.
|
/// A flat acceleration profile.
|
||||||
Flat,
|
|
||||||
/// Adaptive pointer acceleration.
|
|
||||||
///
|
///
|
||||||
/// 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,
|
Adaptive,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The click method for a touchpad.
|
/// The click method defines when to generate software-emulated buttons, usually on a device
|
||||||
#[derive(Debug, PartialEq, Copy, Clone, serde::Serialize)]
|
/// that does not have a specific physical button available.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub enum ClickMethod {
|
pub enum ClickMethod {
|
||||||
/// Use software-button areas to generate button events.
|
/// Use software-button areas to generate button events.
|
||||||
ButtonAreas,
|
ButtonAreas = 1,
|
||||||
/// The number of fingers decides which button press to generate.
|
/// The number of fingers decides which button press to generate.
|
||||||
Clickfinger,
|
Clickfinger,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The scroll method for a touchpad.
|
/// The scroll method of a device selects when to generate scroll axis events instead of pointer motion events.
|
||||||
#[derive(Debug, PartialEq, Copy, Clone, serde::Serialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub enum ScrollMethod {
|
pub enum ScrollMethod {
|
||||||
/// Never send scroll events.
|
/// Never send scroll events instead of pointer motion events.
|
||||||
NoScroll,
|
///
|
||||||
|
/// This has no effect on events generated by scroll wheels.
|
||||||
|
NoScroll = 1,
|
||||||
/// Send scroll events when two fingers are logically down on the device.
|
/// Send scroll events when two fingers are logically down on the device.
|
||||||
TwoFinger,
|
TwoFinger,
|
||||||
/// Send scroll events when a finger moves along the bottom or right edge of a device.
|
/// Send scroll events when a finger moves along the bottom or right edge of a device.
|
||||||
|
@ -43,63 +38,48 @@ pub enum ScrollMethod {
|
||||||
OnButtonDown,
|
OnButtonDown,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The mapping between finger count and button event for a touchpad.
|
/// Map 1/2/3 finger tips to buttons.
|
||||||
#[derive(Debug, PartialEq, Copy, Clone, serde::Serialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub enum TapButtonMap {
|
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,
|
LeftRightMiddle,
|
||||||
/// 1/2/3 finger tap is mapped to left/middle/right click.
|
/// 1/2/3 finger tap maps to left/middle/right
|
||||||
LeftMiddleRight,
|
LeftMiddleRight,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Libinput settings.
|
/// Possible settings for libinput.
|
||||||
#[derive(Debug, PartialEq, Copy, Clone, serde::Serialize)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub enum LibinputSetting {
|
pub enum LibinputSetting {
|
||||||
/// Set the acceleration profile.
|
/// Set the pointer acceleration profile
|
||||||
AccelProfile(AccelProfile),
|
AccelProfile(AccelProfile),
|
||||||
/// Set the acceleration speed.
|
/// Set pointer acceleration speed
|
||||||
///
|
|
||||||
/// This should be a float from -1.0 to 1.0.
|
|
||||||
AccelSpeed(f64),
|
AccelSpeed(f64),
|
||||||
/// Set the calibration matrix.
|
/// Set the calibration matrix
|
||||||
CalibrationMatrix([f32; 6]),
|
CalibrationMatrix([f32; 6]),
|
||||||
/// Set the click method.
|
/// Set the [`ClickMethod`]
|
||||||
///
|
|
||||||
/// The click method defines when to generate software-emulated buttons, usually on a device
|
|
||||||
/// that does not have a specific physical button available.
|
|
||||||
ClickMethod(ClickMethod),
|
ClickMethod(ClickMethod),
|
||||||
/// Set whether or not the device will be disabled while typing.
|
/// Set whether the device gets disabled while typing
|
||||||
DisableWhileTypingEnabled(bool),
|
DisableWhileTyping(bool),
|
||||||
/// Set device left-handedness.
|
/// Set left handed mode
|
||||||
LeftHanded(bool),
|
LeftHanded(bool),
|
||||||
/// Set whether or not the middle click can be emulated.
|
/// Allow or disallow middle mouse button emulation
|
||||||
MiddleEmulationEnabled(bool),
|
MiddleEmulation(bool),
|
||||||
/// Set the rotation angle of a device.
|
/// Set the rotation angle
|
||||||
RotationAngle(u32),
|
RotationAngle(u32),
|
||||||
/// Set the scroll method.
|
/// Set the scroll button
|
||||||
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.
|
|
||||||
ScrollButton(u32),
|
ScrollButton(u32),
|
||||||
/// Set the tap button map,
|
/// Set whether the scroll button should be a drag or toggle
|
||||||
///
|
ScrollButtonLock(bool),
|
||||||
/// This determines whether taps with 2 and 3 fingers register as right and middle clicks or
|
/// Set the [`ScrollMethod`]
|
||||||
/// the reverse.
|
ScrollMethod(ScrollMethod),
|
||||||
|
/// Enable or disable natural scrolling
|
||||||
|
NaturalScroll(bool),
|
||||||
|
/// Set the [`TapButtonMap`]
|
||||||
TapButtonMap(TapButtonMap),
|
TapButtonMap(TapButtonMap),
|
||||||
/// Set whether or not tap-and-drag is enabled.
|
/// Enable or disable tap-to-drag
|
||||||
///
|
TapDrag(bool),
|
||||||
/// When enabled, a single-finger tap immediately followed by a finger down results in
|
/// Enable or disable a timeout where lifting a finger off the device will not stop dragging
|
||||||
/// a button down event, and subsequent finger motion thus triggers a drag.
|
TapDragLock(bool),
|
||||||
/// The button is released on finger up.
|
/// Enable or disable tap-to-click
|
||||||
TapDragEnabled(bool),
|
Tap(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),
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)]
|
#![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;
|
pub mod input;
|
||||||
mod msg;
|
|
||||||
pub mod output;
|
pub mod output;
|
||||||
|
pub mod pinnacle;
|
||||||
pub mod process;
|
pub mod process;
|
||||||
pub mod tag;
|
pub mod tag;
|
||||||
|
pub mod util;
|
||||||
pub mod window;
|
pub mod window;
|
||||||
|
|
||||||
/// The xkbcommon crate, re-exported for your convenience.
|
pub use pinnacle_api_macros::config;
|
||||||
|
pub use tokio;
|
||||||
pub use xkbcommon;
|
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.
|
/// This function is inserted at the top of your config through the [`config`] macro.
|
||||||
/// To that end, you can do `use pinnacle_api::prelude::*` to
|
/// You should use that macro instead of this function directly.
|
||||||
/// prevent your config file from being cluttered with imports.
|
pub async fn connect(
|
||||||
pub mod prelude {
|
) -> Result<(ApiModules, UnboundedReceiver<BoxFuture<'static, ()>>), Box<dyn std::error::Error>> {
|
||||||
pub use crate::input::libinput::*;
|
let channel = Endpoint::try_from("http://[::]:50051")? // port doesn't matter, we use a unix socket
|
||||||
pub use crate::input::Modifier;
|
.connect_with_connector(service_fn(|_: Uri| {
|
||||||
pub use crate::input::MouseButton;
|
tokio::net::UnixStream::connect(
|
||||||
pub use crate::input::MouseEdge;
|
std::env::var("PINNACLE_GRPC_SOCKET")
|
||||||
pub use crate::output::AlignmentHorizontal;
|
.expect("PINNACLE_GRPC_SOCKET was not set; is Pinnacle running?"),
|
||||||
pub use crate::output::AlignmentVertical;
|
)
|
||||||
pub use crate::tag::Layout;
|
}))
|
||||||
pub use crate::window::rules::WindowRule;
|
.await?;
|
||||||
pub use crate::window::rules::WindowRuleCondition;
|
|
||||||
pub use crate::window::FloatingOrTiled;
|
|
||||||
pub use crate::window::FullscreenOrMaximized;
|
|
||||||
}
|
|
||||||
|
|
||||||
use std::{
|
let (fut_sender, fut_recv) = futures::channel::mpsc::unbounded::<BoxFuture<()>>();
|
||||||
collections::{hash_map::Entry, HashMap},
|
|
||||||
convert::Infallible,
|
|
||||||
io::{Read, Write},
|
|
||||||
os::unix::net::UnixStream,
|
|
||||||
path::PathBuf,
|
|
||||||
sync::{atomic::AtomicU32, Mutex, OnceLock},
|
|
||||||
};
|
|
||||||
|
|
||||||
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();
|
let modules = ApiModules {
|
||||||
lazy_static::lazy_static! {
|
pinnacle,
|
||||||
static ref UNREAD_CALLBACK_MSGS: Mutex<HashMap<CallbackId, IncomingMsg>> = Mutex::new(HashMap::new());
|
process,
|
||||||
static ref UNREAD_REQUEST_MSGS: Mutex<HashMap<RequestId, IncomingMsg>> = Mutex::new(HashMap::new());
|
window,
|
||||||
}
|
input,
|
||||||
|
output,
|
||||||
static REQUEST_ID_COUNTER: AtomicU32 = AtomicU32::new(0);
|
tag,
|
||||||
|
|
||||||
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!()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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`,
|
/// This will run all futures returned by configuration methods that take in callbacks in order to
|
||||||
/// which should be set when you start the compositor.
|
/// call them.
|
||||||
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 needs to be called at the very end of your `setup` function.
|
/// This function is inserted at the end of your config through the [`config`] macro.
|
||||||
pub fn listen(mut callback_vec: CallbackVec) -> Infallible {
|
/// You should use the macro instead of this function directly.
|
||||||
loop {
|
pub async fn listen(fut_recv: UnboundedReceiver<BoxFuture<'static, ()>>) {
|
||||||
let mut unread_callback_msgs = UNREAD_CALLBACK_MSGS.lock().unwrap();
|
let mut future_set = FuturesUnordered::<
|
||||||
|
BoxFuture<(
|
||||||
|
Option<BoxFuture<()>>,
|
||||||
|
Option<UnboundedReceiver<BoxFuture<()>>>,
|
||||||
|
)>,
|
||||||
|
>::new();
|
||||||
|
|
||||||
for cb_id in unread_callback_msgs.keys().copied().collect::<Vec<_>>() {
|
future_set.push(Box::pin(async move {
|
||||||
let Entry::Occupied(entry) = unread_callback_msgs.entry(cb_id) else {
|
let (fut, stream) = fut_recv.into_future().await;
|
||||||
unreachable!();
|
(fut, Some(stream))
|
||||||
};
|
}));
|
||||||
let IncomingMsg::CallCallback { callback_id, args } = entry.remove() else {
|
|
||||||
unreachable!();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Take the callback out and replace it with a dummy callback
|
while let Some((fut, stream)) = future_set.next().await {
|
||||||
// to allow callback_vec to be used mutably below.
|
if let Some(fut) = fut {
|
||||||
let mut callback = std::mem::replace(
|
future_set.push(Box::pin(async move {
|
||||||
&mut callback_vec.callbacks[callback_id.0 as usize],
|
fut.await;
|
||||||
Box::new(|_, _| {}),
|
(None, None)
|
||||||
);
|
}));
|
||||||
|
}
|
||||||
callback(args, &mut callback_vec);
|
if let Some(stream) = stream {
|
||||||
|
future_set.push(Box::pin(async move {
|
||||||
// Put it back.
|
let (fut, stream) = stream.into_future().await;
|
||||||
callback_vec.callbacks[callback_id.0 as usize] = callback;
|
(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.
|
//! 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::{
|
use futures::{
|
||||||
msg::{Args, CallbackId, Msg, Request, RequestResponse},
|
channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture, FutureExt, StreamExt,
|
||||||
request, send_msg,
|
|
||||||
tag::TagHandle,
|
|
||||||
CallbackVec,
|
|
||||||
};
|
};
|
||||||
|
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.
|
/// See [`OutputHandle`] for more information.
|
||||||
#[derive(Debug, Hash, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct OutputName(pub String);
|
pub struct Output {
|
||||||
|
channel: Channel,
|
||||||
/// Get an [`OutputHandle`] by its name.
|
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
|
||||||
///
|
|
||||||
/// `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)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a handle to all connected outputs.
|
impl Output {
|
||||||
pub fn get_all() -> impl Iterator<Item = OutputHandle> {
|
pub(crate) fn new(
|
||||||
let RequestResponse::Outputs { output_names } = request(Request::GetOutputs) else {
|
channel: Channel,
|
||||||
unreachable!()
|
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
|
||||||
};
|
) -> Self {
|
||||||
|
Self {
|
||||||
output_names
|
channel,
|
||||||
.into_iter()
|
fut_sender,
|
||||||
.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);
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
let len = callback_vec.callbacks.len();
|
fn create_output_client(&self) -> OutputServiceClient<Channel> {
|
||||||
callback_vec.callbacks.push(Box::new(args_callback));
|
OutputServiceClient::new(self.channel.clone())
|
||||||
|
}
|
||||||
|
|
||||||
let msg = Msg::ConnectForAllOutputs {
|
fn create_tag_client(&self) -> TagServiceClient<Channel> {
|
||||||
callback_id: CallbackId(len as u32),
|
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.
|
/// This allows you to manipulate outputs and get their properties.
|
||||||
/// It serves to make it easier to deal with them, defining methods for getting properties and
|
#[derive(Clone, Debug)]
|
||||||
/// helpers for things like positioning multiple monitors.
|
pub struct OutputHandle {
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
pub(crate) client: OutputServiceClient<Channel>,
|
||||||
pub struct OutputHandle(pub(crate) OutputName);
|
pub(crate) tag_client: TagServiceClient<Channel>,
|
||||||
|
pub(crate) name: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// Properties of an output.
|
impl PartialEq for OutputHandle {
|
||||||
pub struct OutputProperties {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
/// The make.
|
self.name == other.name
|
||||||
pub make: Option<String>,
|
}
|
||||||
/// The model.
|
}
|
||||||
///
|
|
||||||
/// This is something like `27GL850` or whatever gibberish monitor manufacturers name their
|
impl Eq for OutputHandle {}
|
||||||
/// displays.
|
|
||||||
pub model: Option<String>,
|
impl std::hash::Hash for OutputHandle {
|
||||||
/// The location of the output in the global space.
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
pub loc: Option<(i32, i32)>,
|
self.name.hash(state);
|
||||||
/// 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.
|
/// The alignment to use for [`OutputHandle::set_loc_adj_to`].
|
||||||
///
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
/// For example, 60Hz is returned as 60000.
|
pub enum Alignment {
|
||||||
pub refresh_rate: Option<i32>,
|
/// Set above, align left borders
|
||||||
/// The physical size of the output in millimeters.
|
TopAlignLeft,
|
||||||
pub physical_size: Option<(i32, i32)>,
|
/// Set above, align centers
|
||||||
/// Whether or not the output is focused.
|
TopAlignCenter,
|
||||||
pub focused: Option<bool>,
|
/// Set above, align right borders
|
||||||
/// The tags on this output.
|
TopAlignRight,
|
||||||
pub tags: Vec<TagHandle>,
|
/// 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 {
|
impl OutputHandle {
|
||||||
/// Get this output's name.
|
/// Set the location of this output in the global space.
|
||||||
pub fn name(&self) -> String {
|
///
|
||||||
self.0 .0.clone()
|
/// 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
|
/// Set this output adjacent to another one.
|
||||||
/// Get all properties of this output.
|
///
|
||||||
pub fn properties(&self) -> OutputProperties {
|
/// This is a helper method over [`OutputHandle::set_location`] to make laying out outputs
|
||||||
let RequestResponse::OutputProps {
|
/// easier.
|
||||||
make,
|
///
|
||||||
model,
|
/// `alignment` is an [`Alignment`] of how you want this output to be placed.
|
||||||
loc,
|
/// For example, [`TopAlignLeft`][Alignment::TopAlignLeft] will place this output
|
||||||
res,
|
/// above `other` and align the left borders.
|
||||||
refresh_rate,
|
/// Similarly, [`RightAlignCenter`][Alignment::RightAlignCenter] will place this output
|
||||||
physical_size,
|
/// to the right of `other` and align their centers.
|
||||||
focused,
|
///
|
||||||
tag_ids,
|
/// # Examples
|
||||||
} = request(Request::GetOutputProps {
|
///
|
||||||
output_name: self.0 .0.clone(),
|
/// ```
|
||||||
})
|
/// use pinnacle_api::output::Alignment;
|
||||||
else {
|
///
|
||||||
unreachable!()
|
/// // 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 {
|
OutputProperties {
|
||||||
make,
|
make: response.make,
|
||||||
model,
|
model: response.model,
|
||||||
loc,
|
x: response.x,
|
||||||
res,
|
y: response.y,
|
||||||
refresh_rate,
|
pixel_width: response.pixel_width,
|
||||||
physical_size,
|
pixel_height: response.pixel_height,
|
||||||
focused,
|
refresh_rate: response.refresh_rate,
|
||||||
tags: tag_ids
|
physical_width: response.physical_width,
|
||||||
.unwrap_or(vec![])
|
physical_height: response.physical_height,
|
||||||
|
focused: response.focused,
|
||||||
|
tags: response
|
||||||
|
.tag_ids
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(TagHandle)
|
.map(|id| TagHandle {
|
||||||
|
client: self.tag_client.clone(),
|
||||||
|
output_client: self.client.clone(),
|
||||||
|
id,
|
||||||
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add tags with the given `names` to this output.
|
// TODO: make a macro for the following or something
|
||||||
pub fn add_tags(&self, names: &[&str]) {
|
|
||||||
crate::tag::add(self, names);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set this output's location in the global space.
|
/// Get this output's make.
|
||||||
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`.
|
|
||||||
///
|
///
|
||||||
/// It will be aligned vertically based on the given `alignment`.
|
/// Shorthand for `self.props().make`.
|
||||||
pub fn set_loc_right_of(&self, other: &OutputHandle, alignment: AlignmentVertical) {
|
pub fn make(&self) -> Option<String> {
|
||||||
self.set_loc_horizontal(other, LeftOrRight::Right, alignment);
|
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`.
|
/// Shorthand for `self.props().make`.
|
||||||
pub fn set_loc_left_of(&self, other: &OutputHandle, alignment: AlignmentVertical) {
|
pub fn model(&self) -> Option<String> {
|
||||||
self.set_loc_horizontal(other, LeftOrRight::Left, alignment);
|
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`.
|
/// Shorthand for `self.props().x`.
|
||||||
pub fn set_loc_top_of(&self, other: &OutputHandle, alignment: AlignmentHorizontal) {
|
pub fn x(&self) -> Option<i32> {
|
||||||
self.set_loc_vertical(other, TopOrBottom::Top, alignment);
|
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`.
|
/// Shorthand for `self.props().y`.
|
||||||
pub fn set_loc_bottom_of(&self, other: &OutputHandle, alignment: AlignmentHorizontal) {
|
pub fn y(&self) -> Option<i32> {
|
||||||
self.set_loc_vertical(other, TopOrBottom::Bottom, alignment);
|
self.props().y
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_loc_horizontal(
|
/// Get this output's screen width in pixels.
|
||||||
&self,
|
///
|
||||||
other: &OutputHandle,
|
/// Shorthand for `self.props().pixel_width`.
|
||||||
left_or_right: LeftOrRight,
|
pub fn pixel_width(&self) -> Option<u32> {
|
||||||
alignment: AlignmentVertical,
|
self.props().pixel_width
|
||||||
) {
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_loc_vertical(
|
/// Get this output's screen height in pixels.
|
||||||
&self,
|
///
|
||||||
other: &OutputHandle,
|
/// Shorthand for `self.props().pixel_height`.
|
||||||
top_or_bottom: TopOrBottom,
|
pub fn pixel_height(&self) -> Option<u32> {
|
||||||
alignment: AlignmentHorizontal,
|
self.props().pixel_height
|
||||||
) {
|
}
|
||||||
let op1_props = self.properties();
|
|
||||||
let op2_props = other.properties();
|
|
||||||
|
|
||||||
let (Some(_self_loc), Some(self_res), Some(other_loc), Some(other_res)) =
|
/// Get this output's refresh rate in millihertz.
|
||||||
(op1_props.loc, op1_props.res, op2_props.loc, op2_props.res)
|
///
|
||||||
else {
|
/// For example, 144Hz will be returned as 144000.
|
||||||
return;
|
///
|
||||||
};
|
/// Shorthand for `self.props().refresh_rate`.
|
||||||
|
pub fn refresh_rate(&self) -> Option<u32> {
|
||||||
|
self.props().refresh_rate
|
||||||
|
}
|
||||||
|
|
||||||
let y = match top_or_bottom {
|
/// Get this output's physical width in millimeters.
|
||||||
TopOrBottom::Top => other_loc.1 - self_res.1,
|
///
|
||||||
TopOrBottom::Bottom => other_loc.1 + other_res.1,
|
/// Shorthand for `self.props().physical_width`.
|
||||||
};
|
pub fn physical_width(&self) -> Option<u32> {
|
||||||
|
self.props().physical_width
|
||||||
|
}
|
||||||
|
|
||||||
let x = match alignment {
|
/// Get this output's physical height in millimeters.
|
||||||
AlignmentHorizontal::Left => other_loc.0,
|
///
|
||||||
AlignmentHorizontal::Center => other_loc.0 + (other_res.0 - self_res.0) / 2,
|
/// Shorthand for `self.props().physical_height`.
|
||||||
AlignmentHorizontal::Right => other_loc.0 + (other_res.0 - self_res.0),
|
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 {
|
/// The properties of an output.
|
||||||
Top,
|
#[derive(Clone, Debug)]
|
||||||
Bottom,
|
pub struct OutputProperties {
|
||||||
}
|
/// The make of the output
|
||||||
|
pub make: Option<String>,
|
||||||
enum LeftOrRight {
|
/// The model of the output
|
||||||
Left,
|
///
|
||||||
Right,
|
/// This is something like "27GL83A" or whatever crap monitor manufacturers name their monitors
|
||||||
}
|
/// these days.
|
||||||
|
pub model: Option<String>,
|
||||||
/// Horizontal alignment.
|
/// The x position of the output in the global space
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
pub x: Option<i32>,
|
||||||
pub enum AlignmentHorizontal {
|
/// The y position of the output in the global space
|
||||||
/// Align the outputs such that the left edges are in line.
|
pub y: Option<i32>,
|
||||||
Left,
|
/// The output's screen width in pixels
|
||||||
/// Center the outputs horizontally.
|
pub pixel_width: Option<u32>,
|
||||||
Center,
|
/// The output's screen height in pixels
|
||||||
/// Align the outputs such that the right edges are in line.
|
pub pixel_height: Option<u32>,
|
||||||
Right,
|
/// The output's refresh rate in millihertz
|
||||||
}
|
pub refresh_rate: Option<u32>,
|
||||||
|
/// The output's physical width in millimeters
|
||||||
/// Vertical alignment.
|
pub physical_width: Option<u32>,
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
/// The output's physical height in millimeters
|
||||||
pub enum AlignmentVertical {
|
pub physical_height: Option<u32>,
|
||||||
/// Align the outputs such that the top edges are in line.
|
/// Whether this output is focused or not
|
||||||
Top,
|
///
|
||||||
/// Center the outputs vertically.
|
/// This is currently implemented as the output with the most recent pointer motion.
|
||||||
Center,
|
pub focused: Option<bool>,
|
||||||
/// Align the outputs such that the bottom edges are in line.
|
/// The tags this output has
|
||||||
Bottom,
|
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.
|
//! Process management.
|
||||||
|
//!
|
||||||
|
//! This module provides [`Process`], which allows you to spawn processes and set environment
|
||||||
|
//! variables.
|
||||||
|
|
||||||
use crate::{
|
use futures::{
|
||||||
msg::{Args, CallbackId, Msg},
|
channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture, FutureExt, StreamExt,
|
||||||
send_msg, CallbackVec,
|
|
||||||
};
|
};
|
||||||
|
use pinnacle_api_defs::pinnacle::process::v0alpha1::{
|
||||||
|
process_service_client::ProcessServiceClient, SetEnvRequest, SpawnRequest,
|
||||||
|
};
|
||||||
|
use tonic::transport::Channel;
|
||||||
|
|
||||||
/// Spawn a process.
|
/// A struct containing methods to spawn processes with optional callbacks and set environment
|
||||||
///
|
/// variables.
|
||||||
/// This will use Rust's (more specifically `async_process`'s) `Command` to spawn the provided
|
#[derive(Debug, Clone)]
|
||||||
/// arguments. If you are using any shell syntax like `~`, you may need to spawn a shell
|
pub struct Process {
|
||||||
/// instead. If so, you may *also* need to correctly escape the input.
|
channel: Channel,
|
||||||
pub fn spawn(command: Vec<&str>) -> anyhow::Result<()> {
|
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
|
||||||
let msg = Msg::Spawn {
|
|
||||||
command: command.into_iter().map(|s| s.to_string()).collect(),
|
|
||||||
callback_id: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
send_msg(msg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawn a process only if it isn't already running.
|
/// Optional callbacks to be run when a spawned process prints to stdout or stderr or exits.
|
||||||
///
|
pub struct SpawnCallbacks {
|
||||||
/// This will use Rust's (more specifically `async_process`'s) `Command` to spawn the provided
|
/// A callback that will be run when a process prints to stdout with a line
|
||||||
/// arguments. If you are using any shell syntax like `~`, you may need to spawn a shell
|
pub stdout: Option<Box<dyn FnMut(String) + Send>>,
|
||||||
/// instead. If so, you may *also* need to correctly escape the input.
|
/// A callback that will be run when a process prints to stderr with a line
|
||||||
pub fn spawn_once(command: Vec<&str>) -> anyhow::Result<()> {
|
pub stderr: Option<Box<dyn FnMut(String) + Send>>,
|
||||||
let msg = Msg::SpawnOnce {
|
/// A callback that will be run when a process exits with a status code and message
|
||||||
command: command.into_iter().map(|s| s.to_string()).collect(),
|
#[allow(clippy::type_complexity)]
|
||||||
callback_id: None,
|
pub exit: Option<Box<dyn FnMut(Option<i32>, String) + Send>>,
|
||||||
};
|
|
||||||
|
|
||||||
send_msg(msg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawn a process with an optional callback for its stdout, stderr, and exit information.
|
impl Process {
|
||||||
///
|
pub(crate) fn new(
|
||||||
/// `callback` has the following parameters:
|
channel: Channel,
|
||||||
/// - `0`: The process's stdout printed this line.
|
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
|
||||||
/// - `1`: The process's stderr printed this line.
|
) -> Process {
|
||||||
/// - `2`: The process exited with this code.
|
Self {
|
||||||
/// - `3`: The process exited with this message.
|
channel,
|
||||||
/// - `4`: A `&mut `[`CallbackVec`] for use inside the closure.
|
fut_sender,
|
||||||
///
|
|
||||||
/// 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);
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
let len = callback_vec.callbacks.len();
|
fn create_process_client(&self) -> ProcessServiceClient<Channel> {
|
||||||
callback_vec.callbacks.push(Box::new(args_callback));
|
ProcessServiceClient::new(self.channel.clone())
|
||||||
|
}
|
||||||
|
|
||||||
let msg = Msg::Spawn {
|
/// Spawn a process.
|
||||||
command: command.into_iter().map(|s| s.to_string()).collect(),
|
///
|
||||||
callback_id: Some(CallbackId(len as u32)),
|
/// 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)
|
/// Spawn a process with callbacks for its stdout, stderr, and exit information.
|
||||||
}
|
///
|
||||||
|
/// See [`SpawnCallbacks`] for the passed in struct.
|
||||||
// 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,
|
/// Note that windows spawned *before* tags are added will not be displayed.
|
||||||
/// only if it isn't already running.
|
/// This will be changed in the future to be more like Awesome, where windows with no tags are
|
||||||
///
|
/// displayed on every tag instead.
|
||||||
/// `callback` has the following parameters:
|
///
|
||||||
/// - `0`: The process's stdout printed this line.
|
/// # Examples
|
||||||
/// - `1`: The process's stderr printed this line.
|
///
|
||||||
/// - `2`: The process exited with this code.
|
/// ```
|
||||||
/// - `3`: The process exited with this message.
|
/// use pinnacle_api::process::SpawnCallbacks;
|
||||||
/// - `4`: A `&mut `[`CallbackVec`] for use inside the closure.
|
///
|
||||||
///
|
/// process.spawn_with_callbacks(["alacritty"], SpawnCallbacks {
|
||||||
/// You must also pass in a mutable reference to a [`CallbackVec`] in order to store your callback.
|
/// stdout: Some(Box::new(|line| println!("stdout: {line}"))),
|
||||||
pub fn spawn_once_with_callback<'a, F>(
|
/// stderr: Some(Box::new(|line| println!("stderr: {line}"))),
|
||||||
command: Vec<&str>,
|
/// stdout: Some(Box::new(|code, msg| println!("exit code: {code:?}, exit_msg: {msg}"))),
|
||||||
mut callback: F,
|
/// });
|
||||||
callback_vec: &mut CallbackVec<'a>,
|
/// ```
|
||||||
) -> anyhow::Result<()>
|
pub fn spawn_with_callbacks(
|
||||||
where
|
&self,
|
||||||
F: FnMut(Option<String>, Option<String>, Option<i32>, Option<String>, &mut CallbackVec) + 'a,
|
args: impl IntoIterator<Item = impl Into<String>>,
|
||||||
{
|
callbacks: SpawnCallbacks,
|
||||||
let args_callback = move |args: Option<Args>, callback_vec: &mut CallbackVec<'_>| {
|
) {
|
||||||
if let Some(Args::Spawn {
|
self.spawn_inner(args, false, Some(callbacks));
|
||||||
stdout,
|
}
|
||||||
stderr,
|
|
||||||
exit_code,
|
/// Spawn a process only if it isn't already running.
|
||||||
exit_msg,
|
///
|
||||||
}) = args
|
/// This is useful for startup programs.
|
||||||
{
|
///
|
||||||
callback(stdout, stderr, exit_code, exit_msg, callback_vec);
|
/// See [`Process::spawn`] for details.
|
||||||
}
|
pub fn spawn_once(&self, args: impl IntoIterator<Item = impl Into<String>>) {
|
||||||
};
|
self.spawn_inner(args, true, None);
|
||||||
|
}
|
||||||
let len = callback_vec.callbacks.len();
|
|
||||||
callback_vec.callbacks.push(Box::new(args_callback));
|
/// Spawn a process only if it isn't already running with optional callbacks for its stdout,
|
||||||
|
/// stderr, and exit information.
|
||||||
let msg = Msg::SpawnOnce {
|
///
|
||||||
command: command.into_iter().map(|s| s.to_string()).collect(),
|
/// This is useful for startup programs.
|
||||||
callback_id: Some(CallbackId(len as u32)),
|
///
|
||||||
};
|
/// See [`Process::spawn_with_callbacks`] for details.
|
||||||
|
pub fn spawn_once_with_callbacks(
|
||||||
send_msg(msg)
|
&self,
|
||||||
}
|
args: impl IntoIterator<Item = impl Into<String>>,
|
||||||
|
callbacks: SpawnCallbacks,
|
||||||
/// Set an environment variable for Pinnacle. All future processes spawned will have this env set.
|
) {
|
||||||
///
|
self.spawn_inner(args, true, Some(callbacks));
|
||||||
/// 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`].
|
fn spawn_inner(
|
||||||
pub fn set_env(key: &str, value: &str) {
|
&self,
|
||||||
let msg = Msg::SetEnv {
|
args: impl IntoIterator<Item = impl Into<String>>,
|
||||||
key: key.to_string(),
|
once: bool,
|
||||||
value: value.to_string(),
|
callbacks: Option<SpawnCallbacks>,
|
||||||
};
|
) {
|
||||||
|
let mut client = self.create_process_client();
|
||||||
send_msg(msg).unwrap();
|
|
||||||
|
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.
|
//! 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 std::{
|
||||||
|
collections::HashMap,
|
||||||
use crate::{
|
sync::{Arc, Mutex},
|
||||||
msg::{Msg, Request, RequestResponse},
|
|
||||||
output::{OutputHandle, OutputName},
|
|
||||||
request, send_msg,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Get a tag by its name and output. If `output` is `None`, the currently focused output will
|
use futures::{channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture};
|
||||||
/// be used instead.
|
use num_enum::TryFromPrimitive;
|
||||||
///
|
use pinnacle_api_defs::pinnacle::{
|
||||||
/// If multiple tags have the same name, this returns the first one.
|
output::v0alpha1::output_service_client::OutputServiceClient,
|
||||||
pub fn get(name: &str, output: Option<&OutputHandle>) -> Option<TagHandle> {
|
tag::{
|
||||||
get_all()
|
self,
|
||||||
.filter(|tag| {
|
v0alpha1::{
|
||||||
tag.properties().output.is_some_and(|op| match output {
|
tag_service_client::TagServiceClient, AddRequest, RemoveRequest, SetActiveRequest,
|
||||||
Some(output) => &op == output,
|
SetLayoutRequest, SwitchToRequest,
|
||||||
None => Some(op) == crate::output::get_focused(),
|
},
|
||||||
})
|
},
|
||||||
|
};
|
||||||
|
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.
|
/// Get handles to all tags across all outputs.
|
||||||
pub fn get_all() -> impl Iterator<Item = TagHandle> {
|
///
|
||||||
let RequestResponse::Tags { tag_ids } = request(Request::GetTags) else {
|
/// # Examples
|
||||||
unreachable!()
|
///
|
||||||
};
|
/// ```
|
||||||
|
/// 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
|
response.tag_ids.into_iter().map(move |id| TagHandle {
|
||||||
/// Add tags with the names from `names` to `output`.
|
client: client.clone(),
|
||||||
pub fn add(output: &OutputHandle, names: &[&str]) {
|
output_client: output_client.clone(),
|
||||||
let msg = Msg::AddTags {
|
id,
|
||||||
output_name: output.0.clone(),
|
})
|
||||||
tag_names: names.iter().map(|s| s.to_string()).collect(),
|
}
|
||||||
};
|
|
||||||
|
|
||||||
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.
|
self.get_all().find(|tag| {
|
||||||
///
|
let props = tag.props();
|
||||||
/// 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.
|
let same_tag_name = props.name.as_ref() == Some(&name);
|
||||||
///
|
let same_output = props.output.is_some_and(|op| Some(op) == focused_output);
|
||||||
/// # Example
|
|
||||||
/// ```
|
same_tag_name && same_output
|
||||||
/// todo!()
|
})
|
||||||
/// ```
|
}
|
||||||
pub fn layout_cycler(layouts: &[Layout]) -> LayoutCycler {
|
|
||||||
let indices = std::rc::Rc::new(std::cell::RefCell::new(HashMap::<TagId, usize>::new()));
|
/// Get a handle to the first tag with the given name on the specified output.
|
||||||
let indices_clone = indices.clone();
|
///
|
||||||
let layouts = layouts.to_vec();
|
/// If you just need to get a tag on the focused output, see [`Tag::get`].
|
||||||
let layouts_clone = layouts.clone();
|
///
|
||||||
let len = layouts.len();
|
/// # Examples
|
||||||
let next = move |output: Option<&OutputHandle>| {
|
///
|
||||||
let Some(output) = output.cloned().or_else(crate::output::get_focused) else {
|
/// ```
|
||||||
return;
|
/// // 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
|
let prev = move |output: Option<&OutputHandle>| {
|
||||||
.properties()
|
let Some(output) = output
|
||||||
.tags
|
.cloned()
|
||||||
.into_iter()
|
.or_else(|| output_module_clone.get_focused())
|
||||||
.find(|tag| tag.properties().active == Some(true))
|
else {
|
||||||
else {
|
return;
|
||||||
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();
|
LayoutCycler {
|
||||||
let index = indices.entry(tag.0).or_insert(0);
|
prev: Box::new(prev),
|
||||||
|
next: Box::new(next),
|
||||||
if *index + 1 >= len {
|
|
||||||
*index = 0;
|
|
||||||
} else {
|
|
||||||
*index += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
/// layouts on them.
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
pub struct LayoutCycler {
|
pub struct LayoutCycler {
|
||||||
/// Cycle to the next layout on the given output, or the focused output if `None`.
|
/// 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`.
|
/// Cycle to the previous layout on the given output, or the focused output if `None`.
|
||||||
pub prev: Box<dyn FnMut(Option<&OutputHandle>)>,
|
pub next: Box<dyn Fn(Option<&OutputHandle>) + Send + Sync + 'static>,
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub(crate) enum TagId {
|
|
||||||
None,
|
|
||||||
#[serde(untagged)]
|
|
||||||
Some(u32),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A handle to a tag.
|
/// A handle to a tag.
|
||||||
pub struct TagHandle(pub(crate) TagId);
|
///
|
||||||
|
/// This handle allows you to do things like switch to tags and get their properties.
|
||||||
/// Properties of a tag, retrieved through [`TagHandle::properties`].
|
#[derive(Debug, Clone)]
|
||||||
#[derive(Debug)]
|
pub struct TagHandle {
|
||||||
pub struct TagProperties {
|
pub(crate) client: TagServiceClient<Channel>,
|
||||||
/// Whether or not the tag is active.
|
pub(crate) output_client: OutputServiceClient<Channel>,
|
||||||
pub active: Option<bool>,
|
pub(crate) id: u32,
|
||||||
/// The tag's name.
|
|
||||||
pub name: Option<String>,
|
|
||||||
/// The output the tag is on.
|
|
||||||
pub output: Option<OutputHandle>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TagHandle {
|
impl PartialEq for TagHandle {
|
||||||
/// Get this tag's [`TagProperties`].
|
fn eq(&self, other: &Self) -> bool {
|
||||||
pub fn properties(&self) -> TagProperties {
|
self.id == other.id
|
||||||
let RequestResponse::TagProps {
|
|
||||||
active,
|
|
||||||
name,
|
|
||||||
output_name,
|
|
||||||
} = request(Request::GetTagProps { tag_id: self.0 })
|
|
||||||
else {
|
|
||||||
unreachable!()
|
|
||||||
};
|
|
||||||
|
|
||||||
TagProperties {
|
|
||||||
active,
|
|
||||||
name,
|
|
||||||
output: output_name.map(|name| OutputHandle(OutputName(name))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Toggle this tag.
|
|
||||||
pub fn toggle(&self) {
|
|
||||||
let msg = Msg::ToggleTag { tag_id: self.0 };
|
|
||||||
send_msg(msg).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Switch to this tag, deactivating all others on its output.
|
|
||||||
pub fn switch_to(&self) {
|
|
||||||
let msg = Msg::SwitchToTag { tag_id: self.0 };
|
|
||||||
send_msg(msg).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set this tag's [`Layout`].
|
|
||||||
pub fn set_layout(&self, layout: Layout) {
|
|
||||||
let msg = Msg::SetLayout {
|
|
||||||
tag_id: self.0,
|
|
||||||
layout,
|
|
||||||
};
|
|
||||||
|
|
||||||
send_msg(msg).unwrap()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Layouts for tags.
|
impl Eq for TagHandle {}
|
||||||
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
|
|
||||||
|
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 {
|
pub enum Layout {
|
||||||
/// One master window on the left with all other windows stacked to the right.
|
/// One master window on the left with all other windows stacked to the right
|
||||||
MasterStack,
|
MasterStack = 1,
|
||||||
/// Windows split in half towards the bottom right corner.
|
/// Windows split in half towards the bottom right corner
|
||||||
Dwindle,
|
Dwindle,
|
||||||
/// Windows split in half in a spiral
|
/// Windows split in half in a spiral
|
||||||
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,
|
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,
|
CornerTopRight,
|
||||||
/// One main corner window in the bottom left with a column of windows on the right and a row on the top.
|
/// One main corner window in the bottom left with a column of windows on the right and a row on the top.
|
||||||
CornerBottomLeft,
|
CornerBottomLeft,
|
||||||
/// One main corner window in the bottom right with a column of windows on the left and a row on the top.
|
/// One main corner window in the bottom right with a column of windows on the left and a row on the top.
|
||||||
CornerBottomRight,
|
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.
|
//! 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;
|
pub mod rules;
|
||||||
|
|
||||||
use crate::{
|
/// A struct containing methods that get [`WindowHandle`]s and move windows with the mouse.
|
||||||
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.
|
|
||||||
///
|
///
|
||||||
/// This will start a window move grab with the provided button on the window the pointer
|
/// See [`WindowHandle`] for more information.
|
||||||
/// is currently hovering over. Once `button` is let go, the move will end.
|
#[derive(Debug, Clone)]
|
||||||
pub fn begin_move(button: MouseButton) {
|
pub struct Window {
|
||||||
let msg = Msg::WindowMoveGrab {
|
channel: Channel,
|
||||||
button: button as u32,
|
|
||||||
};
|
|
||||||
|
|
||||||
send_msg(msg).unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Begin a window resize.
|
impl Window {
|
||||||
///
|
pub(crate) fn new(channel: Channel) -> Self {
|
||||||
/// This will start a window resize grab with the provided button on the window the
|
Self { channel }
|
||||||
/// 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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.
|
/// This will begin moving the window under the pointer using the specified [`MouseButton`].
|
||||||
/// If used while fullscreen, it becomes unfullscreen.
|
/// The button must be held down at the time this method is called for the move to start.
|
||||||
/// 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.
|
|
||||||
///
|
///
|
||||||
/// If used while not maximized, it becomes maximized.
|
/// This is intended to be used with [`Input::keybind`][crate::input::Input::keybind].
|
||||||
/// If used while maximized, it becomes unmaximized.
|
///
|
||||||
/// If used while fullscreen, it becomes maximized.
|
/// # Examples
|
||||||
pub fn toggle_maximized(&self) {
|
///
|
||||||
send_msg(Msg::ToggleMaximized { window_id: self.0 }).unwrap();
|
/// ```
|
||||||
}
|
/// use pinnacle_api::input::{Mod, MouseButton, MouseEdge};
|
||||||
|
///
|
||||||
/// Set this window's size. None parameters will be ignored.
|
/// // Set `Super + left click` to begin moving a window
|
||||||
pub fn set_size(&self, width: Option<i32>, height: Option<i32>) {
|
/// input.mousebind([Mod::Super], MouseButton::Left, MouseEdge::Press, || {
|
||||||
send_msg(Msg::SetWindowSize {
|
/// window.begin_move(MouseButton::Left);
|
||||||
window_id: self.0,
|
/// });
|
||||||
width,
|
/// ```
|
||||||
height,
|
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();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a close event to this window.
|
/// Start resizing the window with the mouse.
|
||||||
pub fn close(&self) {
|
///
|
||||||
send_msg(Msg::CloseWindow { window_id: self.0 }).unwrap();
|
/// 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`].
|
/// Get all windows.
|
||||||
pub fn properties(&self) -> WindowProperties {
|
///
|
||||||
let RequestResponse::WindowProps {
|
/// # Examples
|
||||||
size,
|
///
|
||||||
loc,
|
/// ```
|
||||||
class,
|
/// let windows = window.get_all();
|
||||||
title,
|
/// ```
|
||||||
focused,
|
pub fn get_all(&self) -> impl Iterator<Item = WindowHandle> {
|
||||||
floating,
|
let mut client = self.create_window_client();
|
||||||
fullscreen_or_maximized,
|
let tag_client = self.create_tag_client();
|
||||||
} = request(Request::GetWindowProps { window_id: self.0 })
|
let output_client = self.create_output_client();
|
||||||
else {
|
block_on(client.get(GetRequest {}))
|
||||||
unreachable!()
|
.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 {
|
WindowProperties {
|
||||||
size,
|
geometry,
|
||||||
loc,
|
class: response.class,
|
||||||
class,
|
title: response.title,
|
||||||
title,
|
focused: response.focused,
|
||||||
focused,
|
floating: response.floating,
|
||||||
floating,
|
|
||||||
fullscreen_or_maximized,
|
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.
|
/// Get this window's location and size.
|
||||||
pub fn toggle_tag(&self, tag: &TagHandle) {
|
///
|
||||||
let msg = Msg::ToggleTagOnWindow {
|
/// Shorthand for `self.props().geometry`.
|
||||||
window_id: self.0,
|
pub fn geometry(&self) -> Option<Geometry> {
|
||||||
tag_id: tag.0,
|
self.props().geometry
|
||||||
};
|
|
||||||
|
|
||||||
send_msg(msg).unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Move this window to `tag`.
|
/// Get this window's class.
|
||||||
///
|
///
|
||||||
/// This will remove all other tags on this window.
|
/// Shorthand for `self.props().class`.
|
||||||
pub fn move_to_tag(&self, tag: &TagHandle) {
|
pub fn class(&self) -> Option<String> {
|
||||||
let msg = Msg::MoveWindowToTag {
|
self.props().class
|
||||||
window_id: self.0,
|
}
|
||||||
tag_id: tag.0,
|
|
||||||
};
|
|
||||||
|
|
||||||
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};
|
use super::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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A condition for a [`WindowRule`] to apply to a window.
|
/// 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 {
|
impl WindowRuleCondition {
|
||||||
/// Create a new, empty `WindowRuleCondition`.
|
/// Create a new, empty `WindowRuleCondition`.
|
||||||
|
@ -86,14 +225,41 @@ impl WindowRuleCondition {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This condition requires that at least one provided condition is true.
|
/// 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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This condition requires that all provided conditions are true.
|
/// 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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,8 +270,26 @@ impl WindowRuleCondition {
|
||||||
///
|
///
|
||||||
/// When used in [`WindowRuleCondition::any`], at least one of the
|
/// When used in [`WindowRuleCondition::any`], at least one of the
|
||||||
/// provided classes must match.
|
/// 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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,8 +300,26 @@ impl WindowRuleCondition {
|
||||||
///
|
///
|
||||||
/// When used in [`WindowRuleCondition::any`], at least one of the
|
/// When used in [`WindowRuleCondition::any`], at least one of the
|
||||||
/// provided titles must match.
|
/// 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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,8 +330,192 @@ impl WindowRuleCondition {
|
||||||
///
|
///
|
||||||
/// When used in [`WindowRuleCondition::any`], the window must open on at least
|
/// When used in [`WindowRuleCondition::any`], the window must open on at least
|
||||||
/// one of the given tags.
|
/// 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
|
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::{
|
use crate::{
|
||||||
api::msg::{Args, OutgoingMsg},
|
|
||||||
backend::Backend,
|
backend::Backend,
|
||||||
config::ConnectorSavedState,
|
config::ConnectorSavedState,
|
||||||
output::OutputName,
|
output::OutputName,
|
||||||
|
@ -985,36 +984,11 @@ impl State {
|
||||||
output.with_state(|state| state.tags = tags.clone());
|
output.with_state(|state| state.tags = tags.clone());
|
||||||
} else {
|
} else {
|
||||||
// Run any output callbacks
|
// Run any output callbacks
|
||||||
let clone = output.clone();
|
for sender in self.config.output_callback_senders.iter() {
|
||||||
self.schedule(
|
let _ = sender.send(Ok(ConnectForAllResponse {
|
||||||
|dt| dt.state.api_state.stream.is_some(),
|
output_name: Some(output.name()),
|
||||||
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()),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{
|
api::{
|
||||||
msg::ModifierMask,
|
InputService, OutputService, PinnacleService, ProcessService, TagService, WindowService,
|
||||||
protocol::{
|
|
||||||
InputService, OutputService, PinnacleService, ProcessService, TagService, WindowService,
|
|
||||||
},
|
|
||||||
PinnacleSocketSource,
|
|
||||||
},
|
},
|
||||||
|
input::ModifierMask,
|
||||||
output::OutputName,
|
output::OutputName,
|
||||||
tag::Tag,
|
tag::Tag,
|
||||||
window::rules::{WindowRule, WindowRuleCondition},
|
window::rules::{WindowRule, WindowRuleCondition},
|
||||||
|
@ -14,7 +11,6 @@ use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process::Stdio,
|
process::Stdio,
|
||||||
sync::{Arc, Mutex},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
@ -32,9 +28,9 @@ use smithay::{
|
||||||
utils::{Logical, Point},
|
utils::{Logical, Point},
|
||||||
};
|
};
|
||||||
use sysinfo::ProcessRefreshKind;
|
use sysinfo::ProcessRefreshKind;
|
||||||
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
use toml::Table;
|
use toml::Table;
|
||||||
|
|
||||||
use crate::api::msg::{CallbackId, Modifier};
|
|
||||||
use xkbcommon::xkb::Keysym;
|
use xkbcommon::xkb::Keysym;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -57,8 +53,34 @@ pub struct Metaconfig {
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Debug)]
|
#[derive(serde::Deserialize, Debug)]
|
||||||
pub struct Keybind {
|
pub struct Keybind {
|
||||||
pub modifiers: Vec<Modifier>,
|
modifiers: Vec<Modifier>,
|
||||||
pub key: Key,
|
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
|
// TODO: accept xkbcommon names instead
|
||||||
|
@ -141,10 +163,7 @@ pub enum Key {
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
/// Window rules and conditions on when those rules should apply
|
/// Window rules and conditions on when those rules should apply
|
||||||
pub window_rules: Vec<(WindowRuleCondition, WindowRule)>,
|
pub window_rules: Vec<(WindowRuleCondition, WindowRule)>,
|
||||||
/// All callbacks that should be run when outputs are connected
|
pub output_callback_senders: Vec<UnboundedSender<Result<ConnectForAllResponse, tonic::Status>>>,
|
||||||
pub output_callback_ids: Vec<CallbackId>,
|
|
||||||
pub grpc_output_callback_senders:
|
|
||||||
Vec<tokio::sync::mpsc::UnboundedSender<Result<ConnectForAllResponse, tonic::Status>>>,
|
|
||||||
/// Saved states when outputs are disconnected
|
/// Saved states when outputs are disconnected
|
||||||
pub connector_saved_states: HashMap<OutputName, ConnectorSavedState>,
|
pub connector_saved_states: HashMap<OutputName, ConnectorSavedState>,
|
||||||
}
|
}
|
||||||
|
@ -214,13 +233,6 @@ impl State {
|
||||||
config_join_handle.abort();
|
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
|
// Love that trailing slash
|
||||||
let data_home = PathBuf::from(
|
let data_home = PathBuf::from(
|
||||||
crate::XDG_BASE_DIRS
|
crate::XDG_BASE_DIRS
|
||||||
|
@ -255,19 +267,6 @@ impl State {
|
||||||
|
|
||||||
self.start_grpc_server(socket_dir.as_path())?;
|
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 reload_keybind = metaconfig.reload_keybind;
|
||||||
let kill_keybind = metaconfig.kill_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 reload_keybind = (reload_mask, Keysym::from(reload_keybind.key as u32));
|
||||||
let kill_keybind = (kill_mask, Keysym::from(kill_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.reload_keybind = Some(reload_keybind);
|
||||||
self.input_state.kill_keybind = Some(kill_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 {
|
self.config_join_handle = Some(tokio::spawn(async move {
|
||||||
let _ = child.wait().await;
|
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 std::{collections::HashMap, mem::Discriminant};
|
||||||
|
|
||||||
use crate::{
|
use crate::{focus::FocusTarget, state::WithState, window::WindowElement};
|
||||||
api::msg::{CallbackId, Modifier, MouseEdge, OutgoingMsg},
|
|
||||||
focus::FocusTarget,
|
|
||||||
state::WithState,
|
|
||||||
window::WindowElement,
|
|
||||||
};
|
|
||||||
use pinnacle_api_defs::pinnacle::input::v0alpha1::{
|
use pinnacle_api_defs::pinnacle::input::v0alpha1::{
|
||||||
set_libinput_setting_request::Setting, set_mousebind_request, SetKeybindResponse,
|
set_libinput_setting_request::Setting, set_mousebind_request, SetKeybindResponse,
|
||||||
SetMousebindResponse,
|
SetMousebindResponse,
|
||||||
|
@ -33,8 +28,6 @@ use xkbcommon::xkb::Keysym;
|
||||||
|
|
||||||
use crate::state::State;
|
use crate::state::State;
|
||||||
|
|
||||||
use self::libinput::LibinputSetting;
|
|
||||||
|
|
||||||
bitflags::bitflags! {
|
bitflags::bitflags! {
|
||||||
#[derive(Debug, Hash, Copy, Clone, PartialEq, Eq)]
|
#[derive(Debug, Hash, Copy, Clone, PartialEq, Eq)]
|
||||||
pub struct ModifierMask: u8 {
|
pub struct ModifierMask: u8 {
|
||||||
|
@ -85,39 +78,30 @@ impl From<&ModifiersState> for ModifierMask {
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct InputState {
|
pub struct InputState {
|
||||||
/// A hashmap of modifier keys and keycodes to callback IDs
|
pub reload_keybind: Option<(ModifierMask, Keysym)>,
|
||||||
pub keybinds: HashMap<(crate::api::msg::ModifierMask, Keysym), CallbackId>,
|
pub kill_keybind: Option<(ModifierMask, Keysym)>,
|
||||||
/// 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>,
|
|
||||||
/// All libinput devices that have been connected
|
/// All libinput devices that have been connected
|
||||||
pub libinput_devices: Vec<input::Device>,
|
pub libinput_devices: Vec<input::Device>,
|
||||||
|
|
||||||
pub grpc_keybinds:
|
pub keybinds:
|
||||||
HashMap<(ModifierMask, Keysym), UnboundedSender<Result<SetKeybindResponse, tonic::Status>>>,
|
HashMap<(ModifierMask, Keysym), UnboundedSender<Result<SetKeybindResponse, tonic::Status>>>,
|
||||||
pub grpc_mousebinds: HashMap<
|
pub mousebinds: HashMap<
|
||||||
(ModifierMask, u32, set_mousebind_request::MouseEdge),
|
(ModifierMask, u32, set_mousebind_request::MouseEdge),
|
||||||
UnboundedSender<Result<SetMousebindResponse, tonic::Status>>,
|
UnboundedSender<Result<SetMousebindResponse, tonic::Status>>,
|
||||||
>,
|
>,
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
pub grpc_libinput_settings:
|
pub libinput_settings: HashMap<Discriminant<Setting>, Box<dyn Fn(&mut input::Device) + Send>>,
|
||||||
HashMap<Discriminant<Setting>, Box<dyn Fn(&mut input::Device) + Send>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for InputState {
|
impl std::fmt::Debug for InputState {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.debug_struct("InputState")
|
f.debug_struct("InputState")
|
||||||
.field("keybinds", &self.keybinds)
|
|
||||||
.field("mousebinds", &self.mousebinds)
|
|
||||||
.field("reload_keybind", &self.reload_keybind)
|
.field("reload_keybind", &self.reload_keybind)
|
||||||
.field("kill_keybind", &self.kill_keybind)
|
.field("kill_keybind", &self.kill_keybind)
|
||||||
.field("libinput_settings", &self.libinput_settings)
|
|
||||||
.field("libinput_devices", &self.libinput_devices)
|
.field("libinput_devices", &self.libinput_devices)
|
||||||
.field("grpc_keybinds", &self.grpc_keybinds)
|
.field("keybinds", &self.keybinds)
|
||||||
.field("grpc_libinput_settings", &"...")
|
.field("mousebinds", &self.mousebinds)
|
||||||
|
.field("libinput_settings", &"...")
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -130,9 +114,7 @@ impl InputState {
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum KeyAction {
|
enum KeyAction {
|
||||||
/// Call a callback from a config process
|
CallCallback(UnboundedSender<Result<SetKeybindResponse, tonic::Status>>),
|
||||||
CallCallback(CallbackId),
|
|
||||||
CallGrpcCallback(UnboundedSender<Result<SetKeybindResponse, tonic::Status>>),
|
|
||||||
Quit,
|
Quit,
|
||||||
SwitchVt(i32),
|
SwitchVt(i32),
|
||||||
ReloadConfig,
|
ReloadConfig,
|
||||||
|
@ -257,59 +239,23 @@ impl State {
|
||||||
|state, modifiers, keysym| {
|
|state, modifiers, keysym| {
|
||||||
// tracing::debug!(keysym = ?keysym, raw_keysyms = ?keysym.raw_syms(), modified_syms = ?keysym.modified_syms());
|
// tracing::debug!(keysym = ?keysym, raw_keysyms = ?keysym.raw_syms(), modified_syms = ?keysym.modified_syms());
|
||||||
if press_state == KeyState::Pressed {
|
if press_state == KeyState::Pressed {
|
||||||
let mut modifier_mask = Vec::<Modifier>::new();
|
let mod_mask = ModifierMask::from(modifiers);
|
||||||
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 raw_sym = keysym.raw_syms().iter().next();
|
let raw_sym = keysym.raw_syms().iter().next();
|
||||||
let mod_sym = keysym.modified_sym();
|
let mod_sym = keysym.modified_sym();
|
||||||
|
|
||||||
if let (Some(sender), _) | (None, Some(sender)) = (
|
if let (Some(sender), _) | (None, Some(sender)) = (
|
||||||
state
|
state.input_state.keybinds.get(&(mod_mask, mod_sym)),
|
||||||
.input_state
|
|
||||||
.grpc_keybinds
|
|
||||||
.get(&(grpc_modifiers, mod_sym)),
|
|
||||||
raw_sym.and_then(|raw_sym| {
|
raw_sym.and_then(|raw_sym| {
|
||||||
state
|
state.input_state.keybinds.get(&(mod_mask, *raw_sym))
|
||||||
.input_state
|
|
||||||
.grpc_keybinds
|
|
||||||
.get(&(grpc_modifiers, *raw_sym))
|
|
||||||
}),
|
}),
|
||||||
) {
|
) {
|
||||||
return FilterResult::Intercept(KeyAction::CallGrpcCallback(
|
return FilterResult::Intercept(KeyAction::CallCallback(sender.clone()));
|
||||||
sender.clone(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let cb_id_mod = state.input_state.keybinds.get(&(modifier_mask, mod_sym));
|
if kill_keybind == Some((mod_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)) {
|
|
||||||
return FilterResult::Intercept(KeyAction::Quit);
|
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);
|
return FilterResult::Intercept(KeyAction::ReloadConfig);
|
||||||
} else if let mut vt @ keysyms::KEY_XF86Switch_VT_1
|
} else if let mut vt @ keysyms::KEY_XF86Switch_VT_1
|
||||||
..=keysyms::KEY_XF86Switch_VT_12 = keysym.modified_sym().raw()
|
..=keysyms::KEY_XF86Switch_VT_12 = keysym.modified_sym().raw()
|
||||||
|
@ -325,20 +271,7 @@ impl State {
|
||||||
);
|
);
|
||||||
|
|
||||||
match action {
|
match action {
|
||||||
Some(KeyAction::CallCallback(callback_id)) => {
|
Some(KeyAction::CallCallback(sender)) => {
|
||||||
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)) => {
|
|
||||||
let _ = sender.send(Ok(SetKeybindResponse {}));
|
let _ = sender.send(Ok(SetKeybindResponse {}));
|
||||||
}
|
}
|
||||||
Some(KeyAction::SwitchVt(vt)) => {
|
Some(KeyAction::SwitchVt(vt)) => {
|
||||||
|
@ -367,42 +300,17 @@ impl State {
|
||||||
|
|
||||||
let pointer_loc = pointer.current_location();
|
let pointer_loc = pointer.current_location();
|
||||||
|
|
||||||
|
let mod_mask = ModifierMask::from(keyboard.modifier_state());
|
||||||
|
|
||||||
let mouse_edge = match button_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::Released => set_mousebind_request::MouseEdge::Release,
|
||||||
ButtonState::Pressed => set_mousebind_request::MouseEdge::Press,
|
ButtonState::Pressed => set_mousebind_request::MouseEdge::Press,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(stream) =
|
if let Some(stream) = self
|
||||||
self.input_state
|
.input_state
|
||||||
.grpc_mousebinds
|
.mousebinds
|
||||||
.get(&(grpc_modifier_mask, button, grpc_mouse_edge))
|
.get(&(mod_mask, button, mouse_edge))
|
||||||
{
|
{
|
||||||
let _ = stream.send(Ok(SetMousebindResponse {}));
|
let _ = stream.send(Ok(SetMousebindResponse {}));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,104 +1,7 @@
|
||||||
use smithay::{
|
use smithay::backend::{input::InputEvent, libinput::LibinputInputBackend};
|
||||||
backend::{input::InputEvent, libinput::LibinputInputBackend},
|
|
||||||
reexports::input::{self, AccelProfile, ClickMethod, ScrollMethod, TapButtonMap},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::state::State;
|
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 {
|
impl State {
|
||||||
/// Apply current libinput settings to new devices.
|
/// Apply current libinput settings to new devices.
|
||||||
pub fn apply_libinput_settings(&mut self, event: &InputEvent<LibinputInputBackend>) {
|
pub fn apply_libinput_settings(&mut self, event: &InputEvent<LibinputInputBackend>) {
|
||||||
|
@ -117,10 +20,7 @@ impl State {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for setting in self.input_state.libinput_settings.iter() {
|
for setting in self.input_state.libinput_settings.values() {
|
||||||
setting.apply_to_device(&mut device);
|
|
||||||
}
|
|
||||||
for setting in self.input_state.grpc_libinput_settings.values() {
|
|
||||||
setting(&mut device);
|
setting(&mut device);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
//! While Pinnacle is not a library, this documentation serves to guide those who want to
|
//! While Pinnacle is not a library, this documentation serves to guide those who want to
|
||||||
//! contribute or learn how building something like this works.
|
//! 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)]
|
#![warn(clippy::unwrap_used)]
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
33
src/state.rs
33
src/state.rs
|
@ -1,22 +1,14 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{msg::Msg, ApiState},
|
backend::Backend, config::Config, cursor::Cursor, focus::FocusState,
|
||||||
backend::Backend,
|
grab::resize_grab::ResizeSurfaceState, window::WindowElement,
|
||||||
config::Config,
|
|
||||||
cursor::Cursor,
|
|
||||||
focus::FocusState,
|
|
||||||
grab::resize_grab::ResizeSurfaceState,
|
|
||||||
window::WindowElement,
|
|
||||||
};
|
};
|
||||||
use smithay::{
|
use smithay::{
|
||||||
desktop::{PopupManager, Space},
|
desktop::{PopupManager, Space},
|
||||||
input::{keyboard::XkbConfig, pointer::CursorImageStatus, Seat, SeatState},
|
input::{keyboard::XkbConfig, pointer::CursorImageStatus, Seat, SeatState},
|
||||||
reexports::{
|
reexports::{
|
||||||
calloop::{
|
calloop::{generic::Generic, Interest, LoopHandle, LoopSignal, Mode, PostAction},
|
||||||
self, channel::Event, generic::Generic, Interest, LoopHandle, LoopSignal, Mode,
|
|
||||||
PostAction,
|
|
||||||
},
|
|
||||||
wayland_server::{
|
wayland_server::{
|
||||||
backend::{ClientData, ClientId, DisconnectReason},
|
backend::{ClientData, ClientId, DisconnectReason},
|
||||||
protocol::wl_surface::WlSurface,
|
protocol::wl_surface::WlSurface,
|
||||||
|
@ -74,8 +66,6 @@ pub struct State {
|
||||||
|
|
||||||
/// The state of key and mousebinds along with libinput settings
|
/// The state of key and mousebinds along with libinput settings
|
||||||
pub input_state: InputState,
|
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
|
/// Keeps track of the focus stack and focused output
|
||||||
pub focus_state: FocusState,
|
pub focus_state: FocusState,
|
||||||
|
|
||||||
|
@ -159,8 +149,6 @@ impl State {
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let (tx_channel, rx_channel) = calloop::channel::channel::<Msg>();
|
|
||||||
|
|
||||||
loop_handle.insert_idle(|data| {
|
loop_handle.insert_idle(|data| {
|
||||||
if let Err(err) = data.state.start_config(crate::config::get_config_dir()) {
|
if let Err(err) = data.state.start_config(crate::config::get_config_dir()) {
|
||||||
panic!("failed to start config: {err}");
|
panic!("failed to start config: {err}");
|
||||||
|
@ -174,16 +162,6 @@ impl State {
|
||||||
|
|
||||||
seat.add_keyboard(XkbConfig::default(), 500, 25)?;
|
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 = {
|
||||||
let (xwayland, channel) = XWayland::new(&display_handle);
|
let (xwayland, channel) = XWayland::new(&display_handle);
|
||||||
let clone = display_handle.clone();
|
let clone = display_handle.clone();
|
||||||
|
@ -253,11 +231,6 @@ impl State {
|
||||||
layer_shell_state: WlrLayerShellState::new::<Self>(&display_handle),
|
layer_shell_state: WlrLayerShellState::new::<Self>(&display_handle),
|
||||||
|
|
||||||
input_state: InputState::new(),
|
input_state: InputState::new(),
|
||||||
api_state: ApiState {
|
|
||||||
stream: None,
|
|
||||||
socket_token: None,
|
|
||||||
tx_channel,
|
|
||||||
},
|
|
||||||
focus_state: FocusState::new(),
|
focus_state: FocusState::new(),
|
||||||
|
|
||||||
config: Config::default(),
|
config: Config::default(),
|
||||||
|
|
Loading…
Reference in a new issue