diff --git a/.github/workflows/rustdoc.yml b/.github/workflows/rustdoc.yml
index 3216823..a02769e 100644
--- a/.github/workflows/rustdoc.yml
+++ b/.github/workflows/rustdoc.yml
@@ -22,6 +22,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
+ - name: Get protoc
+ run: sudo apt install protobuf-compiler
- name: Build docs
run: cd ./api/rust && cargo doc
- name: Create index.html
diff --git a/Cargo.lock b/Cargo.lock
index 8109427..cde96be 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1489,12 +1489,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
-[[package]]
-name = "paste"
-version = "1.0.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
-
[[package]]
name = "percent-encoding"
version = "2.3.1"
@@ -1556,8 +1550,6 @@ dependencies = [
"pinnacle-api-defs",
"prost",
"prost-types",
- "rmp",
- "rmp-serde",
"serde",
"shellexpand",
"smithay",
@@ -1868,28 +1860,6 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
-[[package]]
-name = "rmp"
-version = "0.8.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f9860a6cc38ed1da53456442089b4dfa35e7cedaa326df63017af88385e6b20"
-dependencies = [
- "byteorder",
- "num-traits",
- "paste",
-]
-
-[[package]]
-name = "rmp-serde"
-version = "1.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bffea85eea980d8a74453e5d02a8d93028f3c34725de143085a844ebe953258a"
-dependencies = [
- "byteorder",
- "rmp",
- "serde",
-]
-
[[package]]
name = "rustc-demangle"
version = "0.1.23"
diff --git a/Cargo.toml b/Cargo.toml
index c3054ca..bba02d9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,8 +19,6 @@ thiserror = "1"
xcursor = { version = "0.3", optional = true }
image = { version = "0.24", default-features = false, optional = true }
serde = { version = "1.0", features = ["derive"] }
-rmp = { version = "0.8.12" }
-rmp-serde = { version = "1.1.2" }
x11rb = { version = "0.13", default-features = false, features = ["composite"], optional = true }
shellexpand = "3.1.0"
toml = "0.8"
@@ -65,3 +63,4 @@ xwayland = ["smithay/xwayland", "x11rb", "smithay/x11rb_event_source", "xcursor"
[workspace]
members = ["pinnacle-api-defs"]
+exclude = ["api/rust_grpc", "api/rust"]
diff --git a/README.md b/README.md
index 18fd23c..66f32d2 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,9 @@ Pinnacle is a Wayland compositor built in Rust using [Smithay](https://github.co
It's my attempt at creating something like [AwesomeWM](https://github.com/awesomeWM/awesome)
for Wayland.
-It sports extensive configurability through either Lua or Rust, with the ability to add more languages in the future.
+It sports extensive configurability through either Lua or Rust, with the ability to add more languages
+in the future. And by that I mean other people can do the adding,
+ I'm already maintaining Lua and Rust lol
> ### More video examples below!
>
@@ -61,7 +63,7 @@ You will need:
- [Rust](https://www.rust-lang.org/) 1.72 or newer, to build the project and use the Rust API
- [Lua](https://www.lua.org/) 5.4 or newer, to use the Lua API
- Packages for [Smithay](https://github.com/Smithay/smithay):
-`libwayland libxkbcommon libudev libinput libgdm libseat`, as well as `xwayland`
+ `libwayland libxkbcommon libudev libinput libgdm libseat`, as well as `xwayland`
- Arch:
```sh
sudo pacman -S wayland wayland-protocols libxkbcommon systemd-libs libinput mesa seatd xorg-xwayland
@@ -118,10 +120,6 @@ See flags you can pass in by running `cargo run -- --help` (or `-h`).
# Configuration
Pinnacle is configured in your choice of Lua or Rust.
-> [!NOTE]
-> Pinnacle is currently in the process of migrating the configuration backend from MessagePack to gRPC.
-> The Lua library has already been rewritten, and the Rust API will be rewritten soon.
-
## Out-of-the-box configurations
If you just want to test Pinnacle out without copying stuff to your config directory,
run one of the following in the crate root:
@@ -132,13 +130,13 @@ PINNACLE_CONFIG_DIR="./api/lua/examples/default" cargo run
PINNACLE_CONFIG_DIR="~/.local/share/pinnacle/default_config" cargo run
# For a Rust configuration
-PINNACLE_CONFIG_DIR="./api/rust" cargo run
+PINNACLE_CONFIG_DIR="./api/rust/examples/default_config" cargo run
```
## Custom configuration
> [!IMPORTANT]
-> Pinnacle is under heavy development, and there *will* be major breaking changes to these APIs
+> Pinnacle is under development, and there *will* be major breaking changes to these APIs
> until I release version 0.1, at which point there will be an API stability spec in place.
>
> Until then, I recommend you either use the out-of-the-box configs above or prepare for
@@ -180,7 +178,7 @@ If you want to use Rust to configure Pinnacle, follow these steps:
1. In `~/.config/pinnacle`, run `cargo init`.
2. In the `Cargo.toml` file, add the following under `[dependencies]`:
```toml
-pinnacle_api = { git = "http://github.com/pinnacle-comp/pinnacle" }
+pinnacle-api = { git = "http://github.com/pinnacle-comp/pinnacle" }
```
3. Create the file `metaconfig.toml` at the root. Add the following to the file:
```toml
@@ -188,7 +186,7 @@ command = ["cargo", "run"]
reload_keybind = { modifiers = ["Ctrl", "Alt"], key = "r" }
kill_keybind = { modifiers = ["Ctrl", "Alt", "Shift"], key = "escape" }
```
-4. Copy the contents from [`example_config.rs`](api/rust/examples/example_config.rs) to `src/main.rs`.
+4. Copy the [default config](api/rust/examples/default_config/main.rs) to `src/main.rs`.
5. Run Pinnacle! (You may want to run `cargo build` beforehand so you don't have to wait for your config to compile.)
### API Documentation
diff --git a/api/lua/pinnacle/output.lua b/api/lua/pinnacle/output.lua
index 8e5415c..3139c9d 100644
--- a/api/lua/pinnacle/output.lua
+++ b/api/lua/pinnacle/output.lua
@@ -191,7 +191,7 @@ end
--- -- ┌─────┤ │
--- -- │DP-1 │HDMI-1 │
--- -- └─────┴───────┘
---- -- Notice that x = 0 aligns with the top of "DP-1", and the top of "HDMI-1" is at x = -360.
+--- -- Notice that y = 0 aligns with the top of "DP-1", and the top of "HDMI-1" is at y = -360.
---```
---
---@param loc { x: integer?, y: integer? }
diff --git a/api/lua/pinnacle/process.lua b/api/lua/pinnacle/process.lua
index 28c70f2..da9d195 100644
--- a/api/lua/pinnacle/process.lua
+++ b/api/lua/pinnacle/process.lua
@@ -108,6 +108,18 @@ function Process:spawn_once(args, callbacks)
spawn_inner(self.config_client, args, callbacks, true)
end
+---Set an environment variable for the compositor.
+---This will cause any future spawned processes to have this environment variable.
+---
+---@param key string The environment variable key
+---@param value string The environment variable value
+function Process:set_env(key, value)
+ self.config_client:unary_request(build_grpc_request_params("SetEnv", {
+ key = key,
+ value = value,
+ }))
+end
+
function process.new(config_client)
---@type Process
local self = { config_client = config_client }
diff --git a/api/rust/.gitignore b/api/rust/.gitignore
new file mode 100644
index 0000000..03314f7
--- /dev/null
+++ b/api/rust/.gitignore
@@ -0,0 +1 @@
+Cargo.lock
diff --git a/api/rust/Cargo.toml b/api/rust/Cargo.toml
index bf259ba..0d0caf2 100644
--- a/api/rust/Cargo.toml
+++ b/api/rust/Cargo.toml
@@ -1,14 +1,25 @@
[package]
-name = "pinnacle_api"
-version = "0.0.1"
+name = "pinnacle-api"
+version = "0.0.2"
edition = "2021"
-
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+authors = ["Ottatop "]
+description = "The Rust implementation of the Pinnacle compositor's configuration API"
+license = "MPL-2.0"
+repository = "https://github.com/pinnacle-comp/pinnacle"
+keywords = ["compositor", "pinnacle", "api", "config"]
+categories = ["api-bindings", "config"]
[dependencies]
-serde = { version = "1.0.188", features = ["derive"] }
-rmp = { version = "0.8.12" }
-rmp-serde = { version = "1.1.2" }
-anyhow = { version = "1.0.75", features = ["backtrace"] }
-lazy_static = "1.4.0"
+pinnacle-api-defs = { path = "../../pinnacle-api-defs" }
+pinnacle-api-macros = { path = "./pinnacle-api-macros" }
+tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread", "net"] }
+async-net = "2.0.0"
+async-compat = "0.2.3"
+tonic = "0.10.2"
+tower = { version = "0.4.13", features = ["util"] }
+futures = "0.3.30"
+num_enum = "0.7.2"
xkbcommon = "0.7.0"
+
+[workspace]
+members = ["pinnacle-api-macros"]
diff --git a/api/rust/examples/default_config/main.rs b/api/rust/examples/default_config/main.rs
new file mode 100644
index 0000000..7462eb5
--- /dev/null
+++ b/api/rust/examples/default_config/main.rs
@@ -0,0 +1,151 @@
+use pinnacle_api::xkbcommon::xkb::Keysym;
+use pinnacle_api::{
+ input::{Mod, MouseButton, MouseEdge},
+ tag::{Layout, LayoutCycler},
+ ApiModules,
+};
+
+#[pinnacle_api::config(modules)]
+async fn main() {
+ let ApiModules {
+ pinnacle,
+ process,
+ window,
+ input,
+ output,
+ tag,
+ } = modules;
+
+ let mod_key = Mod::Ctrl;
+
+ let terminal = "alacritty";
+
+ // Mousebinds
+
+ // `mod_key + left click` starts moving a window
+ input.mousebind([mod_key], MouseButton::Left, MouseEdge::Press, || {
+ window.begin_move(MouseButton::Left);
+ });
+
+ // `mod_key + right click` starts resizing a window
+ input.mousebind([mod_key], MouseButton::Right, MouseEdge::Press, || {
+ window.begin_resize(MouseButton::Right);
+ });
+
+ // Keybinds
+
+ // `mod_key + alt + q` quits Pinnacle
+ input.keybind([mod_key, Mod::Alt], 'q', || {
+ pinnacle.quit();
+ });
+
+ // `mod_key + alt + c` closes the focused window
+ input.keybind([mod_key, Mod::Alt], 'c', || {
+ if let Some(window) = window.get_focused() {
+ window.close();
+ }
+ });
+
+ // `mod_key + Return` spawns a terminal
+ input.keybind([mod_key], Keysym::Return, move || {
+ process.spawn([terminal]);
+ });
+
+ // `mod_key + alt + space` toggles floating
+ input.keybind([mod_key, Mod::Alt], Keysym::space, || {
+ if let Some(window) = window.get_focused() {
+ window.toggle_floating();
+ }
+ });
+
+ // `mod_key + f` toggles fullscreen
+ input.keybind([mod_key], 'f', || {
+ if let Some(window) = window.get_focused() {
+ window.toggle_fullscreen();
+ }
+ });
+
+ // `mod_key + m` toggles maximized
+ input.keybind([mod_key], 'm', || {
+ if let Some(window) = window.get_focused() {
+ window.toggle_maximized();
+ }
+ });
+
+ // Window rules
+ //
+ // You can define window rules to get windows to open with desired properties.
+ // See `pinnacle_api::window::rules` in the docs for more information.
+
+ // Tags
+
+ let tag_names = ["1", "2", "3", "4", "5"];
+
+ // Setup all monitors with tags "1" through "5"
+ output.connect_for_all(move |op| {
+ let mut tags = tag.add(&op, tag_names);
+
+ // Be sure to set a tag to active or windows won't display
+ tags.next().unwrap().set_active(true);
+ });
+
+ process.spawn_once([terminal]);
+
+ // Create a layout cycler to cycle through the given layouts
+ let LayoutCycler {
+ prev: layout_prev,
+ next: layout_next,
+ } = tag.new_layout_cycler([
+ Layout::MasterStack,
+ Layout::Dwindle,
+ Layout::Spiral,
+ Layout::CornerTopLeft,
+ Layout::CornerTopRight,
+ Layout::CornerBottomLeft,
+ Layout::CornerBottomRight,
+ ]);
+
+ // `mod_key + space` cycles to the next layout
+ input.keybind([mod_key], Keysym::space, move || {
+ layout_next(None);
+ });
+
+ // `mod_key + shift + space` cycles to the previous layout
+ input.keybind([mod_key, Mod::Shift], Keysym::space, move || {
+ layout_prev(None);
+ });
+
+ for tag_name in tag_names {
+ // `mod_key + 1-5` switches to tag "1" to "5"
+ input.keybind([mod_key], tag_name, move || {
+ if let Some(tg) = tag.get(tag_name) {
+ tg.switch_to();
+ }
+ });
+
+ // `mod_key + shift + 1-5` toggles tag "1" to "5"
+ input.keybind([mod_key, Mod::Shift], tag_name, move || {
+ if let Some(tg) = tag.get(tag_name) {
+ tg.toggle_active();
+ }
+ });
+
+ // `mod_key + alt + 1-5` moves the focused window to tag "1" to "5"
+ input.keybind([mod_key, Mod::Alt], tag_name, move || {
+ if let Some(tg) = tag.get(tag_name) {
+ if let Some(win) = window.get_focused() {
+ win.move_to_tag(&tg);
+ }
+ }
+ });
+
+ // `mod_key + shift + alt + 1-5` toggles tag "1" to "5" on the focused window
+ input.keybind([mod_key, Mod::Shift, Mod::Alt], tag_name, move || {
+ if let Some(tg) = tag.get(tag_name) {
+ if let Some(win) = window.get_focused() {
+ win.toggle_tag(&tg);
+ }
+ }
+ });
+ }
+}
diff --git a/api/rust/metaconfig.toml b/api/rust/examples/default_config/metaconfig.toml
similarity index 65%
rename from api/rust/metaconfig.toml
rename to api/rust/examples/default_config/metaconfig.toml
index aa70e7d..414a25a 100644
--- a/api/rust/metaconfig.toml
+++ b/api/rust/examples/default_config/metaconfig.toml
@@ -7,10 +7,11 @@
# ~/.config/pinnacle/
#
# When Pinnacle finds a metaconfig.toml file, it will execute the command provided to `command`.
-# For now, the only thing that should be here is `lua` with a path to the main config file.
-# In the future, there will be a Rust API that can be run using `cargo run`.
+# To use a Rust config, this should be changed to something like ["cargo", "run"].
#
# Because configuration is done using an external process, if it ever crashes, you lose all of your keybinds.
+# The compositor will load the default config if that happens, but in the event that you don't have
+# the necessary dependencies for it to run, you may get softlocked.
# In order prevent you from getting stuck in the compositor, you must define keybinds to reload your config
# and kill Pinnacle.
#
@@ -19,7 +20,7 @@
# The command Pinnacle will run on startup and when you reload your config.
# Paths are relative to the directory the metaconfig.toml file is in.
# This must be an array.
-command = ["cargo", "run", "--example", "example_config"]
+command = ["cargo", "run", "--example", "default_config"]
### Keybinds ###
# Each keybind takes in a table with two fields: `modifiers` and `key`.
@@ -40,13 +41,6 @@ kill_keybind = { modifiers = ["Ctrl", "Alt", "Shift"], key = "escape" }
# socket_dir = "/your/dir/here/"
### Environment Variables ###
-# You may need to specify to Lua where Pinnacle's Lua API library is.
-# This is currently done using the `envs` table, with keys as the name of the environment variable and
-# the value as the variable value. This supports $var expansion, and paths are relative to this metaconfig.toml file.
-#
-# Pinnacle will run your config with the additional PINNACLE_DIR environment variable.
-#
-# Here, LUA_PATH and LUA_CPATH are used to tell Lua the path to the library.
+# If you need to spawn your config with any environment variables, list them here.
[envs]
-# LUA_PATH = "$PINNACLE_LIB_DIR/lua/?.lua;$PINNACLE_LIB_DIR/lua/?/init.lua;$PINNACLE_LIB_DIR/lua/lib/?.lua;$PINNACLE_LIB_DIR/lua/lib/?/init.lua;$LUA_PATH"
-# LUA_CPATH = "$PINNACLE_LIB_DIR/lua/lib/?.so;$LUA_CPATH"
+# key = "value"
diff --git a/api/rust/examples/example_config.rs b/api/rust/examples/example_config.rs
deleted file mode 100644
index ea5b8cb..0000000
--- a/api/rust/examples/example_config.rs
+++ /dev/null
@@ -1,210 +0,0 @@
-// You should glob import these to prevent your config from getting cluttered.
-use pinnacle_api::prelude::*;
-use pinnacle_api::*;
-
-fn main() {
- // Connect to the Pinnacle server.
- // This needs to be called before you start calling any config functions.
- pinnacle_api::connect().unwrap();
-
- let mod_key = Modifier::Ctrl; // This is set to Ctrl to not conflict with your WM/DE keybinds.
-
- let terminal = "alacritty";
-
- process::set_env("MOZ_ENABLE_WAYLAND", "1");
-
- // You must create a callback_vec to hold your callbacks.
- // Rust is not Lua, so it takes a bit more work to get captures working.
- //
- // Anything that requires a callback will also require a mut reference to this struct.
- //
- // Additionally, all callbacks also take in `&mut CallbackVec`.
- // This allows you to call functions that need callbacks within other callbacks.
- let mut callback_vec = CallbackVec::new();
-
- // Keybinds ------------------------------------------------------
-
- input::mousebind(
- &[mod_key],
- MouseButton::Left,
- MouseEdge::Press,
- move |_| {
- window::begin_move(MouseButton::Left);
- },
- &mut callback_vec,
- );
-
- input::mousebind(
- &[mod_key],
- MouseButton::Right,
- MouseEdge::Press,
- move |_| {
- window::begin_resize(MouseButton::Right);
- },
- &mut callback_vec,
- );
-
- input::keybind(
- &[mod_key, Modifier::Alt],
- 'q',
- |_| pinnacle_api::quit(),
- &mut callback_vec,
- );
-
- input::keybind(
- &[mod_key, Modifier::Alt],
- 'c',
- move |_| {
- if let Some(window) = window::get_focused() {
- window.close();
- }
- },
- &mut callback_vec,
- );
-
- input::keybind(
- &[mod_key],
- xkbcommon::xkb::keysyms::KEY_Return,
- move |_| {
- process::spawn(vec![terminal]).unwrap();
- },
- &mut callback_vec,
- );
-
- input::keybind(
- &[mod_key, Modifier::Alt],
- xkbcommon::xkb::keysyms::KEY_space,
- move |_| {
- if let Some(window) = window::get_focused() {
- window.toggle_floating();
- }
- },
- &mut callback_vec,
- );
-
- input::keybind(
- &[mod_key],
- 'f',
- move |_| {
- if let Some(window) = window::get_focused() {
- window.toggle_fullscreen();
- }
- },
- &mut callback_vec,
- );
-
- input::keybind(
- &[mod_key],
- 'm',
- move |_| {
- if let Some(window) = window::get_focused() {
- window.toggle_maximized();
- }
- },
- &mut callback_vec,
- );
-
- // Output stuff -------------------------------------------------------
-
- let tags = ["1", "2", "3", "4", "5"];
-
- output::connect_for_all(
- move |output, _| {
- tag::add(&output, tags.as_slice());
- tag::get("1", Some(&output)).unwrap().toggle();
- },
- &mut callback_vec,
- );
-
- // Layouts -----------------------------------------------------------
-
- // Create a `LayoutCycler` to cycle your layouts.
- let mut layout_cycler = tag::layout_cycler(&[
- Layout::MasterStack,
- Layout::Dwindle,
- Layout::Spiral,
- Layout::CornerTopLeft,
- Layout::CornerTopRight,
- Layout::CornerBottomLeft,
- Layout::CornerBottomRight,
- ]);
-
- // Cycle forward.
- input::keybind(
- &[mod_key],
- xkbcommon::xkb::keysyms::KEY_space,
- move |_| {
- (layout_cycler.next)(None);
- },
- &mut callback_vec,
- );
-
- // Cycle backward.
- input::keybind(
- &[mod_key, Modifier::Shift],
- xkbcommon::xkb::keysyms::KEY_space,
- move |_| {
- (layout_cycler.prev)(None);
- },
- &mut callback_vec,
- );
-
- // Keybinds for tags ------------------------------------------
-
- for tag_name in tags.iter().map(|t| t.to_string()) {
- // mod_key + 1-5 to switch to tag
- let t = tag_name.clone();
- let num = tag_name.chars().next().unwrap();
- input::keybind(
- &[mod_key],
- num,
- move |_| {
- tag::get(&t, None).unwrap().switch_to();
- },
- &mut callback_vec,
- );
-
- // mod_key + Shift + 1-5 to toggle tag
- let t = tag_name.clone();
- input::keybind(
- &[mod_key, Modifier::Shift],
- num,
- move |_| {
- tag::get(&t, None).unwrap().toggle();
- },
- &mut callback_vec,
- );
-
- // mod_key + Alt + 1-5 to move focused window to tag
- let t = tag_name.clone();
- input::keybind(
- &[mod_key, Modifier::Alt],
- num,
- move |_| {
- if let Some(window) = window::get_focused() {
- window.move_to_tag(&tag::get(&t, None).unwrap());
- }
- },
- &mut callback_vec,
- );
-
- // mod_key + Shift + Alt + 1-5 to toggle tag on focused window
- let t = tag_name.clone();
- input::keybind(
- &[mod_key, Modifier::Shift, Modifier::Alt],
- num,
- move |_| {
- if let Some(window) = window::get_focused() {
- window.toggle_tag(&tag::get(&t, None).unwrap());
- }
- },
- &mut callback_vec,
- );
- }
-
- // At the very end of your config, you will need to start listening to Pinnacle in order for
- // your callbacks to be correctly called.
- //
- // This will not return unless an error occurs.
- pinnacle_api::listen(callback_vec);
-}
diff --git a/api/rust/pinnacle-api-macros/Cargo.toml b/api/rust/pinnacle-api-macros/Cargo.toml
new file mode 100644
index 0000000..ba39c76
--- /dev/null
+++ b/api/rust/pinnacle-api-macros/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "pinnacle-api-macros"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+quote = "1.0.35"
+syn = { version = "2.0.48", features = ["full", "parsing"] }
+proc-macro2 = "1.0.76"
+
+[lib]
+proc-macro = true
diff --git a/api/rust/pinnacle-api-macros/src/lib.rs b/api/rust/pinnacle-api-macros/src/lib.rs
new file mode 100644
index 0000000..7838045
--- /dev/null
+++ b/api/rust/pinnacle-api-macros/src/lib.rs
@@ -0,0 +1,166 @@
+use proc_macro2::{Ident, Span};
+use quote::{quote, quote_spanned};
+use syn::{
+ parse::Parse, parse_macro_input, punctuated::Punctuated, spanned::Spanned, Expr, Lit,
+ MetaNameValue, ReturnType, Stmt, Token,
+};
+
+/// Transform the annotated function into one used to configure the Pinnacle compositor.
+///
+/// This will cause the function to connect to Pinnacle's gRPC server, run your configuration code,
+/// then await all necessary futures needed to call callbacks.
+///
+/// This function will not return unless an error occurs.
+///
+/// # Usage
+/// The function must be marked `async`, as this macro will insert the `#[tokio::main]` macro below
+/// it.
+///
+/// It takes in an ident, with which Pinnacle's `ApiModules` struct will be bound to.
+///
+/// ```
+/// #[pinnacle_api::config(modules)]
+/// async fn main() {
+/// // `modules` is now accessible in the function body
+/// let ApiModules { .. } = modules;
+/// }
+/// ```
+///
+/// `pinnacle_api` annotates the function with a bare `#[tokio::main]` attribute.
+/// If you would like to configure Tokio's options, additionally pass in
+/// `internal_tokio = false` to this macro and annotate the function
+/// with your own `tokio::main` attribute.
+///
+/// `pinnacle_api` provides a re-export of `tokio` that may prove useful. If you need other Tokio
+/// features, you may need to bring them in with your own Cargo.toml.
+///
+/// Note: the `tokio::main` attribute must be inserted *below* the `pinnacle_api::config`
+/// attribute, as attributes are expanded from top to bottom.
+///
+/// ```
+/// #[pinnacle_api::config(modules, internal_tokio = false)]
+/// #[pinnacle_api::tokio::main(worker_threads = 8)]
+/// async fn main() {}
+/// ```
+#[proc_macro_attribute]
+pub fn config(
+ args: proc_macro::TokenStream,
+ item: proc_macro::TokenStream,
+) -> proc_macro::TokenStream {
+ let item = parse_macro_input!(item as syn::ItemFn);
+ let macro_input = parse_macro_input!(args as MacroInput);
+
+ let vis = item.vis;
+ let sig = item.sig;
+
+ if sig.asyncness.is_none() {
+ return quote_spanned! {sig.fn_token.span()=>
+ compile_error!("this function must be marked `async` to run a Pinnacle config");
+ }
+ .into();
+ }
+
+ if let ReturnType::Type(_, ty) = sig.output {
+ return quote_spanned! {ty.span()=>
+ compile_error!("this function must not have a return type");
+ }
+ .into();
+ }
+
+ let attrs = item.attrs;
+
+ let stmts = item.block.stmts;
+
+ if let Some(ret @ Stmt::Expr(Expr::Return(_), _)) = stmts.last() {
+ return quote_spanned! {ret.span()=>
+ compile_error!("this function must not return, as it awaits futures after the end of this statement");
+ }.into();
+ }
+
+ let module_ident = macro_input.ident;
+
+ let options = macro_input.options;
+
+ let mut has_internal_tokio = false;
+
+ let mut internal_tokio = true;
+
+ if let Some(options) = options {
+ for name_value in options.iter() {
+ if name_value.path.get_ident() == Some(&Ident::new("internal_tokio", Span::call_site()))
+ {
+ if has_internal_tokio {
+ return quote_spanned! {name_value.path.span()=>
+ compile_error!("`internal_tokio` defined twice, remove this one");
+ }
+ .into();
+ }
+
+ has_internal_tokio = true;
+ if let Expr::Lit(lit) = &name_value.value {
+ if let Lit::Bool(bool) = &lit.lit {
+ internal_tokio = bool.value;
+ continue;
+ }
+ }
+
+ return quote_spanned! {name_value.value.span()=>
+ compile_error!("expected `true` or `false`");
+ }
+ .into();
+ } else {
+ return quote_spanned! {name_value.path.span()=>
+ compile_error!("expected valid option (currently only `internal_tokio`)");
+ }
+ .into();
+ }
+ }
+ }
+
+ let tokio_attr = internal_tokio.then(|| {
+ quote! {
+ #[::pinnacle_api::tokio::main(crate = "::pinnacle_api::tokio")]
+ }
+ });
+
+ quote! {
+ #(#attrs)*
+ #tokio_attr
+ #vis #sig {
+ let (#module_ident, __fut_receiver) = ::pinnacle_api::connect().await.unwrap();
+
+ #(#stmts)*
+
+ ::pinnacle_api::listen(__fut_receiver).await;
+ }
+ }
+ .into()
+}
+
+struct MacroInput {
+ ident: syn::Ident,
+ options: Option>,
+}
+
+impl Parse for MacroInput {
+ fn parse(input: syn::parse::ParseStream) -> syn::Result {
+ let ident = input.parse()?;
+
+ let comma = input.parse::();
+
+ let mut options = None;
+
+ if comma.is_ok() {
+ options = Some(input.parse_terminated(MetaNameValue::parse, Token![,])?);
+ }
+
+ if !input.is_empty() {
+ return Err(syn::Error::new(
+ input.span(),
+ "expected `,` followed by options",
+ ));
+ }
+
+ Ok(MacroInput { ident, options })
+ }
+}
diff --git a/api/rust/src/input.rs b/api/rust/src/input.rs
index e80fba1..5127c0a 100644
--- a/api/rust/src/input.rs
+++ b/api/rust/src/input.rs
@@ -1,165 +1,383 @@
//! Input management.
+//!
+//! This module provides [`Input`], a struct that gives you several different
+//! methods for setting key- and mousebinds, changing xkeyboard settings, and more.
+//! View the struct's documentation for more information.
+
+use futures::{
+ channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture, FutureExt, StreamExt,
+};
+use num_enum::TryFromPrimitive;
+use pinnacle_api_defs::pinnacle::input::{
+ self,
+ v0alpha1::{
+ input_service_client::InputServiceClient,
+ set_libinput_setting_request::{CalibrationMatrix, Setting},
+ SetKeybindRequest, SetLibinputSettingRequest, SetMousebindRequest, SetRepeatRateRequest,
+ SetXkbConfigRequest,
+ },
+};
+use tonic::transport::Channel;
+use xkbcommon::xkb::Keysym;
+
+use self::libinput::LibinputSetting;
pub mod libinput;
-use xkbcommon::xkb::Keysym;
-
-use crate::{
- msg::{Args, CallbackId, KeyIntOrString, Msg},
- send_msg, CallbackVec,
-};
-
-/// Set a keybind.
-///
-/// This function takes in four parameters:
-/// - `modifiers`: A slice of the modifiers you want held for the keybind to trigger.
-/// - `key`: The key that needs to be pressed. This takes `impl Into` and can
-/// take the following three types:
-/// - [`char`]: A character of the key you want. This can be `a`, `~`, `@`, and so on.
-/// - [`u32`]: The key in numeric form. You can use the keys defined in [`xkbcommon::xkb::keysyms`] for this.
-/// - [`Keysym`]: The key in `Keysym` form, from [xkbcommon::xkb::Keysym].
-/// - `action`: What you want to run.
-/// - `callback_vec`: Your [`CallbackVec`] to insert `action` into.
-///
-/// `action` takes in a `&mut `[`CallbackVec`] for use in the closure.
-pub fn keybind<'a, F>(
- modifiers: &[Modifier],
- key: impl Into,
- mut action: F,
- callback_vec: &mut CallbackVec<'a>,
-) where
- F: FnMut(&mut CallbackVec) + 'a,
-{
- let args_callback = move |_: Option, callback_vec: &mut CallbackVec<'_>| {
- action(callback_vec);
- };
-
- let len = callback_vec.callbacks.len();
- callback_vec.callbacks.push(Box::new(args_callback));
-
- let key = key.into();
-
- let msg = Msg::SetKeybind {
- key,
- modifiers: modifiers.to_vec(),
- callback_id: CallbackId(len as u32),
- };
-
- send_msg(msg).unwrap();
-}
-
-/// Set a mousebind. If called with an already existing mousebind, it gets replaced.
-///
-/// The mousebind can happen either on button press or release, so you must
-/// specify which edge you desire.
-///
-/// `action` takes in a `&mut `[`CallbackVec`] for use in the closure.
-pub fn mousebind<'a, F>(
- modifiers: &[Modifier],
- button: MouseButton,
- edge: MouseEdge,
- mut action: F,
- callback_vec: &mut CallbackVec<'a>,
-) where
- F: FnMut(&mut CallbackVec) + 'a,
-{
- let args_callback = move |_: Option, callback_vec: &mut CallbackVec<'_>| {
- action(callback_vec);
- };
-
- let len = callback_vec.callbacks.len();
- callback_vec.callbacks.push(Box::new(args_callback));
-
- let msg = Msg::SetMousebind {
- modifiers: modifiers.to_vec(),
- button: button as u32,
- edge,
- callback_id: CallbackId(len as u32),
- };
-
- send_msg(msg).unwrap();
-}
-
-/// Set the xkbconfig for your keyboard.
-///
-/// Parameters set to `None` will be set to their default values.
-///
-/// Read `xkeyboard-config(7)` for more information.
-pub fn set_xkb_config(
- rules: Option<&str>,
- model: Option<&str>,
- layout: Option<&str>,
- variant: Option<&str>,
- options: Option<&str>,
-) {
- let msg = Msg::SetXkbConfig {
- rules: rules.map(|s| s.to_string()),
- variant: variant.map(|s| s.to_string()),
- layout: layout.map(|s| s.to_string()),
- model: model.map(|s| s.to_string()),
- options: options.map(|s| s.to_string()),
- };
-
- send_msg(msg).unwrap();
-}
-
/// A mouse button.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum MouseButton {
- /// The left mouse button.
+ /// The left mouse button
Left = 0x110,
- /// The right mouse button.
- Right,
- /// The middle mouse button, pressed usually by clicking the scroll wheel.
- Middle,
- ///
- Side,
- ///
- Extra,
- ///
- Forward,
- ///
- Back,
+ /// The right mouse button
+ Right = 0x111,
+ /// The middle mouse button
+ Middle = 0x112,
+ /// The side mouse button
+ Side = 0x113,
+ /// The extra mouse button
+ Extra = 0x114,
+ /// The forward mouse button
+ Forward = 0x115,
+ /// The backward mouse button
+ Back = 0x116,
}
-/// The edge on which you want things to trigger.
-#[derive(Debug, Hash, serde::Serialize, serde::Deserialize, Clone, Copy, PartialEq, Eq)]
+/// Keyboard modifiers.
+#[repr(i32)]
+#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, TryFromPrimitive)]
+pub enum Mod {
+ /// The shift key
+ Shift = 1,
+ /// The ctrl key
+ Ctrl,
+ /// The alt key
+ Alt,
+ /// The super key, aka meta, win, mod4
+ Super,
+}
+
+/// Press or release.
+#[repr(i32)]
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, TryFromPrimitive)]
pub enum MouseEdge {
- /// Actions will be triggered on button press.
- Press,
- /// Actions will be triggered on button release.
+ /// Perform actions on button press
+ Press = 1,
+ /// Perform actions on button release
Release,
}
-impl From for KeyIntOrString {
- fn from(value: char) -> Self {
- Self::String(value.to_string())
- }
+/// A struct that lets you define xkeyboard config options.
+///
+/// See `xkeyboard-config(7)` for more information.
+#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)]
+pub struct XkbConfig {
+ /// Files of rules to be used for keyboard mapping composition
+ pub rules: Option<&'static str>,
+ /// Name of the model of your keyboard type
+ pub model: Option<&'static str>,
+ /// Layout(s) you intend to use
+ pub layout: Option<&'static str>,
+ /// Variant(s) of the layout you intend to use
+ pub variant: Option<&'static str>,
+ /// Extra xkb configuration options
+ pub options: Option<&'static str>,
}
-impl From for KeyIntOrString {
- fn from(value: u32) -> Self {
- Self::Int(value)
- }
+/// The `Input` struct.
+///
+/// This struct contains methods that allow you to set key- and mousebinds,
+/// change xkeyboard and libinput settings, and change the keyboard's repeat rate.
+#[derive(Debug, Clone)]
+pub struct Input {
+ channel: Channel,
+ fut_sender: UnboundedSender>,
}
-impl From for KeyIntOrString {
- fn from(value: Keysym) -> Self {
- Self::Int(value.raw())
+impl Input {
+ pub(crate) fn new(
+ channel: Channel,
+ fut_sender: UnboundedSender>,
+ ) -> Self {
+ Self {
+ channel,
+ fut_sender,
+ }
}
-}
-/// A modifier key.
-#[derive(Debug, PartialEq, Eq, Copy, Clone, serde::Serialize, serde::Deserialize)]
-pub enum Modifier {
- /// The shift key.
- Shift,
- /// The control key.
- Ctrl,
- /// The alt key.
- Alt,
- /// The super key.
+ fn create_input_client(&self) -> InputServiceClient {
+ InputServiceClient::new(self.channel.clone())
+ }
+
+ /// Set a keybind.
///
- /// This is also known as the Windows key, meta, or Mod4 for those coming from Xorg.
- Super,
+ /// If called with an already set keybind, it gets replaced.
+ ///
+ /// You must supply:
+ /// - `mods`: A list of [`Mod`]s. These must be held down for the keybind to trigger.
+ /// - `key`: The key that needs to be pressed. This can be anything that implements the [Key] trait:
+ /// - `char`
+ /// - `&str` and `String`: This is any name from
+ /// [xkbcommon-keysyms.h](https://xkbcommon.org/doc/current/xkbcommon-keysyms_8h.html)
+ /// without the `XKB_KEY_` prefix.
+ /// - `u32`: The numerical key code from the website above.
+ /// - A [`keysym`][Keysym] from the [`xkbcommon`] re-export.
+ /// - `action`: A closure that will be run when the keybind is triggered.
+ /// - Currently, any captures must be both `Send` and `'static`. If you want to mutate
+ /// something, consider using channels or [`Box::leak`].
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use pinnacle_api::input::Mod;
+ ///
+ /// // Set `Super + Shift + c` to close the focused window
+ /// input.keybind([Mod::Super, Mod::Shift], 'c', || {
+ /// if let Some(win) = window.get_focused() {
+ /// win.close();
+ /// }
+ /// });
+ ///
+ /// // With a string key
+ /// input.keybind([], "BackSpace", || { /* ... */ });
+ ///
+ /// // With a numeric key
+ /// input.keybind([], 65, || { /* ... */ }); // 65 = 'A'
+ ///
+ /// // With a `Keysym`
+ /// input.keybind([], pinnacle_api::xkbcommon::xkb::Keysym::Return, || { /* ... */ });
+ /// ```
+ pub fn keybind(
+ &self,
+ mods: impl IntoIterator,
+ key: impl Key + Send + 'static,
+ mut action: impl FnMut() + Send + 'static,
+ ) {
+ let mut client = self.create_input_client();
+
+ let modifiers = mods.into_iter().map(|modif| modif as i32).collect();
+
+ self.fut_sender
+ .unbounded_send(
+ async move {
+ let mut stream = client
+ .set_keybind(SetKeybindRequest {
+ modifiers,
+ key: Some(input::v0alpha1::set_keybind_request::Key::RawCode(
+ key.into_keysym().raw(),
+ )),
+ })
+ .await
+ .unwrap()
+ .into_inner();
+
+ while let Some(Ok(_response)) = stream.next().await {
+ action();
+ }
+ }
+ .boxed(),
+ )
+ .unwrap();
+ }
+
+ /// Set a mousebind.
+ ///
+ /// If called with an already set mousebind, it gets replaced.
+ ///
+ /// You must supply:
+ /// - `mods`: A list of [`Mod`]s. These must be held down for the keybind to trigger.
+ /// - `button`: A [`MouseButton`].
+ /// - `edge`: A [`MouseEdge`]. This allows you to trigger the bind on either mouse press or release.
+ /// - `action`: A closure that will be run when the mousebind is triggered.
+ /// - Currently, any captures must be both `Send` and `'static`. If you want to mutate
+ /// something, consider using channels or [`Box::leak`].
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use pinnacle_api::input::{Mod, MouseButton, MouseEdge};
+ ///
+ /// // Set `Super + left click` to start moving a window
+ /// input.mousebind([Mod::Super], MouseButton::Left, MouseEdge::Press, || {
+ /// window.begin_move(MouseButton::Press);
+ /// });
+ /// ```
+ pub fn mousebind(
+ &self,
+ mods: impl IntoIterator,
+ button: MouseButton,
+ edge: MouseEdge,
+ mut action: impl FnMut() + 'static + Send,
+ ) {
+ let mut client = self.create_input_client();
+
+ let modifiers = mods.into_iter().map(|modif| modif as i32).collect();
+
+ self.fut_sender
+ .unbounded_send(
+ async move {
+ let mut stream = client
+ .set_mousebind(SetMousebindRequest {
+ modifiers,
+ button: Some(button as u32),
+ edge: Some(edge as i32),
+ })
+ .await
+ .unwrap()
+ .into_inner();
+
+ while let Some(Ok(_response)) = stream.next().await {
+ action();
+ }
+ }
+ .boxed(),
+ )
+ .unwrap();
+ }
+
+ /// Set the xkeyboard config.
+ ///
+ /// This allows you to set several xkeyboard options like `layout` and `rules`.
+ ///
+ /// See `xkeyboard-config(7)` for more information.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use pinnacle_api::input::XkbConfig;
+ ///
+ /// input.set_xkb_config(Xkbconfig {
+ /// layout: Some("us,fr,ge"),
+ /// options: Some("ctrl:swapcaps,caps:shift"),
+ /// ..Default::default()
+ /// });
+ /// ```
+ pub fn set_xkb_config(&self, xkb_config: XkbConfig) {
+ let mut client = self.create_input_client();
+
+ block_on(client.set_xkb_config(SetXkbConfigRequest {
+ rules: xkb_config.rules.map(String::from),
+ variant: xkb_config.variant.map(String::from),
+ layout: xkb_config.layout.map(String::from),
+ model: xkb_config.model.map(String::from),
+ options: xkb_config.options.map(String::from),
+ }))
+ .unwrap();
+ }
+
+ /// Set the keyboard's repeat rate.
+ ///
+ /// This allows you to set the time between holding down a key and it repeating
+ /// as well as the time between each repeat.
+ ///
+ /// Units are in milliseconds.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// // Set keyboard to repeat after holding down for half a second,
+ /// // and repeat once every 25ms (40 times a second)
+ /// input.set_repeat_rate(25, 500);
+ /// ```
+ pub fn set_repeat_rate(&self, rate: i32, delay: i32) {
+ let mut client = self.create_input_client();
+
+ block_on(client.set_repeat_rate(SetRepeatRateRequest {
+ rate: Some(rate),
+ delay: Some(delay),
+ }))
+ .unwrap();
+ }
+
+ /// Set a libinput setting.
+ ///
+ /// From [freedesktop.org](https://www.freedesktop.org/wiki/Software/libinput/):
+ /// > libinput is a library to handle input devices in Wayland compositors
+ ///
+ /// As such, this method allows you to set various settings related to input devices.
+ /// This includes things like pointer acceleration and natural scrolling.
+ ///
+ /// See [`LibinputSetting`] for all the settings you can change.
+ ///
+ /// Note: currently Pinnacle applies anything set here to *every* device, regardless of what it
+ /// actually is. This will be fixed in the future.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use pinnacle_api::input::libinput::*;
+ ///
+ /// // Set pointer acceleration to flat
+ /// input.set_libinput_setting(LibinputSetting::AccelProfile(AccelProfile::Flat));
+ ///
+ /// // Enable natural scrolling (reverses scroll direction; usually used with trackpads)
+ /// input.set_libinput_setting(LibinputSetting::NaturalScroll(true));
+ /// ```
+ pub fn set_libinput_setting(&self, setting: LibinputSetting) {
+ let mut client = self.create_input_client();
+
+ let setting = match setting {
+ LibinputSetting::AccelProfile(profile) => Setting::AccelProfile(profile as i32),
+ LibinputSetting::AccelSpeed(speed) => Setting::AccelSpeed(speed),
+ LibinputSetting::CalibrationMatrix(matrix) => {
+ Setting::CalibrationMatrix(CalibrationMatrix {
+ matrix: matrix.to_vec(),
+ })
+ }
+ LibinputSetting::ClickMethod(method) => Setting::ClickMethod(method as i32),
+ LibinputSetting::DisableWhileTyping(disable) => Setting::DisableWhileTyping(disable),
+ LibinputSetting::LeftHanded(enable) => Setting::LeftHanded(enable),
+ LibinputSetting::MiddleEmulation(enable) => Setting::MiddleEmulation(enable),
+ LibinputSetting::RotationAngle(angle) => Setting::RotationAngle(angle),
+ LibinputSetting::ScrollButton(button) => Setting::RotationAngle(button),
+ LibinputSetting::ScrollButtonLock(enable) => Setting::ScrollButtonLock(enable),
+ LibinputSetting::ScrollMethod(method) => Setting::ScrollMethod(method as i32),
+ LibinputSetting::NaturalScroll(enable) => Setting::NaturalScroll(enable),
+ LibinputSetting::TapButtonMap(map) => Setting::TapButtonMap(map as i32),
+ LibinputSetting::TapDrag(enable) => Setting::TapDrag(enable),
+ LibinputSetting::TapDragLock(enable) => Setting::TapDragLock(enable),
+ LibinputSetting::Tap(enable) => Setting::Tap(enable),
+ };
+
+ block_on(client.set_libinput_setting(SetLibinputSettingRequest {
+ setting: Some(setting),
+ }))
+ .unwrap();
+ }
+}
+
+/// A trait that designates anything that can be converted into a [`Keysym`].
+pub trait Key {
+ /// Convert this into a [`Keysym`].
+ fn into_keysym(self) -> Keysym;
+}
+
+impl Key for Keysym {
+ fn into_keysym(self) -> Keysym {
+ self
+ }
+}
+
+impl Key for char {
+ fn into_keysym(self) -> Keysym {
+ Keysym::from_char(self)
+ }
+}
+
+impl Key for &str {
+ fn into_keysym(self) -> Keysym {
+ xkbcommon::xkb::keysym_from_name(self, xkbcommon::xkb::KEYSYM_NO_FLAGS)
+ }
+}
+
+impl Key for String {
+ fn into_keysym(self) -> Keysym {
+ xkbcommon::xkb::keysym_from_name(&self, xkbcommon::xkb::KEYSYM_NO_FLAGS)
+ }
+}
+
+impl Key for u32 {
+ fn into_keysym(self) -> Keysym {
+ Keysym::from(self)
+ }
}
diff --git a/api/rust/src/input/libinput.rs b/api/rust/src/input/libinput.rs
index 23dddeb..5e24df7 100644
--- a/api/rust/src/input/libinput.rs
+++ b/api/rust/src/input/libinput.rs
@@ -1,40 +1,35 @@
-//! Libinput settings.
+//! Types for libinput configuration.
-use crate::{msg::Msg, send_msg};
-
-/// Set a libinput setting.
-///
-/// This takes a [`LibinputSetting`] containing what you want set.
-pub fn set(setting: LibinputSetting) {
- let msg = Msg::SetLibinputSetting(setting);
- send_msg(msg).unwrap();
-}
-
-/// The acceleration profile.
-#[derive(Debug, PartialEq, Copy, Clone, serde::Serialize)]
+/// Pointer acceleration profile
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AccelProfile {
- /// Flat pointer acceleration.
- Flat,
- /// Adaptive pointer acceleration.
+ /// A flat acceleration profile.
///
- /// This is the default for most devices.
+ /// Pointer motion is accelerated by a constant (device-specific) factor, depending on the current speed.
+ Flat = 1,
+ /// An adaptive acceleration profile.
+ ///
+ /// Pointer acceleration depends on the input speed. This is the default profile for most devices.
Adaptive,
}
-/// The click method for a touchpad.
-#[derive(Debug, PartialEq, Copy, Clone, serde::Serialize)]
+/// The click method defines when to generate software-emulated buttons, usually on a device
+/// that does not have a specific physical button available.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ClickMethod {
/// Use software-button areas to generate button events.
- ButtonAreas,
+ ButtonAreas = 1,
/// The number of fingers decides which button press to generate.
Clickfinger,
}
-/// The scroll method for a touchpad.
-#[derive(Debug, PartialEq, Copy, Clone, serde::Serialize)]
+/// The scroll method of a device selects when to generate scroll axis events instead of pointer motion events.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ScrollMethod {
- /// Never send scroll events.
- NoScroll,
+ /// Never send scroll events instead of pointer motion events.
+ ///
+ /// This has no effect on events generated by scroll wheels.
+ NoScroll = 1,
/// Send scroll events when two fingers are logically down on the device.
TwoFinger,
/// Send scroll events when a finger moves along the bottom or right edge of a device.
@@ -43,63 +38,48 @@ pub enum ScrollMethod {
OnButtonDown,
}
-/// The mapping between finger count and button event for a touchpad.
-#[derive(Debug, PartialEq, Copy, Clone, serde::Serialize)]
+/// Map 1/2/3 finger tips to buttons.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TapButtonMap {
- /// 1/2/3 finger tap is mapped to left/right/middle click.
+ /// 1/2/3 finger tap maps to left/right/middle
LeftRightMiddle,
- /// 1/2/3 finger tap is mapped to left/middle/right click.
+ /// 1/2/3 finger tap maps to left/middle/right
LeftMiddleRight,
}
-/// Libinput settings.
-#[derive(Debug, PartialEq, Copy, Clone, serde::Serialize)]
+/// Possible settings for libinput.
+#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LibinputSetting {
- /// Set the acceleration profile.
+ /// Set the pointer acceleration profile
AccelProfile(AccelProfile),
- /// Set the acceleration speed.
- ///
- /// This should be a float from -1.0 to 1.0.
+ /// Set pointer acceleration speed
AccelSpeed(f64),
- /// Set the calibration matrix.
+ /// Set the calibration matrix
CalibrationMatrix([f32; 6]),
- /// Set the click method.
- ///
- /// The click method defines when to generate software-emulated buttons, usually on a device
- /// that does not have a specific physical button available.
+ /// Set the [`ClickMethod`]
ClickMethod(ClickMethod),
- /// Set whether or not the device will be disabled while typing.
- DisableWhileTypingEnabled(bool),
- /// Set device left-handedness.
+ /// Set whether the device gets disabled while typing
+ DisableWhileTyping(bool),
+ /// Set left handed mode
LeftHanded(bool),
- /// Set whether or not the middle click can be emulated.
- MiddleEmulationEnabled(bool),
- /// Set the rotation angle of a device.
+ /// Allow or disallow middle mouse button emulation
+ MiddleEmulation(bool),
+ /// Set the rotation angle
RotationAngle(u32),
- /// Set the scroll method.
- ScrollMethod(ScrollMethod),
- /// Set whether or not natural scroll is enabled.
- ///
- /// This reverses the direction of scrolling and is mainly used with touchpads.
- NaturalScrollEnabled(bool),
- /// Set the scroll button.
+ /// Set the scroll button
ScrollButton(u32),
- /// Set the tap button map,
- ///
- /// This determines whether taps with 2 and 3 fingers register as right and middle clicks or
- /// the reverse.
+ /// Set whether the scroll button should be a drag or toggle
+ ScrollButtonLock(bool),
+ /// Set the [`ScrollMethod`]
+ ScrollMethod(ScrollMethod),
+ /// Enable or disable natural scrolling
+ NaturalScroll(bool),
+ /// Set the [`TapButtonMap`]
TapButtonMap(TapButtonMap),
- /// Set whether or not tap-and-drag is enabled.
- ///
- /// When enabled, a single-finger tap immediately followed by a finger down results in
- /// a button down event, and subsequent finger motion thus triggers a drag.
- /// The button is released on finger up.
- TapDragEnabled(bool),
- /// Set whether or not tap drag lock is enabled.
- ///
- /// When enabled, a finger may be lifted and put back on the touchpad within a timeout and the drag process
- /// continues. When disabled, lifting the finger during a tap-and-drag will immediately stop the drag.
- TapDragLockEnabled(bool),
- /// Set whether or not tap-to-click is enabled.
- TapEnabled(bool),
+ /// Enable or disable tap-to-drag
+ TapDrag(bool),
+ /// Enable or disable a timeout where lifting a finger off the device will not stop dragging
+ TapDragLock(bool),
+ /// Enable or disable tap-to-click
+ Tap(bool),
}
diff --git a/api/rust/src/lib.rs b/api/rust/src/lib.rs
index b403eec..64acc50 100644
--- a/api/rust/src/lib.rs
+++ b/api/rust/src/lib.rs
@@ -1,234 +1,200 @@
-//! The Rust implementation of the configuration API for Pinnacle,
-//! a [Smithay](https://github.com/Smithay/smithay)-based Wayland compositor
-//! inspired by [AwesomeWM](https://github.com/awesomeWM/awesome).
-
#![warn(missing_docs)]
+//! The Rust implementation of [Pinnacle](https://github.com/pinnacle-comp/pinnacle)'s
+//! configuration API.
+//!
+//! This library allows you to interface with the Pinnacle compositor and configure various aspects
+//! like input and the tag system.
+//!
+//! # Configuration
+//!
+//! ## 1. Create a cargo project
+//! To create your own Rust config, create a Cargo project in `~/.config/pinnacle`.
+//!
+//! ## 2. Create `metaconfig.toml`
+//! Then, create a file named `metaconfig.toml`. This is the file Pinnacle will use to determine
+//! what to run, kill and reload-config keybinds, an optional socket directory, and any environment
+//! variables to give the config client.
+//!
+//! In `metaconfig.toml`, put the following:
+//! ```toml
+//! # `command` will tell Pinnacle to run `cargo run` in your config directory.
+//! # You can add stuff like "--release" here if you want to.
+//! command = ["cargo", "run"]
+//!
+//! # You must define a keybind to reload your config if it crashes, otherwise you'll get stuck if
+//! # the Lua config doesn't kick in properly.
+//! reload_keybind = { modifiers = ["Ctrl", "Alt"], key = "r" }
+//!
+//! # Similarly, you must define a keybind to kill Pinnacle.
+//! kill_keybind = { modifiers = ["Ctrl", "Alt", "Shift"], key = "escape" }
+//!
+//! # You can specify an optional socket directory if you need to place the socket Pinnacle will
+//! # use for configuration in a different place.
+//! # socket_dir = "your/dir/here"
+//!
+//! # If you need to set any environment variables for the config process, you can do so here if
+//! # you don't want to do it in the config itself.
+//! [envs]
+//! # key = "value"
+//! ```
+//!
+//! ## 3. Set up dependencies
+//! In your `Cargo.toml`, add a dependency to `pinnacle-api`:
+//!
+//! ```toml
+//! # Cargo.toml
+//!
+//! [dependencies]
+//! pinnacle-api = { git = "https://github.com/pinnacle-comp/pinnacle" }
+//! ```
+//!
+//! ## 4. Set up the main function
+//! In `main.rs`, change `fn main()` to `async fn main()` and annotate it with the
+//! [`pinnacle_api::config`][`crate::config`] macro. Pass in the identifier you want to bind the
+//! config modules to:
+//!
+//! ```
+//! use pinnacle_api::ApiModules;
+//!
+//! #[pinnacle_api::config(modules)]
+//! async fn main() {
+//! // `modules` is now available in the function body.
+//! // You can deconstruct `ApiModules` to get all the config structs.
+//! let ApiModules {
+//! pinnacle,
+//! process,
+//! window,
+//! input,
+//! output,
+//! tag,
+//! } = modules;
+//! }
+//! ```
+//!
+//! ## 5. Begin crafting your config!
+//! You can peruse the documentation for things to configure.
+
+use std::sync::OnceLock;
+
+use futures::{
+ channel::mpsc::UnboundedReceiver, future::BoxFuture, stream::FuturesUnordered, StreamExt,
+};
+use input::Input;
+use output::Output;
+use pinnacle::Pinnacle;
+use process::Process;
+use tag::Tag;
+use tonic::transport::{Endpoint, Uri};
+use tower::service_fn;
+use window::Window;
+
pub mod input;
-mod msg;
pub mod output;
+pub mod pinnacle;
pub mod process;
pub mod tag;
+pub mod util;
pub mod window;
-/// The xkbcommon crate, re-exported for your convenience.
+pub use pinnacle_api_macros::config;
+pub use tokio;
pub use xkbcommon;
-/// The prelude for the Pinnacle API.
+static PINNACLE: OnceLock = OnceLock::new();
+static PROCESS: OnceLock = OnceLock::new();
+static WINDOW: OnceLock = OnceLock::new();
+static INPUT: OnceLock = OnceLock::new();
+static OUTPUT: OnceLock