Nuke the old Rust API form orbit

This commit is contained in:
Ottatop 2024-01-21 23:45:09 -06:00
parent 0b88ad298b
commit 1cdeb59a38
28 changed files with 2698 additions and 4750 deletions

View file

@ -1,14 +1,25 @@
[package]
name = "pinnacle_api"
version = "0.0.1"
name = "pinnacle-api"
version = "0.0.2"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
authors = ["Ottatop <ottatop1227@gmail.com>"]
description = "The Rust implementation of the Pinnacle compositor's configuration API"
license = "MPL-2.0"
repository = "https://github.com/pinnacle-comp/pinnacle"
keywords = ["compositor", "pinnacle", "api", "config"]
categories = ["api-bindings", "config"]
[dependencies]
serde = { version = "1.0.188", features = ["derive"] }
rmp = { version = "0.8.12" }
rmp-serde = { version = "1.1.2" }
anyhow = { version = "1.0.75", features = ["backtrace"] }
lazy_static = "1.4.0"
pinnacle-api-defs = { path = "../../pinnacle-api-defs" }
pinnacle-api-macros = { path = "./pinnacle-api-macros" }
tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread", "net"] }
async-net = "2.0.0"
async-compat = "0.2.3"
tonic = "0.10.2"
tower = { version = "0.4.13", features = ["util"] }
futures = "0.3.30"
num_enum = "0.7.2"
xkbcommon = "0.7.0"
[workspace]
members = ["pinnacle-api-macros"]

View file

@ -1,210 +0,0 @@
// You should glob import these to prevent your config from getting cluttered.
use pinnacle_api::prelude::*;
use pinnacle_api::*;
fn main() {
// Connect to the Pinnacle server.
// This needs to be called before you start calling any config functions.
pinnacle_api::connect().unwrap();
let mod_key = Modifier::Ctrl; // This is set to Ctrl to not conflict with your WM/DE keybinds.
let terminal = "alacritty";
process::set_env("MOZ_ENABLE_WAYLAND", "1");
// You must create a callback_vec to hold your callbacks.
// Rust is not Lua, so it takes a bit more work to get captures working.
//
// Anything that requires a callback will also require a mut reference to this struct.
//
// Additionally, all callbacks also take in `&mut CallbackVec`.
// This allows you to call functions that need callbacks within other callbacks.
let mut callback_vec = CallbackVec::new();
// Keybinds ------------------------------------------------------
input::mousebind(
&[mod_key],
MouseButton::Left,
MouseEdge::Press,
move |_| {
window::begin_move(MouseButton::Left);
},
&mut callback_vec,
);
input::mousebind(
&[mod_key],
MouseButton::Right,
MouseEdge::Press,
move |_| {
window::begin_resize(MouseButton::Right);
},
&mut callback_vec,
);
input::keybind(
&[mod_key, Modifier::Alt],
'q',
|_| pinnacle_api::quit(),
&mut callback_vec,
);
input::keybind(
&[mod_key, Modifier::Alt],
'c',
move |_| {
if let Some(window) = window::get_focused() {
window.close();
}
},
&mut callback_vec,
);
input::keybind(
&[mod_key],
xkbcommon::xkb::keysyms::KEY_Return,
move |_| {
process::spawn(vec![terminal]).unwrap();
},
&mut callback_vec,
);
input::keybind(
&[mod_key, Modifier::Alt],
xkbcommon::xkb::keysyms::KEY_space,
move |_| {
if let Some(window) = window::get_focused() {
window.toggle_floating();
}
},
&mut callback_vec,
);
input::keybind(
&[mod_key],
'f',
move |_| {
if let Some(window) = window::get_focused() {
window.toggle_fullscreen();
}
},
&mut callback_vec,
);
input::keybind(
&[mod_key],
'm',
move |_| {
if let Some(window) = window::get_focused() {
window.toggle_maximized();
}
},
&mut callback_vec,
);
// Output stuff -------------------------------------------------------
let tags = ["1", "2", "3", "4", "5"];
output::connect_for_all(
move |output, _| {
tag::add(&output, tags.as_slice());
tag::get("1", Some(&output)).unwrap().toggle();
},
&mut callback_vec,
);
// Layouts -----------------------------------------------------------
// Create a `LayoutCycler` to cycle your layouts.
let mut layout_cycler = tag::layout_cycler(&[
Layout::MasterStack,
Layout::Dwindle,
Layout::Spiral,
Layout::CornerTopLeft,
Layout::CornerTopRight,
Layout::CornerBottomLeft,
Layout::CornerBottomRight,
]);
// Cycle forward.
input::keybind(
&[mod_key],
xkbcommon::xkb::keysyms::KEY_space,
move |_| {
(layout_cycler.next)(None);
},
&mut callback_vec,
);
// Cycle backward.
input::keybind(
&[mod_key, Modifier::Shift],
xkbcommon::xkb::keysyms::KEY_space,
move |_| {
(layout_cycler.prev)(None);
},
&mut callback_vec,
);
// Keybinds for tags ------------------------------------------
for tag_name in tags.iter().map(|t| t.to_string()) {
// mod_key + 1-5 to switch to tag
let t = tag_name.clone();
let num = tag_name.chars().next().unwrap();
input::keybind(
&[mod_key],
num,
move |_| {
tag::get(&t, None).unwrap().switch_to();
},
&mut callback_vec,
);
// mod_key + Shift + 1-5 to toggle tag
let t = tag_name.clone();
input::keybind(
&[mod_key, Modifier::Shift],
num,
move |_| {
tag::get(&t, None).unwrap().toggle();
},
&mut callback_vec,
);
// mod_key + Alt + 1-5 to move focused window to tag
let t = tag_name.clone();
input::keybind(
&[mod_key, Modifier::Alt],
num,
move |_| {
if let Some(window) = window::get_focused() {
window.move_to_tag(&tag::get(&t, None).unwrap());
}
},
&mut callback_vec,
);
// mod_key + Shift + Alt + 1-5 to toggle tag on focused window
let t = tag_name.clone();
input::keybind(
&[mod_key, Modifier::Shift, Modifier::Alt],
num,
move |_| {
if let Some(window) = window::get_focused() {
window.toggle_tag(&tag::get(&t, None).unwrap());
}
},
&mut callback_vec,
);
}
// At the very end of your config, you will need to start listening to Pinnacle in order for
// your callbacks to be correctly called.
//
// This will not return unless an error occurs.
pinnacle_api::listen(callback_vec);
}

View file

@ -1,52 +0,0 @@
# This metaconfig.toml file dictates what config Pinnacle will run.
#
# When running Pinnacle, the compositor will look in the following directories for a metaconfig.toml file,
# in order from top to bottom:
# $PINNACLE_CONFIG_DIR
# $XDG_CONFIG_HOME/pinnacle/
# ~/.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`.
#
# Because configuration is done using an external process, if it ever crashes, you lose all of your keybinds.
# In order prevent you from getting stuck in the compositor, you must define keybinds to reload your config
# and kill Pinnacle.
#
# More details on each setting can be found below.
# 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"]
### Keybinds ###
# Each keybind takes in a table with two fields: `modifiers` and `key`.
# - `modifiers` can be one of "Ctrl", "Alt", "Shift", or "Super".
# - `key` can be a string of any lowercase letter, number,
# "numN" where N is a number for numpad keys, or "esc"/"escape".
# Support for any xkbcommon key is planned for a future update.
# The keybind that will reload your config.
reload_keybind = { modifiers = ["Ctrl", "Alt"], key = "r" }
# The keybind that will kill Pinnacle.
kill_keybind = { modifiers = ["Ctrl", "Alt", "Shift"], key = "escape" }
### Socket directory ###
# Pinnacle will open a Unix socket at `$XDG_RUNTIME_DIR` by default, falling back to `/tmp` if it doesn't exist.
# If you want/need to change this, use the `socket_dir` setting set to the directory of your choosing.
#
# 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.
[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"

View file

@ -1,165 +1,383 @@
//! Input management.
//!
//! This module provides [`Input`], a struct that gives you several different
//! methods for setting key- and mousebinds, changing xkeyboard settings, and more.
//! View the struct's documentation for more information.
use futures::{
channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture, FutureExt, StreamExt,
};
use num_enum::TryFromPrimitive;
use pinnacle_api_defs::pinnacle::input::{
self,
v0alpha1::{
input_service_client::InputServiceClient,
set_libinput_setting_request::{CalibrationMatrix, Setting},
SetKeybindRequest, SetLibinputSettingRequest, SetMousebindRequest, SetRepeatRateRequest,
SetXkbConfigRequest,
},
};
use tonic::transport::Channel;
use xkbcommon::xkb::Keysym;
use self::libinput::LibinputSetting;
pub mod libinput;
use xkbcommon::xkb::Keysym;
use crate::{
msg::{Args, CallbackId, KeyIntOrString, Msg},
send_msg, CallbackVec,
};
/// Set a keybind.
///
/// This function takes in four parameters:
/// - `modifiers`: A slice of the modifiers you want held for the keybind to trigger.
/// - `key`: The key that needs to be pressed. This takes `impl Into<KeyIntOrString>` and can
/// take the following three types:
/// - [`char`]: A character of the key you want. This can be `a`, `~`, `@`, and so on.
/// - [`u32`]: The key in numeric form. You can use the keys defined in [`xkbcommon::xkb::keysyms`] for this.
/// - [`Keysym`]: The key in `Keysym` form, from [xkbcommon::xkb::Keysym].
/// - `action`: What you want to run.
/// - `callback_vec`: Your [`CallbackVec`] to insert `action` into.
///
/// `action` takes in a `&mut `[`CallbackVec`] for use in the closure.
pub fn keybind<'a, F>(
modifiers: &[Modifier],
key: impl Into<KeyIntOrString>,
mut action: F,
callback_vec: &mut CallbackVec<'a>,
) where
F: FnMut(&mut CallbackVec) + 'a,
{
let args_callback = move |_: Option<Args>, callback_vec: &mut CallbackVec<'_>| {
action(callback_vec);
};
let len = callback_vec.callbacks.len();
callback_vec.callbacks.push(Box::new(args_callback));
let key = key.into();
let msg = Msg::SetKeybind {
key,
modifiers: modifiers.to_vec(),
callback_id: CallbackId(len as u32),
};
send_msg(msg).unwrap();
}
/// Set a mousebind. If called with an already existing mousebind, it gets replaced.
///
/// The mousebind can happen either on button press or release, so you must
/// specify which edge you desire.
///
/// `action` takes in a `&mut `[`CallbackVec`] for use in the closure.
pub fn mousebind<'a, F>(
modifiers: &[Modifier],
button: MouseButton,
edge: MouseEdge,
mut action: F,
callback_vec: &mut CallbackVec<'a>,
) where
F: FnMut(&mut CallbackVec) + 'a,
{
let args_callback = move |_: Option<Args>, callback_vec: &mut CallbackVec<'_>| {
action(callback_vec);
};
let len = callback_vec.callbacks.len();
callback_vec.callbacks.push(Box::new(args_callback));
let msg = Msg::SetMousebind {
modifiers: modifiers.to_vec(),
button: button as u32,
edge,
callback_id: CallbackId(len as u32),
};
send_msg(msg).unwrap();
}
/// Set the xkbconfig for your keyboard.
///
/// Parameters set to `None` will be set to their default values.
///
/// Read `xkeyboard-config(7)` for more information.
pub fn set_xkb_config(
rules: Option<&str>,
model: Option<&str>,
layout: Option<&str>,
variant: Option<&str>,
options: Option<&str>,
) {
let msg = Msg::SetXkbConfig {
rules: rules.map(|s| s.to_string()),
variant: variant.map(|s| s.to_string()),
layout: layout.map(|s| s.to_string()),
model: model.map(|s| s.to_string()),
options: options.map(|s| s.to_string()),
};
send_msg(msg).unwrap();
}
/// A mouse button.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum MouseButton {
/// The left mouse button.
/// The left mouse button
Left = 0x110,
/// The right mouse button.
Right,
/// The middle mouse button, pressed usually by clicking the scroll wheel.
Middle,
///
Side,
///
Extra,
///
Forward,
///
Back,
/// The right mouse button
Right = 0x111,
/// The middle mouse button
Middle = 0x112,
/// The side mouse button
Side = 0x113,
/// The extra mouse button
Extra = 0x114,
/// The forward mouse button
Forward = 0x115,
/// The backward mouse button
Back = 0x116,
}
/// The edge on which you want things to trigger.
#[derive(Debug, Hash, serde::Serialize, serde::Deserialize, Clone, Copy, PartialEq, Eq)]
/// Keyboard modifiers.
#[repr(i32)]
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, TryFromPrimitive)]
pub enum Mod {
/// The shift key
Shift = 1,
/// The ctrl key
Ctrl,
/// The alt key
Alt,
/// The super key, aka meta, win, mod4
Super,
}
/// Press or release.
#[repr(i32)]
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, TryFromPrimitive)]
pub enum MouseEdge {
/// Actions will be triggered on button press.
Press,
/// Actions will be triggered on button release.
/// Perform actions on button press
Press = 1,
/// Perform actions on button release
Release,
}
impl From<char> for KeyIntOrString {
fn from(value: char) -> Self {
Self::String(value.to_string())
}
/// A struct that lets you define xkeyboard config options.
///
/// See `xkeyboard-config(7)` for more information.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)]
pub struct XkbConfig {
/// Files of rules to be used for keyboard mapping composition
pub rules: Option<&'static str>,
/// Name of the model of your keyboard type
pub model: Option<&'static str>,
/// Layout(s) you intend to use
pub layout: Option<&'static str>,
/// Variant(s) of the layout you intend to use
pub variant: Option<&'static str>,
/// Extra xkb configuration options
pub options: Option<&'static str>,
}
impl From<u32> for KeyIntOrString {
fn from(value: u32) -> Self {
Self::Int(value)
}
/// The `Input` struct.
///
/// This struct contains methods that allow you to set key- and mousebinds,
/// change xkeyboard and libinput settings, and change the keyboard's repeat rate.
#[derive(Debug, Clone)]
pub struct Input {
channel: Channel,
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
}
impl From<Keysym> for KeyIntOrString {
fn from(value: Keysym) -> Self {
Self::Int(value.raw())
impl Input {
pub(crate) fn new(
channel: Channel,
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
) -> Self {
Self {
channel,
fut_sender,
}
}
}
/// A modifier key.
#[derive(Debug, PartialEq, Eq, Copy, Clone, serde::Serialize, serde::Deserialize)]
pub enum Modifier {
/// The shift key.
Shift,
/// The control key.
Ctrl,
/// The alt key.
Alt,
/// The super key.
fn create_input_client(&self) -> InputServiceClient<Channel> {
InputServiceClient::new(self.channel.clone())
}
/// Set a keybind.
///
/// This is also known as the Windows key, meta, or Mod4 for those coming from Xorg.
Super,
/// If called with an already set keybind, it gets replaced.
///
/// You must supply:
/// - `mods`: A list of [`Mod`]s. These must be held down for the keybind to trigger.
/// - `key`: The key that needs to be pressed. This can be anything that implements the [Key] trait:
/// - `char`
/// - `&str` and `String`: This is any name from
/// [xkbcommon-keysyms.h](https://xkbcommon.org/doc/current/xkbcommon-keysyms_8h.html)
/// without the `XKB_KEY_` prefix.
/// - `u32`: The numerical key code from the website above.
/// - A [`keysym`][Keysym] from the [`xkbcommon`] re-export.
/// - `action`: A closure that will be run when the keybind is triggered.
/// - Currently, any captures must be both `Send` and `'static`. If you want to mutate
/// something, consider using channels or [`Box::leak`].
///
/// # Examples
///
/// ```
/// use pinnacle_api::input::Mod;
///
/// // Set `Super + Shift + c` to close the focused window
/// input.keybind([Mod::Super, Mod::Shift], 'c', || {
/// if let Some(win) = window.get_focused() {
/// win.close();
/// }
/// });
///
/// // With a string key
/// input.keybind([], "BackSpace", || { /* ... */ });
///
/// // With a numeric key
/// input.keybind([], 65, || { /* ... */ }); // 65 = 'A'
///
/// // With a `Keysym`
/// input.keybind([], pinnacle_api::xkbcommon::xkb::Keysym::Return, || { /* ... */ });
/// ```
pub fn keybind(
&self,
mods: impl IntoIterator<Item = Mod>,
key: impl Key + Send + 'static,
mut action: impl FnMut() + Send + 'static,
) {
let mut client = self.create_input_client();
let modifiers = mods.into_iter().map(|modif| modif as i32).collect();
self.fut_sender
.unbounded_send(
async move {
let mut stream = client
.set_keybind(SetKeybindRequest {
modifiers,
key: Some(input::v0alpha1::set_keybind_request::Key::RawCode(
key.into_keysym().raw(),
)),
})
.await
.unwrap()
.into_inner();
while let Some(Ok(_response)) = stream.next().await {
action();
}
}
.boxed(),
)
.unwrap();
}
/// Set a mousebind.
///
/// If called with an already set mousebind, it gets replaced.
///
/// You must supply:
/// - `mods`: A list of [`Mod`]s. These must be held down for the keybind to trigger.
/// - `button`: A [`MouseButton`].
/// - `edge`: A [`MouseEdge`]. This allows you to trigger the bind on either mouse press or release.
/// - `action`: A closure that will be run when the mousebind is triggered.
/// - Currently, any captures must be both `Send` and `'static`. If you want to mutate
/// something, consider using channels or [`Box::leak`].
///
/// # Examples
///
/// ```
/// use pinnacle_api::input::{Mod, MouseButton, MouseEdge};
///
/// // Set `Super + left click` to start moving a window
/// input.mousebind([Mod::Super], MouseButton::Left, MouseEdge::Press, || {
/// window.begin_move(MouseButton::Press);
/// });
/// ```
pub fn mousebind(
&self,
mods: impl IntoIterator<Item = Mod>,
button: MouseButton,
edge: MouseEdge,
mut action: impl FnMut() + 'static + Send,
) {
let mut client = self.create_input_client();
let modifiers = mods.into_iter().map(|modif| modif as i32).collect();
self.fut_sender
.unbounded_send(
async move {
let mut stream = client
.set_mousebind(SetMousebindRequest {
modifiers,
button: Some(button as u32),
edge: Some(edge as i32),
})
.await
.unwrap()
.into_inner();
while let Some(Ok(_response)) = stream.next().await {
action();
}
}
.boxed(),
)
.unwrap();
}
/// Set the xkeyboard config.
///
/// This allows you to set several xkeyboard options like `layout` and `rules`.
///
/// See `xkeyboard-config(7)` for more information.
///
/// # Examples
///
/// ```
/// use pinnacle_api::input::XkbConfig;
///
/// input.set_xkb_config(Xkbconfig {
/// layout: Some("us,fr,ge"),
/// options: Some("ctrl:swapcaps,caps:shift"),
/// ..Default::default()
/// });
/// ```
pub fn set_xkb_config(&self, xkb_config: XkbConfig) {
let mut client = self.create_input_client();
block_on(client.set_xkb_config(SetXkbConfigRequest {
rules: xkb_config.rules.map(String::from),
variant: xkb_config.variant.map(String::from),
layout: xkb_config.layout.map(String::from),
model: xkb_config.model.map(String::from),
options: xkb_config.options.map(String::from),
}))
.unwrap();
}
/// Set the keyboard's repeat rate.
///
/// This allows you to set the time between holding down a key and it repeating
/// as well as the time between each repeat.
///
/// Units are in milliseconds.
///
/// # Examples
///
/// ```
/// // Set keyboard to repeat after holding down for half a second,
/// // and repeat once every 25ms (40 times a second)
/// input.set_repeat_rate(25, 500);
/// ```
pub fn set_repeat_rate(&self, rate: i32, delay: i32) {
let mut client = self.create_input_client();
block_on(client.set_repeat_rate(SetRepeatRateRequest {
rate: Some(rate),
delay: Some(delay),
}))
.unwrap();
}
/// Set a libinput setting.
///
/// From [freedesktop.org](https://www.freedesktop.org/wiki/Software/libinput/):
/// > libinput is a library to handle input devices in Wayland compositors
///
/// As such, this method allows you to set various settings related to input devices.
/// This includes things like pointer acceleration and natural scrolling.
///
/// See [`LibinputSetting`] for all the settings you can change.
///
/// Note: currently Pinnacle applies anything set here to *every* device, regardless of what it
/// actually is. This will be fixed in the future.
///
/// # Examples
///
/// ```
/// use pinnacle_api::input::libinput::*;
///
/// // Set pointer acceleration to flat
/// input.set_libinput_setting(LibinputSetting::AccelProfile(AccelProfile::Flat));
///
/// // Enable natural scrolling (reverses scroll direction; usually used with trackpads)
/// input.set_libinput_setting(LibinputSetting::NaturalScroll(true));
/// ```
pub fn set_libinput_setting(&self, setting: LibinputSetting) {
let mut client = self.create_input_client();
let setting = match setting {
LibinputSetting::AccelProfile(profile) => Setting::AccelProfile(profile as i32),
LibinputSetting::AccelSpeed(speed) => Setting::AccelSpeed(speed),
LibinputSetting::CalibrationMatrix(matrix) => {
Setting::CalibrationMatrix(CalibrationMatrix {
matrix: matrix.to_vec(),
})
}
LibinputSetting::ClickMethod(method) => Setting::ClickMethod(method as i32),
LibinputSetting::DisableWhileTyping(disable) => Setting::DisableWhileTyping(disable),
LibinputSetting::LeftHanded(enable) => Setting::LeftHanded(enable),
LibinputSetting::MiddleEmulation(enable) => Setting::MiddleEmulation(enable),
LibinputSetting::RotationAngle(angle) => Setting::RotationAngle(angle),
LibinputSetting::ScrollButton(button) => Setting::RotationAngle(button),
LibinputSetting::ScrollButtonLock(enable) => Setting::ScrollButtonLock(enable),
LibinputSetting::ScrollMethod(method) => Setting::ScrollMethod(method as i32),
LibinputSetting::NaturalScroll(enable) => Setting::NaturalScroll(enable),
LibinputSetting::TapButtonMap(map) => Setting::TapButtonMap(map as i32),
LibinputSetting::TapDrag(enable) => Setting::TapDrag(enable),
LibinputSetting::TapDragLock(enable) => Setting::TapDragLock(enable),
LibinputSetting::Tap(enable) => Setting::Tap(enable),
};
block_on(client.set_libinput_setting(SetLibinputSettingRequest {
setting: Some(setting),
}))
.unwrap();
}
}
/// A trait that designates anything that can be converted into a [`Keysym`].
pub trait Key {
/// Convert this into a [`Keysym`].
fn into_keysym(self) -> Keysym;
}
impl Key for Keysym {
fn into_keysym(self) -> Keysym {
self
}
}
impl Key for char {
fn into_keysym(self) -> Keysym {
Keysym::from_char(self)
}
}
impl Key for &str {
fn into_keysym(self) -> Keysym {
xkbcommon::xkb::keysym_from_name(self, xkbcommon::xkb::KEYSYM_NO_FLAGS)
}
}
impl Key for String {
fn into_keysym(self) -> Keysym {
xkbcommon::xkb::keysym_from_name(&self, xkbcommon::xkb::KEYSYM_NO_FLAGS)
}
}
impl Key for u32 {
fn into_keysym(self) -> Keysym {
Keysym::from(self)
}
}

View file

@ -1,40 +1,35 @@
//! Libinput settings.
//! Types for libinput configuration.
use crate::{msg::Msg, send_msg};
/// Set a libinput setting.
///
/// This takes a [`LibinputSetting`] containing what you want set.
pub fn set(setting: LibinputSetting) {
let msg = Msg::SetLibinputSetting(setting);
send_msg(msg).unwrap();
}
/// The acceleration profile.
#[derive(Debug, PartialEq, Copy, Clone, serde::Serialize)]
/// Pointer acceleration profile
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AccelProfile {
/// Flat pointer acceleration.
Flat,
/// Adaptive pointer acceleration.
/// A flat acceleration profile.
///
/// This is the default for most devices.
/// Pointer motion is accelerated by a constant (device-specific) factor, depending on the current speed.
Flat = 1,
/// An adaptive acceleration profile.
///
/// Pointer acceleration depends on the input speed. This is the default profile for most devices.
Adaptive,
}
/// The click method for a touchpad.
#[derive(Debug, PartialEq, Copy, Clone, serde::Serialize)]
/// The click method defines when to generate software-emulated buttons, usually on a device
/// that does not have a specific physical button available.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ClickMethod {
/// Use software-button areas to generate button events.
ButtonAreas,
ButtonAreas = 1,
/// The number of fingers decides which button press to generate.
Clickfinger,
}
/// The scroll method for a touchpad.
#[derive(Debug, PartialEq, Copy, Clone, serde::Serialize)]
/// The scroll method of a device selects when to generate scroll axis events instead of pointer motion events.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ScrollMethod {
/// Never send scroll events.
NoScroll,
/// Never send scroll events instead of pointer motion events.
///
/// This has no effect on events generated by scroll wheels.
NoScroll = 1,
/// Send scroll events when two fingers are logically down on the device.
TwoFinger,
/// Send scroll events when a finger moves along the bottom or right edge of a device.
@ -43,63 +38,48 @@ pub enum ScrollMethod {
OnButtonDown,
}
/// The mapping between finger count and button event for a touchpad.
#[derive(Debug, PartialEq, Copy, Clone, serde::Serialize)]
/// Map 1/2/3 finger tips to buttons.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TapButtonMap {
/// 1/2/3 finger tap is mapped to left/right/middle click.
/// 1/2/3 finger tap maps to left/right/middle
LeftRightMiddle,
/// 1/2/3 finger tap is mapped to left/middle/right click.
/// 1/2/3 finger tap maps to left/middle/right
LeftMiddleRight,
}
/// Libinput settings.
#[derive(Debug, PartialEq, Copy, Clone, serde::Serialize)]
/// Possible settings for libinput.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LibinputSetting {
/// Set the acceleration profile.
/// Set the pointer acceleration profile
AccelProfile(AccelProfile),
/// Set the acceleration speed.
///
/// This should be a float from -1.0 to 1.0.
/// Set pointer acceleration speed
AccelSpeed(f64),
/// Set the calibration matrix.
/// Set the calibration matrix
CalibrationMatrix([f32; 6]),
/// Set the click method.
///
/// The click method defines when to generate software-emulated buttons, usually on a device
/// that does not have a specific physical button available.
/// Set the [`ClickMethod`]
ClickMethod(ClickMethod),
/// Set whether or not the device will be disabled while typing.
DisableWhileTypingEnabled(bool),
/// Set device left-handedness.
/// Set whether the device gets disabled while typing
DisableWhileTyping(bool),
/// Set left handed mode
LeftHanded(bool),
/// Set whether or not the middle click can be emulated.
MiddleEmulationEnabled(bool),
/// Set the rotation angle of a device.
/// Allow or disallow middle mouse button emulation
MiddleEmulation(bool),
/// Set the rotation angle
RotationAngle(u32),
/// Set the scroll method.
ScrollMethod(ScrollMethod),
/// Set whether or not natural scroll is enabled.
///
/// This reverses the direction of scrolling and is mainly used with touchpads.
NaturalScrollEnabled(bool),
/// Set the scroll button.
/// Set the scroll button
ScrollButton(u32),
/// Set the tap button map,
///
/// This determines whether taps with 2 and 3 fingers register as right and middle clicks or
/// the reverse.
/// Set whether the scroll button should be a drag or toggle
ScrollButtonLock(bool),
/// Set the [`ScrollMethod`]
ScrollMethod(ScrollMethod),
/// Enable or disable natural scrolling
NaturalScroll(bool),
/// Set the [`TapButtonMap`]
TapButtonMap(TapButtonMap),
/// Set whether or not tap-and-drag is enabled.
///
/// When enabled, a single-finger tap immediately followed by a finger down results in
/// a button down event, and subsequent finger motion thus triggers a drag.
/// The button is released on finger up.
TapDragEnabled(bool),
/// Set whether or not tap drag lock is enabled.
///
/// When enabled, a finger may be lifted and put back on the touchpad within a timeout and the drag process
/// continues. When disabled, lifting the finger during a tap-and-drag will immediately stop the drag.
TapDragLockEnabled(bool),
/// Set whether or not tap-to-click is enabled.
TapEnabled(bool),
/// Enable or disable tap-to-drag
TapDrag(bool),
/// Enable or disable a timeout where lifting a finger off the device will not stop dragging
TapDragLock(bool),
/// Enable or disable tap-to-click
Tap(bool),
}

View file

@ -1,234 +1,202 @@
//! 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 to to interface with the Pinnacle compositor and configure various aspects
//! like input and the tag system.
//!
//! # Configuration
//!
//! ## 1. Create a cargo project
//! To create your own Rust config, create a Cargo project in `~/.config/pinnacle`.
//!
//! ## 2. Create `metaconfig.toml`
//! Then, create a file named `metaconfig.toml`. This is the file Pinnacle will use to determine
//! what to run, kill and reload-config keybinds, an optional socket directory, and any environment
//! variables to give the config client.
//!
//! In `metaconfig.toml`, put the following:
//! ```toml
//! # `command` will tell Pinnacle to run `cargo run` in your config directory.
//! # You can add stuff like "--release" here if you want to.
//! command = ["cargo", "run"]
//!
//! # You must define a keybind to reload your config if it crashes, otherwise you'll get stuck if
//! # the Lua config doesn't kick in properly.
//! reload_keybind = { modifiers = ["Ctrl", "Alt"], key = "r" }
//!
//! # Similarly, you must define a keybind to kill Pinnacle.
//! kill_keybind = { modifiers = ["Ctrl", "Alt", "Shift"], key = "escape" }
//!
//! # You can specify an optional socket directory if you need to place the socket Pinnacle will
//! # use for configuration in a different place.
//! # socket_dir = "your/dir/here"
//!
//! # If you need to set any environment variables for the config process, you can do so here if
//! # you don't want to do it in the config itself.
//! [envs]
//! # key = "value"
//! ```
//!
//! ## 3. Set up dependencies
//! In your `Cargo.toml`, add a dependency to `pinnacle-api`:
//!
//! ```toml
//! # Cargo.toml
//!
//! [dependencies]
//! pinnacle-api = { git = "https://github.com/pinnacle-comp/pinnacle" }
//! ```
//!
//! ## 4. Set up the main function
//! In `main.rs`, change `fn main()` to `async fn main()` and annotate it with the
//! [`pinnacle_api::config`][`crate::config`] macro. Pass in the identifier you want to bind the
//! config modules to:
//!
//! ```
//! use pinnacle_api::ApiModules;
//!
//! #[pinnacle_api::config(modules)]
//! async fn main() {
//! // `modules` is now available in the function body.
//! // You can deconstruct `ApiModules` to get all the config structs.
//! let ApiModules {
//! pinnacle,
//! process,
//! window,
//! input,
//! output,
//! tag,
//! } = modules;
//! }
//! ```
//!
//! ## 5. Begin crafting your config!
//! You can peruse the documentation for things to configure.
use std::sync::OnceLock;
use futures::{
channel::mpsc::UnboundedReceiver, future::BoxFuture, stream::FuturesUnordered, StreamExt,
};
use input::Input;
use output::Output;
use pinnacle::Pinnacle;
use process::Process;
use tag::Tag;
use tonic::transport::{Endpoint, Uri};
use tower::service_fn;
use window::Window;
pub mod input;
mod msg;
pub mod output;
pub mod pinnacle;
pub mod process;
pub mod tag;
pub mod util;
pub mod window;
/// The xkbcommon crate, re-exported for your convenience.
pub use pinnacle_api_macros::config;
pub use tokio;
pub use xkbcommon;
/// The prelude for the Pinnacle API.
static PINNACLE: OnceLock<Pinnacle> = OnceLock::new();
static PROCESS: OnceLock<Process> = OnceLock::new();
static WINDOW: OnceLock<Window> = OnceLock::new();
static INPUT: OnceLock<Input> = OnceLock::new();
static OUTPUT: OnceLock<Output> = OnceLock::new();
static TAG: OnceLock<Tag> = OnceLock::new();
/// A struct containing static references to all of the configuration structs.
#[derive(Debug, Clone, Copy)]
pub struct ApiModules {
/// The [`Pinnacle`] struct
pub pinnacle: &'static Pinnacle,
/// The [`Process`] struct
pub process: &'static Process,
/// The [`Window`] struct
pub window: &'static Window,
/// The [`Input`] struct
pub input: &'static Input,
/// The [`Output`] struct
pub output: &'static Output,
/// The [`Tag`] struct
pub tag: &'static Tag,
}
/// Connects to Pinnacle and builds the configuration structs.
///
/// This contains useful imports that you will likely need.
/// To that end, you can do `use pinnacle_api::prelude::*` to
/// prevent your config file from being cluttered with imports.
pub mod prelude {
pub use crate::input::libinput::*;
pub use crate::input::Modifier;
pub use crate::input::MouseButton;
pub use crate::input::MouseEdge;
pub use crate::output::AlignmentHorizontal;
pub use crate::output::AlignmentVertical;
pub use crate::tag::Layout;
pub use crate::window::rules::WindowRule;
pub use crate::window::rules::WindowRuleCondition;
pub use crate::window::FloatingOrTiled;
pub use crate::window::FullscreenOrMaximized;
}
/// This function is inserted at the top of your config through the [`config`] macro.
/// You should use that macro instead of this function directly.
pub async fn connect(
) -> Result<(ApiModules, UnboundedReceiver<BoxFuture<'static, ()>>), Box<dyn std::error::Error>> {
let channel = Endpoint::try_from("http://[::]:50051")? // port doesn't matter, we use a unix socket
.connect_with_connector(service_fn(|_: Uri| {
tokio::net::UnixStream::connect(
std::env::var("PINNACLE_GRPC_SOCKET")
.expect("PINNACLE_GRPC_SOCKET was not set; is Pinnacle running?"),
)
}))
.await?;
use std::{
collections::{hash_map::Entry, HashMap},
convert::Infallible,
io::{Read, Write},
os::unix::net::UnixStream,
path::PathBuf,
sync::{atomic::AtomicU32, Mutex, OnceLock},
};
let (fut_sender, fut_recv) = futures::channel::mpsc::unbounded::<BoxFuture<()>>();
use msg::{Args, CallbackId, IncomingMsg, Msg, Request, RequestResponse};
let output = Output::new(channel.clone(), fut_sender.clone());
use crate::msg::RequestId;
let pinnacle = PINNACLE.get_or_init(|| Pinnacle::new(channel.clone()));
let process = PROCESS.get_or_init(|| Process::new(channel.clone(), fut_sender.clone()));
let window = WINDOW.get_or_init(|| Window::new(channel.clone()));
let input = INPUT.get_or_init(|| Input::new(channel.clone(), fut_sender.clone()));
let tag = TAG.get_or_init(|| Tag::new(channel.clone(), fut_sender.clone()));
let output = OUTPUT.get_or_init(|| output);
static STREAM: OnceLock<Mutex<UnixStream>> = OnceLock::new();
lazy_static::lazy_static! {
static ref UNREAD_CALLBACK_MSGS: Mutex<HashMap<CallbackId, IncomingMsg>> = Mutex::new(HashMap::new());
static ref UNREAD_REQUEST_MSGS: Mutex<HashMap<RequestId, IncomingMsg>> = Mutex::new(HashMap::new());
}
static REQUEST_ID_COUNTER: AtomicU32 = AtomicU32::new(0);
fn send_msg(msg: Msg) -> anyhow::Result<()> {
let mut msg = rmp_serde::encode::to_vec_named(&msg)?;
let mut msg_len = (msg.len() as u32).to_ne_bytes();
let mut stream = STREAM.get().unwrap().lock().unwrap();
stream.write_all(msg_len.as_mut_slice())?;
stream.write_all(msg.as_mut_slice())?;
Ok(())
}
fn read_msg(request_id: Option<RequestId>) -> IncomingMsg {
loop {
if let Some(request_id) = request_id {
if let Some(msg) = UNREAD_REQUEST_MSGS.lock().unwrap().remove(&request_id) {
return msg;
}
}
let mut stream = STREAM.get().unwrap().lock().unwrap();
let mut msg_len_bytes = [0u8; 4];
stream.read_exact(msg_len_bytes.as_mut_slice()).unwrap();
let msg_len = u32::from_ne_bytes(msg_len_bytes);
let mut msg_bytes = vec![0u8; msg_len as usize];
stream.read_exact(msg_bytes.as_mut_slice()).unwrap();
let incoming_msg: IncomingMsg = rmp_serde::from_slice(msg_bytes.as_slice()).unwrap();
if let Some(request_id) = request_id {
match &incoming_msg {
IncomingMsg::CallCallback {
callback_id,
args: _,
} => {
UNREAD_CALLBACK_MSGS
.lock()
.unwrap()
.insert(*callback_id, incoming_msg);
}
IncomingMsg::RequestResponse {
request_id: req_id,
response: _,
} => {
if req_id != &request_id {
UNREAD_REQUEST_MSGS
.lock()
.unwrap()
.insert(*req_id, incoming_msg);
} else {
return incoming_msg;
}
}
}
} else {
return incoming_msg;
}
}
}
fn request(request: Request) -> RequestResponse {
use std::sync::atomic::Ordering;
let request_id = REQUEST_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
let msg = Msg::Request {
request_id: RequestId(request_id),
request,
};
send_msg(msg).unwrap(); // TODO: propogate
let IncomingMsg::RequestResponse {
request_id: _,
response,
} = read_msg(Some(RequestId(request_id)))
else {
unreachable!()
let modules = ApiModules {
pinnacle,
process,
window,
input,
output,
tag,
};
response
Ok((modules, fut_recv))
}
/// Connect to Pinnacle. This needs to be called before you begin calling config functions.
/// Listen to Pinnacle for incoming messages.
///
/// This will open up a connection to the Unix socket at `$PINNACLE_SOCKET`,
/// which should be set when you start the compositor.
pub fn connect() -> anyhow::Result<()> {
STREAM
.set(Mutex::new(
UnixStream::connect(PathBuf::from(
std::env::var("PINNACLE_SOCKET").unwrap_or("/tmp/pinnacle_socket".to_string()),
))
.unwrap(),
))
.unwrap();
Ok(())
}
/// Begin listening for messages coming from Pinnacle.
/// This will run all futures returned by configuration methods that take in callbacks in order to
/// call them.
///
/// This needs to be called at the very end of your `setup` function.
pub fn listen(mut callback_vec: CallbackVec) -> Infallible {
loop {
let mut unread_callback_msgs = UNREAD_CALLBACK_MSGS.lock().unwrap();
/// This function is inserted at the end of your config through the [`config`] macro.
/// You should use the macro instead of this function directly.
pub async fn listen(
fut_recv: UnboundedReceiver<BoxFuture<'static, ()>>, // api_modules: ApiModules<'a>,
) {
let mut future_set = FuturesUnordered::<
BoxFuture<(
Option<BoxFuture<()>>,
Option<UnboundedReceiver<BoxFuture<()>>>,
)>,
>::new();
for cb_id in unread_callback_msgs.keys().copied().collect::<Vec<_>>() {
let Entry::Occupied(entry) = unread_callback_msgs.entry(cb_id) else {
unreachable!();
};
let IncomingMsg::CallCallback { callback_id, args } = entry.remove() else {
unreachable!();
};
future_set.push(Box::pin(async move {
let (fut, stream) = fut_recv.into_future().await;
(fut, Some(stream))
}));
// Take the callback out and replace it with a dummy callback
// to allow callback_vec to be used mutably below.
let mut callback = std::mem::replace(
&mut callback_vec.callbacks[callback_id.0 as usize],
Box::new(|_, _| {}),
);
callback(args, &mut callback_vec);
// Put it back.
callback_vec.callbacks[callback_id.0 as usize] = callback;
while let Some((fut, stream)) = future_set.next().await {
if let Some(fut) = fut {
future_set.push(Box::pin(async move {
fut.await;
(None, None)
}));
}
if let Some(stream) = stream {
future_set.push(Box::pin(async move {
let (fut, stream) = stream.into_future().await;
(fut, Some(stream))
}))
}
let incoming_msg = read_msg(None);
let IncomingMsg::CallCallback { callback_id, args } = incoming_msg else {
unreachable!();
};
let mut callback = std::mem::replace(
&mut callback_vec.callbacks[callback_id.0 as usize],
Box::new(|_, _| {}),
);
callback(args, &mut callback_vec);
callback_vec.callbacks[callback_id.0 as usize] = callback;
}
}
/// Quit Pinnacle.
pub fn quit() {
send_msg(Msg::Quit).unwrap();
}
/// A wrapper around a vector that holds all of your callbacks.
///
/// You will need to create this before you can start calling config functions
/// that require callbacks.
///
/// Because your callbacks can capture things, we need a non-static way to hold them.
/// That's where this struct comes in.
///
/// Every function that needs you to provide a callback will also need you to
/// provide a `&mut CallbackVec`. This will insert the callback for use in [`listen`].
///
/// Additionally, all callbacks will also take in `&mut CallbackVec`. This is so you can
/// call functions that need it inside of other callbacks.
///
/// At the end of your config, you will need to call [`listen`] to begin listening for
/// messages from Pinnacle that will call your callbacks. Here, you must in pass your
/// `CallbackVec`.
#[derive(Default)]
pub struct CallbackVec<'a> {
#[allow(clippy::type_complexity)]
pub(crate) callbacks: Vec<Box<dyn FnMut(Option<Args>, &mut CallbackVec) + 'a>>,
}
impl<'a> CallbackVec<'a> {
/// Create a new, empty `CallbackVec`.
pub fn new() -> Self {
Default::default()
}
}

View file

@ -1,290 +0,0 @@
use std::num::NonZeroU32;
use crate::{
input::{libinput::LibinputSetting, Modifier, MouseEdge},
output::OutputName,
tag::{Layout, TagId},
window::{FloatingOrTiled, FullscreenOrMaximized, WindowId},
};
#[derive(Debug, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize, Clone, Copy)]
pub(crate) struct CallbackId(pub u32);
#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub(crate) struct WindowRuleCondition {
/// This condition is met when any of the conditions provided is met.
#[serde(default)]
pub cond_any: Option<Vec<WindowRuleCondition>>,
/// This condition is met when all of the conditions provided are met.
#[serde(default)]
pub cond_all: Option<Vec<WindowRuleCondition>>,
/// This condition is met when the class matches.
#[serde(default)]
pub class: Option<Vec<String>>,
/// This condition is met when the title matches.
#[serde(default)]
pub title: Option<Vec<String>>,
/// This condition is met when the tag matches.
#[serde(default)]
pub tag: Option<Vec<TagId>>,
}
#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub(crate) struct WindowRule {
/// Set the output the window will open on.
#[serde(default)]
pub output: Option<OutputName>,
/// Set the tags the output will have on open.
#[serde(default)]
pub tags: Option<Vec<TagId>>,
/// Set the window to floating or tiled on open.
#[serde(default)]
pub floating_or_tiled: Option<FloatingOrTiled>,
/// Set the window to fullscreen, maximized, or force it to neither.
#[serde(default)]
pub fullscreen_or_maximized: Option<FullscreenOrMaximized>,
/// Set the window's initial size.
#[serde(default)]
pub size: Option<(NonZeroU32, NonZeroU32)>,
/// Set the window's initial location. If the window is tiled, it will snap to this position
/// when set to floating.
#[serde(default)]
pub location: Option<(i32, i32)>,
}
#[derive(Debug, Hash, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct RequestId(pub u32);
#[derive(Debug, serde::Serialize)]
pub(crate) enum Msg {
// Input
SetKeybind {
key: KeyIntOrString,
modifiers: Vec<Modifier>,
callback_id: CallbackId,
},
SetMousebind {
modifiers: Vec<Modifier>,
button: u32,
edge: MouseEdge,
callback_id: CallbackId,
},
// Window management
CloseWindow {
window_id: WindowId,
},
SetWindowSize {
window_id: WindowId,
#[serde(default)]
width: Option<i32>,
#[serde(default)]
height: Option<i32>,
},
MoveWindowToTag {
window_id: WindowId,
tag_id: TagId,
},
ToggleTagOnWindow {
window_id: WindowId,
tag_id: TagId,
},
ToggleFloating {
window_id: WindowId,
},
ToggleFullscreen {
window_id: WindowId,
},
ToggleMaximized {
window_id: WindowId,
},
AddWindowRule {
cond: WindowRuleCondition,
rule: WindowRule,
},
WindowMoveGrab {
button: u32,
},
WindowResizeGrab {
button: u32,
},
// Tag management
ToggleTag {
tag_id: TagId,
},
SwitchToTag {
tag_id: TagId,
},
AddTags {
/// The name of the output you want these tags on.
output_name: OutputName,
tag_names: Vec<String>,
},
// TODO:
RemoveTags {
/// The name of the output you want these tags removed from.
tag_ids: Vec<TagId>,
},
SetLayout {
tag_id: TagId,
layout: Layout,
},
// Output management
ConnectForAllOutputs {
callback_id: CallbackId,
},
SetOutputLocation {
output_name: OutputName,
#[serde(default)]
x: Option<i32>,
#[serde(default)]
y: Option<i32>,
},
// Process management
/// Spawn a program with an optional callback.
Spawn {
command: Vec<String>,
#[serde(default)]
callback_id: Option<CallbackId>,
},
/// Spawn a program with an optional callback only if it isn't running.
SpawnOnce {
command: Vec<String>,
#[serde(default)]
callback_id: Option<CallbackId>,
},
SetEnv {
key: String,
value: String,
},
// Pinnacle management
/// Quit the compositor.
Quit,
// Input management
SetXkbConfig {
#[serde(default)]
rules: Option<String>,
#[serde(default)]
variant: Option<String>,
#[serde(default)]
layout: Option<String>,
#[serde(default)]
model: Option<String>,
#[serde(default)]
options: Option<String>,
},
SetLibinputSetting(LibinputSetting),
Request {
request_id: RequestId,
request: Request,
},
}
#[allow(clippy::enum_variant_names)]
#[derive(Debug, serde::Serialize, serde::Deserialize)]
/// Messages that require a server response, usually to provide some data.
pub(crate) enum Request {
// Windows
GetWindows,
GetWindowProps { window_id: WindowId },
// Outputs
GetOutputs,
GetOutputProps { output_name: String },
// Tags
GetTags,
GetTagProps { tag_id: TagId },
}
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
pub enum KeyIntOrString {
Int(u32),
String(String),
}
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
pub enum Args {
/// Send a message with lines from the spawned process.
Spawn {
#[serde(default)]
stdout: Option<String>,
#[serde(default)]
stderr: Option<String>,
#[serde(default)]
exit_code: Option<i32>,
#[serde(default)]
exit_msg: Option<String>,
},
ConnectForAllOutputs {
output_name: String,
},
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub(crate) enum IncomingMsg {
CallCallback {
callback_id: CallbackId,
#[serde(default)]
args: Option<Args>,
},
RequestResponse {
request_id: RequestId,
response: RequestResponse,
},
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub(crate) enum RequestResponse {
Window {
window_id: Option<WindowId>,
},
Windows {
window_ids: Vec<WindowId>,
},
WindowProps {
size: Option<(i32, i32)>,
loc: Option<(i32, i32)>,
class: Option<String>,
title: Option<String>,
focused: Option<bool>,
floating: Option<bool>,
fullscreen_or_maximized: Option<FullscreenOrMaximized>,
},
Output {
output_name: Option<String>,
},
Outputs {
output_names: Vec<String>,
},
OutputProps {
/// The make of the output.
make: Option<String>,
/// The model of the output.
model: Option<String>,
/// The location of the output in the space.
loc: Option<(i32, i32)>,
/// The resolution of the output.
res: Option<(i32, i32)>,
/// The refresh rate of the output.
refresh_rate: Option<i32>,
/// The size of the output, in millimeters.
physical_size: Option<(i32, i32)>,
/// Whether the output is focused or not.
focused: Option<bool>,
tag_ids: Option<Vec<TagId>>,
},
Tags {
tag_ids: Vec<TagId>,
},
TagProps {
active: Option<bool>,
name: Option<String>,
output_name: Option<String>,
},
}

View file

@ -1,301 +1,515 @@
//! Output management.
//!
//! An output is Pinnacle's terminology for a monitor.
//!
//! This module provides [`Output`], which allows you to get [`OutputHandle`]s for different
//! connected monitors and set them up.
use crate::{
msg::{Args, CallbackId, Msg, Request, RequestResponse},
request, send_msg,
tag::TagHandle,
CallbackVec,
use futures::{
channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture, FutureExt, StreamExt,
};
use pinnacle_api_defs::pinnacle::{
output::{
self,
v0alpha1::{
output_service_client::OutputServiceClient, ConnectForAllRequest, SetLocationRequest,
},
},
tag::v0alpha1::tag_service_client::TagServiceClient,
};
use tonic::transport::Channel;
/// A unique identifier for an output.
use crate::tag::TagHandle;
/// A struct that allows you to get handles to connected outputs and set them up.
///
/// An empty string represents an invalid output.
#[derive(Debug, Hash, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
pub(crate) struct OutputName(pub String);
/// Get an [`OutputHandle`] by its name.
///
/// `name` is the name of the port the output is plugged in to.
/// This is something like `HDMI-1` or `eDP-0`.
pub fn get_by_name(name: &str) -> Option<OutputHandle> {
let RequestResponse::Outputs { output_names } = request(Request::GetOutputs) else {
unreachable!()
};
output_names
.into_iter()
.find(|s| s == name)
.map(|s| OutputHandle(OutputName(s)))
/// See [`OutputHandle`] for more information.
#[derive(Debug, Clone)]
pub struct Output {
channel: Channel,
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
}
/// Get a handle to all connected outputs.
pub fn get_all() -> impl Iterator<Item = OutputHandle> {
let RequestResponse::Outputs { output_names } = request(Request::GetOutputs) else {
unreachable!()
};
output_names
.into_iter()
.map(|name| OutputHandle(OutputName(name)))
}
/// Get the currently focused output.
///
/// This is currently defined as the one with the cursor on it.
pub fn get_focused() -> Option<OutputHandle> {
let RequestResponse::Outputs { output_names } = request(Request::GetOutputs) else {
unreachable!()
};
output_names
.into_iter()
.map(|s| OutputHandle(OutputName(s)))
.find(|op| op.properties().focused == Some(true))
}
/// Connect a function to be run on all current and future outputs.
///
/// When called, `connect_for_all` will run `func` with all currently connected outputs.
/// If a new output is connected, `func` will also be called with it.
///
/// `func` takes in two parameters:
/// - `0`: An [`OutputHandle`] you can act on.
/// - `1`: A `&mut `[`CallbackVec`] for use in the closure.
///
/// This will *not* be called if it has already been called for a given connector.
/// This means turning your monitor off and on or unplugging and replugging it *to the same port*
/// won't trigger `func`. Plugging it in to a new port *will* trigger `func`.
/// This is intended to prevent duplicate setup.
///
/// Please note: this function will be run *after* Pinnacle processes your entire config.
/// For example, if you define tags in `func` but toggle them directly after `connect_for_all`,
/// nothing will happen as the tags haven't been added yet.
pub fn connect_for_all<'a, F>(mut func: F, callback_vec: &mut CallbackVec<'a>)
where
F: FnMut(OutputHandle, &mut CallbackVec) + 'a,
{
let args_callback = move |args: Option<Args>, callback_vec: &mut CallbackVec<'_>| {
if let Some(Args::ConnectForAllOutputs { output_name }) = args {
func(OutputHandle(OutputName(output_name)), callback_vec);
impl Output {
pub(crate) fn new(
channel: Channel,
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
) -> Self {
Self {
channel,
fut_sender,
}
};
}
let len = callback_vec.callbacks.len();
callback_vec.callbacks.push(Box::new(args_callback));
fn create_output_client(&self) -> OutputServiceClient<Channel> {
OutputServiceClient::new(self.channel.clone())
}
let msg = Msg::ConnectForAllOutputs {
callback_id: CallbackId(len as u32),
};
fn create_tag_client(&self) -> TagServiceClient<Channel> {
TagServiceClient::new(self.channel.clone())
}
send_msg(msg).unwrap();
/// Get a handle to all connected outputs.
///
/// # Examples
///
/// ```
/// let outputs = output.get_all();
/// ```
pub fn get_all(&self) -> impl Iterator<Item = OutputHandle> {
let mut client = self.create_output_client();
let tag_client = self.create_tag_client();
block_on(client.get(output::v0alpha1::GetRequest {}))
.unwrap()
.into_inner()
.output_names
.into_iter()
.map(move |name| OutputHandle {
client: client.clone(),
tag_client: tag_client.clone(),
name,
})
}
/// Get a handle to the output with the given name.
///
/// By "name", we mean the name of the connector the output is connected to.
///
/// # Examples
///
/// ```
/// let op = output.get_by_name("eDP-1")?;
/// let op2 = output.get_by_name("HDMI-2")?;
/// ```
pub fn get_by_name(&self, name: impl Into<String>) -> Option<OutputHandle> {
let name: String = name.into();
self.get_all().find(|output| output.name == name)
}
/// Get a handle to the focused output.
///
/// This is currently implemented as the one that has had the most recent pointer movement.
///
/// # Examples
///
/// ```
/// let op = output.get_focused()?;
/// ```
pub fn get_focused(&self) -> Option<OutputHandle> {
self.get_all()
.find(|output| matches!(output.props().focused, Some(true)))
}
/// Connect a closure to be run on all current and future outputs.
///
/// When called, `connect_for_all` will do two things:
/// 1. Immediately run `for_all` with all currently connected outputs.
/// 2. Create a future that will call `for_all` with any newly connected outputs.
///
/// Note that `for_all` will *not* run with outputs that have been unplugged and replugged.
/// This is to prevent duplicate setup. Instead, the compositor keeps track of any tags and
/// state the output had when unplugged and restores them on replug.
///
/// # Examples
///
/// ```
/// // Add tags 1-3 to all outputs and set tag "1" to active
/// output.connect_for_all(|op| {
/// let tags = tag.add(&op, ["1", "2", "3"]);
/// tags.next().unwrap().set_active(true);
/// });
/// ```
pub fn connect_for_all(&self, mut for_all: impl FnMut(OutputHandle) + Send + 'static) {
for output in self.get_all() {
for_all(output);
}
let mut client = self.create_output_client();
let tag_client = self.create_tag_client();
self.fut_sender
.unbounded_send(
async move {
let mut stream = client
.connect_for_all(ConnectForAllRequest {})
.await
.unwrap()
.into_inner();
while let Some(Ok(response)) = stream.next().await {
let Some(output_name) = response.output_name else {
continue;
};
let output = OutputHandle {
client: client.clone(),
tag_client: tag_client.clone(),
name: output_name,
};
for_all(output);
}
}
.boxed(),
)
.unwrap();
}
}
/// An output handle.
/// A handle to an output.
///
/// This is a handle to one of your monitors.
/// It serves to make it easier to deal with them, defining methods for getting properties and
/// helpers for things like positioning multiple monitors.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct OutputHandle(pub(crate) OutputName);
/// This allows you to manipulate outputs and get their properties.
#[derive(Clone, Debug)]
pub struct OutputHandle {
pub(crate) client: OutputServiceClient<Channel>,
pub(crate) tag_client: TagServiceClient<Channel>,
pub(crate) name: String,
}
/// Properties of an output.
pub struct OutputProperties {
/// The make.
pub make: Option<String>,
/// The model.
///
/// This is something like `27GL850` or whatever gibberish monitor manufacturers name their
/// displays.
pub model: Option<String>,
/// The location of the output in the global space.
pub loc: Option<(i32, i32)>,
/// The resolution of the output in pixels, where `res.0` is the width and `res.1` is the
/// height.
pub res: Option<(i32, i32)>,
/// The refresh rate of the output in millihertz.
///
/// For example, 60Hz is returned as 60000.
pub refresh_rate: Option<i32>,
/// The physical size of the output in millimeters.
pub physical_size: Option<(i32, i32)>,
/// Whether or not the output is focused.
pub focused: Option<bool>,
/// The tags on this output.
pub tags: Vec<TagHandle>,
/// The alignment to use for [`OutputHandle::set_loc_adj_to`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Alignment {
/// Set above, align left borders
TopAlignLeft,
/// Set above, align centers
TopAlignCenter,
/// Set above, align right borders
TopAlignRight,
/// Set below, align left borders
BottomAlignLeft,
/// Set below, align centers
BottomAlignCenter,
/// Set below, align right borders
BottomAlignRight,
/// Set to left, align top borders
LeftAlignTop,
/// Set to left, align centers
LeftAlignCenter,
/// Set to left, align bottom borders
LeftAlignBottom,
/// Set to right, align top borders
RightAlignTop,
/// Set to right, align centers
RightAlignCenter,
/// Set to right, align bottom borders
RightAlignBottom,
}
impl OutputHandle {
/// Get this output's name.
pub fn name(&self) -> String {
self.0 .0.clone()
/// Set the location of this output in the global space.
///
/// On startup, Pinnacle will lay out all connected outputs starting at (0, 0)
/// and going to the right, with their top borders aligned.
///
/// This method allows you to move outputs where necessary.
///
/// Note: If you leave space between two outputs when setting their locations,
/// the pointer will not be able to move between them.
///
/// # Examples
///
/// ```
/// // Assume two monitors in order, "DP-1" and "HDMI-1", with the following dimensions:
/// // - "DP-1": ┌─────┐
/// // │ │1920x1080
/// // └─────┘
/// // - "HDMI-1": ┌───────┐
/// // │ 2560x │
/// // │ 1440 │
/// // └───────┘
///
/// output.get_by_name("DP-1")?.set_location(0, 0);
/// output.get_by_name("HDMI-1")?.set_location(1920, -360);
///
/// // Results in:
/// // x=0 ┌───────┐y=-360
/// // y=0┌─────┤ │
/// // │DP-1 │HDMI-1 │
/// // └─────┴───────┘
/// // ^x=1920
/// ```
pub fn set_location(&self, x: impl Into<Option<i32>>, y: impl Into<Option<i32>>) {
let mut client = self.client.clone();
block_on(client.set_location(SetLocationRequest {
output_name: Some(self.name.clone()),
x: x.into(),
y: y.into(),
}))
.unwrap();
}
// TODO: Make OutputProperties an option, make non null fields not options
/// Get all properties of this output.
pub fn properties(&self) -> OutputProperties {
let RequestResponse::OutputProps {
make,
model,
loc,
res,
refresh_rate,
physical_size,
focused,
tag_ids,
} = request(Request::GetOutputProps {
output_name: self.0 .0.clone(),
})
else {
unreachable!()
/// Set this output adjacent to another one.
///
/// This is a helper method over [`OutputHandle::set_location`] to make laying out outputs
/// easier.
///
/// `alignment` is an [`Alignment`] of how you want this output to be placed.
/// For example, [`TopAlignLeft`][Alignment::TopAlignLeft] will place this output
/// above `other` and align the left borders.
/// Similarly, [`RightAlignCenter`][Alignment::RightAlignCenter] will place this output
/// to the right of `other` and align their centers.
///
/// # Examples
///
/// ```
/// use pinnacle_api::output::Alignment;
///
/// // Assume two monitors in order, "DP-1" and "HDMI-1", with the following dimensions:
/// // - "DP-1": ┌─────┐
/// // │ │1920x1080
/// // └─────┘
/// // - "HDMI-1": ┌───────┐
/// // │ 2560x │
/// // │ 1440 │
/// // └───────┘
///
/// output.get_by_name("DP-1")?.set_loc_adj_to(output.get_by_name("HDMI-1")?, Alignment::BottomAlignRight);
///
/// // Results in:
/// // ┌───────┐
/// // │ │
/// // │HDMI-1 │
/// // └──┬────┤
/// // │DP-1│
/// // └────┘
/// // Notice that "DP-1" now has the coordinates (2280, 1440) because "DP-1" is getting moved, not "HDMI-1".
/// // "HDMI-1" was placed at (1920, 0) during the compositor's initial output layout.
/// ```
pub fn set_loc_adj_to(&self, other: &OutputHandle, alignment: Alignment) {
let self_props = self.props();
let other_props = other.props();
// poor man's try {}
let attempt_set_loc = || -> Option<()> {
let other_x = other_props.x?;
let other_y = other_props.y?;
let other_width = other_props.pixel_width? as i32;
let other_height = other_props.pixel_height? as i32;
let self_width = self_props.pixel_width? as i32;
let self_height = self_props.pixel_height? as i32;
use Alignment::*;
let x: i32;
let y: i32;
if let TopAlignLeft | TopAlignCenter | TopAlignRight | BottomAlignLeft
| BottomAlignCenter | BottomAlignRight = alignment
{
if let TopAlignLeft | TopAlignCenter | TopAlignRight = alignment {
y = other_y - self_height;
} else {
// bottom
y = other_y + other_height;
}
match alignment {
TopAlignLeft | BottomAlignLeft => x = other_x,
TopAlignCenter | BottomAlignCenter => {
x = other_x + (other_width - self_width) / 2;
}
TopAlignRight | BottomAlignRight => x = other_x + (other_width - self_width),
_ => unreachable!(),
}
} else {
if let LeftAlignTop | LeftAlignCenter | LeftAlignBottom = alignment {
x = other_x - self_width;
} else {
x = other_x + other_width;
}
match alignment {
LeftAlignTop | RightAlignTop => y = other_y,
LeftAlignCenter | RightAlignCenter => {
y = other_y + (other_height - self_height) / 2;
}
LeftAlignBottom | RightAlignBottom => {
y = other_y + (other_height - self_height);
}
_ => unreachable!(),
}
}
self.set_location(Some(x), Some(y));
Some(())
};
attempt_set_loc();
}
/// Get all properties of this output.
///
/// # Examples
///
/// ```
/// use pinnacle_api::output::OutputProperties;
///
/// let OutputProperties {
/// make,
/// model,
/// x,
/// y,
/// pixel_width,
/// pixel_height,
/// refresh_rate,
/// physical_width,
/// physical_height,
/// focused,
/// tags,
/// } = output.get_focused()?.props();
/// ```
pub fn props(&self) -> OutputProperties {
let mut client = self.client.clone();
let response = block_on(
client.get_properties(output::v0alpha1::GetPropertiesRequest {
output_name: Some(self.name.clone()),
}),
)
.unwrap()
.into_inner();
OutputProperties {
make,
model,
loc,
res,
refresh_rate,
physical_size,
focused,
tags: tag_ids
.unwrap_or(vec![])
make: response.make,
model: response.model,
x: response.x,
y: response.y,
pixel_width: response.pixel_width,
pixel_height: response.pixel_height,
refresh_rate: response.refresh_rate,
physical_width: response.physical_width,
physical_height: response.physical_height,
focused: response.focused,
tags: response
.tag_ids
.into_iter()
.map(TagHandle)
.map(|id| TagHandle {
client: self.tag_client.clone(),
output_client: self.client.clone(),
id,
})
.collect(),
}
}
/// Add tags with the given `names` to this output.
pub fn add_tags(&self, names: &[&str]) {
crate::tag::add(self, names);
}
// TODO: make a macro for the following or something
/// Set this output's location in the global space.
pub fn set_loc(&self, x: Option<i32>, y: Option<i32>) {
let msg = Msg::SetOutputLocation {
output_name: self.0.clone(),
x,
y,
};
send_msg(msg).unwrap();
}
/// Set this output's location to the right of `other`.
/// Get this output's make.
///
/// It will be aligned vertically based on the given `alignment`.
pub fn set_loc_right_of(&self, other: &OutputHandle, alignment: AlignmentVertical) {
self.set_loc_horizontal(other, LeftOrRight::Right, alignment);
/// Shorthand for `self.props().make`.
pub fn make(&self) -> Option<String> {
self.props().make
}
/// Set this output's location to the left of `other`.
/// Get this output's model.
///
/// It will be aligned vertically based on the given `alignment`.
pub fn set_loc_left_of(&self, other: &OutputHandle, alignment: AlignmentVertical) {
self.set_loc_horizontal(other, LeftOrRight::Left, alignment);
/// Shorthand for `self.props().make`.
pub fn model(&self) -> Option<String> {
self.props().model
}
/// Set this output's location to the top of `other`.
/// Get this output's x position in the global space.
///
/// It will be aligned horizontally based on the given `alignment`.
pub fn set_loc_top_of(&self, other: &OutputHandle, alignment: AlignmentHorizontal) {
self.set_loc_vertical(other, TopOrBottom::Top, alignment);
/// Shorthand for `self.props().x`.
pub fn x(&self) -> Option<i32> {
self.props().x
}
/// Set this output's location to the bottom of `other`.
/// Get this output's y position in the global space.
///
/// It will be aligned horizontally based on the given `alignment`.
pub fn set_loc_bottom_of(&self, other: &OutputHandle, alignment: AlignmentHorizontal) {
self.set_loc_vertical(other, TopOrBottom::Bottom, alignment);
/// Shorthand for `self.props().y`.
pub fn y(&self) -> Option<i32> {
self.props().y
}
fn set_loc_horizontal(
&self,
other: &OutputHandle,
left_or_right: LeftOrRight,
alignment: AlignmentVertical,
) {
let op1_props = self.properties();
let op2_props = other.properties();
let (Some(_self_loc), Some(self_res), Some(other_loc), Some(other_res)) =
(op1_props.loc, op1_props.res, op2_props.loc, op2_props.res)
else {
return;
};
let x = match left_or_right {
LeftOrRight::Left => other_loc.0 - self_res.0,
LeftOrRight::Right => other_loc.0 + self_res.0,
};
let y = match alignment {
AlignmentVertical::Top => other_loc.1,
AlignmentVertical::Center => other_loc.1 + (other_res.1 - self_res.1) / 2,
AlignmentVertical::Bottom => other_loc.1 + (other_res.1 - self_res.1),
};
self.set_loc(Some(x), Some(y));
/// Get this output's screen width in pixels.
///
/// Shorthand for `self.props().pixel_width`.
pub fn pixel_width(&self) -> Option<u32> {
self.props().pixel_width
}
fn set_loc_vertical(
&self,
other: &OutputHandle,
top_or_bottom: TopOrBottom,
alignment: AlignmentHorizontal,
) {
let op1_props = self.properties();
let op2_props = other.properties();
/// Get this output's screen height in pixels.
///
/// Shorthand for `self.props().pixel_height`.
pub fn pixel_height(&self) -> Option<u32> {
self.props().pixel_height
}
let (Some(_self_loc), Some(self_res), Some(other_loc), Some(other_res)) =
(op1_props.loc, op1_props.res, op2_props.loc, op2_props.res)
else {
return;
};
/// Get this output's refresh rate in millihertz.
///
/// For example, 144Hz will be returned as 144000.
///
/// Shorthand for `self.props().refresh_rate`.
pub fn refresh_rate(&self) -> Option<u32> {
self.props().refresh_rate
}
let y = match top_or_bottom {
TopOrBottom::Top => other_loc.1 - self_res.1,
TopOrBottom::Bottom => other_loc.1 + other_res.1,
};
/// Get this output's physical width in millimeters.
///
/// Shorthand for `self.props().physical_width`.
pub fn physical_width(&self) -> Option<u32> {
self.props().physical_width
}
let x = match alignment {
AlignmentHorizontal::Left => other_loc.0,
AlignmentHorizontal::Center => other_loc.0 + (other_res.0 - self_res.0) / 2,
AlignmentHorizontal::Right => other_loc.0 + (other_res.0 - self_res.0),
};
/// Get this output's physical height in millimeters.
///
/// Shorthand for `self.props().physical_height`.
pub fn physical_height(&self) -> Option<u32> {
self.props().physical_height
}
self.set_loc(Some(x), Some(y));
/// Get whether this output is focused or not.
///
/// This is currently implemented as the output with the most recent pointer motion.
///
/// Shorthand for `self.props().focused`.
pub fn focused(&self) -> Option<bool> {
self.props().focused
}
/// Get the tags this output has.
///
/// Shorthand for `self.props().tags`
pub fn tags(&self) -> Vec<TagHandle> {
self.props().tags
}
/// Get this output's unique name (the name of its connector).
pub fn name(&self) -> &str {
&self.name
}
}
enum TopOrBottom {
Top,
Bottom,
}
enum LeftOrRight {
Left,
Right,
}
/// Horizontal alignment.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum AlignmentHorizontal {
/// Align the outputs such that the left edges are in line.
Left,
/// Center the outputs horizontally.
Center,
/// Align the outputs such that the right edges are in line.
Right,
}
/// Vertical alignment.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum AlignmentVertical {
/// Align the outputs such that the top edges are in line.
Top,
/// Center the outputs vertically.
Center,
/// Align the outputs such that the bottom edges are in line.
Bottom,
/// The properties of an output.
#[derive(Clone, Debug)]
pub struct OutputProperties {
/// The make of the output
pub make: Option<String>,
/// The model of the output
///
/// This is something like "27GL83A" or whatever crap monitor manufacturers name their monitors
/// these days.
pub model: Option<String>,
/// The x position of the output in the global space
pub x: Option<i32>,
/// The y position of the output in the global space
pub y: Option<i32>,
/// The output's screen width in pixels
pub pixel_width: Option<u32>,
/// The output's screen height in pixels
pub pixel_height: Option<u32>,
/// The output's refresh rate in millihertz
pub refresh_rate: Option<u32>,
/// The output's physical width in millimeters
pub physical_width: Option<u32>,
/// The output's physical height in millimeters
pub physical_height: Option<u32>,
/// Whether this output is focused or not
///
/// This is currently implemented as the output with the most recent pointer motion.
pub focused: Option<bool>,
/// The tags this output has
pub tags: Vec<TagHandle>,
}

View file

@ -1,132 +1,178 @@
//! Process management.
//!
//! This module provides [`Process`], which allows you to spawn processes and set environment
//! variables.
use crate::{
msg::{Args, CallbackId, Msg},
send_msg, CallbackVec,
use futures::{
channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture, FutureExt, StreamExt,
};
use pinnacle_api_defs::pinnacle::process::v0alpha1::{
process_service_client::ProcessServiceClient, SetEnvRequest, SpawnRequest,
};
use tonic::transport::Channel;
/// Spawn a process.
///
/// This will use Rust's (more specifically `async_process`'s) `Command` to spawn the provided
/// arguments. If you are using any shell syntax like `~`, you may need to spawn a shell
/// instead. If so, you may *also* need to correctly escape the input.
pub fn spawn(command: Vec<&str>) -> anyhow::Result<()> {
let msg = Msg::Spawn {
command: command.into_iter().map(|s| s.to_string()).collect(),
callback_id: None,
};
send_msg(msg)
/// A struct containing methods to spawn processes with optional callbacks and set environment
/// variables.
#[derive(Debug, Clone)]
pub struct Process {
channel: Channel,
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
}
/// Spawn a process only if it isn't already running.
///
/// This will use Rust's (more specifically `async_process`'s) `Command` to spawn the provided
/// arguments. If you are using any shell syntax like `~`, you may need to spawn a shell
/// instead. If so, you may *also* need to correctly escape the input.
pub fn spawn_once(command: Vec<&str>) -> anyhow::Result<()> {
let msg = Msg::SpawnOnce {
command: command.into_iter().map(|s| s.to_string()).collect(),
callback_id: None,
};
send_msg(msg)
/// Optional callbacks to be run when a spawned process prints to stdout or stderr or exits.
pub struct SpawnCallbacks {
/// A callback that will be run when a process prints to stdout with a line
pub stdout: Option<Box<dyn FnMut(String) + Send>>,
/// A callback that will be run when a process prints to stderr with a line
pub stderr: Option<Box<dyn FnMut(String) + Send>>,
/// A callback that will be run when a process exits with a status code and message
#[allow(clippy::type_complexity)]
pub exit: Option<Box<dyn FnMut(Option<i32>, String) + Send>>,
}
/// Spawn a process with an optional callback for its stdout, stderr, and exit information.
///
/// `callback` has the following parameters:
/// - `0`: The process's stdout printed this line.
/// - `1`: The process's stderr printed this line.
/// - `2`: The process exited with this code.
/// - `3`: The process exited with this message.
/// - `4`: A `&mut `[`CallbackVec`] for use inside the closure.
///
/// You must also pass in a mutable reference to a [`CallbackVec`] in order to store your callback.
pub fn spawn_with_callback<'a, F>(
command: Vec<&str>,
mut callback: F,
callback_vec: &mut CallbackVec<'a>,
) -> anyhow::Result<()>
where
F: FnMut(Option<String>, Option<String>, Option<i32>, Option<String>, &mut CallbackVec) + 'a,
{
let args_callback = move |args: Option<Args>, callback_vec: &mut CallbackVec<'_>| {
if let Some(Args::Spawn {
stdout,
stderr,
exit_code,
exit_msg,
}) = args
{
callback(stdout, stderr, exit_code, exit_msg, callback_vec);
impl Process {
pub(crate) fn new(
channel: Channel,
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
) -> Process {
Self {
channel,
fut_sender,
}
};
}
let len = callback_vec.callbacks.len();
callback_vec.callbacks.push(Box::new(args_callback));
fn create_process_client(&self) -> ProcessServiceClient<Channel> {
ProcessServiceClient::new(self.channel.clone())
}
let msg = Msg::Spawn {
command: command.into_iter().map(|s| s.to_string()).collect(),
callback_id: Some(CallbackId(len as u32)),
};
/// Spawn a process.
///
/// Note that windows spawned *before* tags are added will not be displayed.
/// This will be changed in the future to be more like Awesome, where windows with no tags are
/// displayed on every tag instead.
///
/// # Examples
///
/// ```
/// process.spawn(["alacritty"]);
/// process.spawn(["bash", "-c", "swaybg -i ~/path_to_wallpaper"]);
/// ```
pub fn spawn(&self, args: impl IntoIterator<Item = impl Into<String>>) {
self.spawn_inner(args, false, None);
}
send_msg(msg)
}
// TODO: literally copy pasted from above, but will be rewritten so meh
/// Spawn a process with an optional callback for its stdout, stderr, and exit information,
/// only if it isn't already running.
///
/// `callback` has the following parameters:
/// - `0`: The process's stdout printed this line.
/// - `1`: The process's stderr printed this line.
/// - `2`: The process exited with this code.
/// - `3`: The process exited with this message.
/// - `4`: A `&mut `[`CallbackVec`] for use inside the closure.
///
/// You must also pass in a mutable reference to a [`CallbackVec`] in order to store your callback.
pub fn spawn_once_with_callback<'a, F>(
command: Vec<&str>,
mut callback: F,
callback_vec: &mut CallbackVec<'a>,
) -> anyhow::Result<()>
where
F: FnMut(Option<String>, Option<String>, Option<i32>, Option<String>, &mut CallbackVec) + 'a,
{
let args_callback = move |args: Option<Args>, callback_vec: &mut CallbackVec<'_>| {
if let Some(Args::Spawn {
stdout,
stderr,
exit_code,
exit_msg,
}) = args
{
callback(stdout, stderr, exit_code, exit_msg, callback_vec);
}
};
let len = callback_vec.callbacks.len();
callback_vec.callbacks.push(Box::new(args_callback));
let msg = Msg::SpawnOnce {
command: command.into_iter().map(|s| s.to_string()).collect(),
callback_id: Some(CallbackId(len as u32)),
};
send_msg(msg)
}
/// Set an environment variable for Pinnacle. All future processes spawned will have this env set.
///
/// Note that this will only set the variable for the compositor, not the running config process.
/// If you need to set an environment variable for this config, place them in the `metaconfig.toml` file instead
/// or use [`std::env::set_var`].
pub fn set_env(key: &str, value: &str) {
let msg = Msg::SetEnv {
key: key.to_string(),
value: value.to_string(),
};
send_msg(msg).unwrap();
/// Spawn a process with callbacks for its stdout, stderr, and exit information.
///
/// See [`SpawnCallbacks`] for the passed in struct.
///
/// Note that windows spawned *before* tags are added will not be displayed.
/// This will be changed in the future to be more like Awesome, where windows with no tags are
/// displayed on every tag instead.
///
/// # Examples
///
/// ```
/// use pinnacle_api::process::SpawnCallbacks;
///
/// process.spawn_with_callbacks(["alacritty"], SpawnCallbacks {
/// stdout: Some(Box::new(|line| println!("stdout: {line}"))),
/// stderr: Some(Box::new(|line| println!("stderr: {line}"))),
/// stdout: Some(Box::new(|code, msg| println!("exit code: {code:?}, exit_msg: {msg}"))),
/// });
/// ```
pub fn spawn_with_callbacks(
&self,
args: impl IntoIterator<Item = impl Into<String>>,
callbacks: SpawnCallbacks,
) {
self.spawn_inner(args, false, Some(callbacks));
}
/// Spawn a process only if it isn't already running.
///
/// This is useful for startup programs.
///
/// See [`Process::spawn`] for details.
pub fn spawn_once(&self, args: impl IntoIterator<Item = impl Into<String>>) {
self.spawn_inner(args, true, None);
}
/// Spawn a process only if it isn't already running with optional callbacks for its stdout,
/// stderr, and exit information.
///
/// This is useful for startup programs.
///
/// See [`Process::spawn_with_callbacks`] for details.
pub fn spawn_once_with_callbacks(
&self,
args: impl IntoIterator<Item = impl Into<String>>,
callbacks: SpawnCallbacks,
) {
self.spawn_inner(args, true, Some(callbacks));
}
fn spawn_inner(
&self,
args: impl IntoIterator<Item = impl Into<String>>,
once: bool,
callbacks: Option<SpawnCallbacks>,
) {
let mut client = self.create_process_client();
let args = args.into_iter().map(Into::into).collect::<Vec<_>>();
let request = SpawnRequest {
args,
once: Some(once),
has_callback: Some(callbacks.is_some()),
};
self.fut_sender
.unbounded_send(
async move {
let mut stream = client.spawn(request).await.unwrap().into_inner();
let Some(mut callbacks) = callbacks else { return };
while let Some(Ok(response)) = stream.next().await {
if let Some(line) = response.stdout {
if let Some(stdout) = callbacks.stdout.as_mut() {
stdout(line);
}
}
if let Some(line) = response.stderr {
if let Some(stderr) = callbacks.stderr.as_mut() {
stderr(line);
}
}
if let Some(exit_msg) = response.exit_message {
if let Some(exit) = callbacks.exit.as_mut() {
exit(response.exit_code, exit_msg);
}
}
}
}
.boxed(),
)
.unwrap();
}
/// Set an environment variable for the compositor.
/// This will cause any future spawned processes to have this environment variable.
///
/// # Examples
///
/// ```
/// process.set_env("ENV", "a value lalala");
/// ```
pub fn set_env(&self, key: impl Into<String>, value: impl Into<String>) {
let key = key.into();
let value = value.into();
let mut client = self.create_process_client();
block_on(client.set_env(SetEnvRequest {
key: Some(key),
value: Some(value),
}))
.unwrap();
}
}

View file

@ -1,208 +1,528 @@
//! Tag management.
//!
//! This module allows you to interact with Pinnacle's tag system.
//!
//! # The Tag System
//! Many Wayland compositors use workspaces for window management.
//! Each window is assigned to a workspace and will only show up if that workspace is being
//! viewed. This is a find way to manage windows, but it's not that powerful.
//!
//! Instead, Pinnacle works with a tag system similar to window managers like [dwm](https://dwm.suckless.org/)
//! and, the window manager Pinnacle takes inspiration from, [awesome](https://awesomewm.org/).
//!
//! In a tag system, there are no workspaces. Instead, each window can be tagged with zero or more
//! tags, and zero or more tags can be displayed on a monitor at once. This allows you to, for
//! example, bring in your browsers on the same screen as your IDE by toggling the "Browser" tag.
//!
//! Workspaces can be emulated by only displaying one tag at a time. Combining this feature with
//! the ability to tag windows with multiple tags allows you to have one window show up on multiple
//! different "workspaces". As you can see, this system is much more powerful than workspaces
//! alone.
//!
//! # Configuration
//! `tag` contains the [`Tag`] struct, which allows you to add new tags
//! and get handles to already defined ones.
//!
//! These [`TagHandle`]s allow you to manipulate individual tags and get their properties.
use std::collections::HashMap;
use crate::{
msg::{Msg, Request, RequestResponse},
output::{OutputHandle, OutputName},
request, send_msg,
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
/// Get a tag by its name and output. If `output` is `None`, the currently focused output will
/// be used instead.
///
/// If multiple tags have the same name, this returns the first one.
pub fn get(name: &str, output: Option<&OutputHandle>) -> Option<TagHandle> {
get_all()
.filter(|tag| {
tag.properties().output.is_some_and(|op| match output {
Some(output) => &op == output,
None => Some(op) == crate::output::get_focused(),
})
use futures::{channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture};
use num_enum::TryFromPrimitive;
use pinnacle_api_defs::pinnacle::{
output::v0alpha1::output_service_client::OutputServiceClient,
tag::{
self,
v0alpha1::{
tag_service_client::TagServiceClient, AddRequest, RemoveRequest, SetActiveRequest,
SetLayoutRequest, SwitchToRequest,
},
},
};
use tonic::transport::Channel;
use crate::output::{Output, OutputHandle};
/// A struct that allows you to add and remove tags and get [`TagHandle`]s.
#[derive(Clone, Debug)]
pub struct Tag {
channel: Channel,
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
}
impl Tag {
pub(crate) fn new(
channel: Channel,
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
) -> Self {
Self {
channel,
fut_sender,
}
}
fn create_tag_client(&self) -> TagServiceClient<Channel> {
TagServiceClient::new(self.channel.clone())
}
fn create_output_client(&self) -> OutputServiceClient<Channel> {
OutputServiceClient::new(self.channel.clone())
}
/// Add tags to the specified output.
///
/// This will add tags with the given names to `output` and return [`TagHandle`]s to all of
/// them.
///
/// # Examples
///
/// ```
/// // Add tags 1-5 to the focused output
/// if let Some(op) = output.get_focused() {
/// let tags = tag.add(&op, ["1", "2", "3", "4", "5"]);
/// }
/// ```
pub fn add(
&self,
output: &OutputHandle,
tag_names: impl IntoIterator<Item = impl Into<String>>,
) -> impl Iterator<Item = TagHandle> {
let mut client = self.create_tag_client();
let output_client = self.create_output_client();
let tag_names = tag_names.into_iter().map(Into::into).collect();
let response = block_on(client.add(AddRequest {
output_name: Some(output.name.clone()),
tag_names,
}))
.unwrap()
.into_inner();
response.tag_ids.into_iter().map(move |id| TagHandle {
client: client.clone(),
output_client: output_client.clone(),
id,
})
.find(|tag| tag.properties().name.is_some_and(|s| s == name))
}
}
/// Get all tags.
pub fn get_all() -> impl Iterator<Item = TagHandle> {
let RequestResponse::Tags { tag_ids } = request(Request::GetTags) else {
unreachable!()
};
/// Get handles to all tags across all outputs.
///
/// # Examples
///
/// ```
/// let all_tags = tag.get_all();
/// ```
pub fn get_all(&self) -> impl Iterator<Item = TagHandle> {
let mut client = self.create_tag_client();
let output_client = self.create_output_client();
tag_ids.into_iter().map(TagHandle)
}
let response = block_on(client.get(tag::v0alpha1::GetRequest {}))
.unwrap()
.into_inner();
// TODO: return taghandles here
/// Add tags with the names from `names` to `output`.
pub fn add(output: &OutputHandle, names: &[&str]) {
let msg = Msg::AddTags {
output_name: output.0.clone(),
tag_names: names.iter().map(|s| s.to_string()).collect(),
};
response.tag_ids.into_iter().map(move |id| TagHandle {
client: client.clone(),
output_client: output_client.clone(),
id,
})
}
send_msg(msg).unwrap();
}
/// Get a handle to the first tag with the given name on `output`.
///
/// If `output` is `None`, the focused output will be used.
///
/// # Examples
///
/// ```
/// // Get tag "1" on output "HDMI-1"
/// if let Some(op) = output.get_by_name("HDMI-1") {
/// let tg = tag.get("1", &op);
/// }
///
/// // Get tag "Thing" on the focused output
/// let tg = tag.get("Thing", None);
/// ```
pub fn get<'a>(
&self,
name: impl Into<String>,
output: impl Into<Option<&'a OutputHandle>>,
) -> Option<TagHandle> {
let name = name.into();
let output: Option<&OutputHandle> = output.into();
let output_module = Output::new(self.channel.clone(), self.fut_sender.clone());
/// Create a `LayoutCycler` to cycle layouts on tags.
///
/// Given a slice of layouts, this will create a `LayoutCycler` with two methods;
/// one will cycle forward the layout for the active tag, and one will cycle backward.
///
/// # Example
/// ```
/// todo!()
/// ```
pub fn layout_cycler(layouts: &[Layout]) -> LayoutCycler {
let indices = std::rc::Rc::new(std::cell::RefCell::new(HashMap::<TagId, usize>::new()));
let indices_clone = indices.clone();
let layouts = layouts.to_vec();
let layouts_clone = layouts.clone();
let len = layouts.len();
let next = move |output: Option<&OutputHandle>| {
let Some(output) = output.cloned().or_else(crate::output::get_focused) else {
return;
self.get_all().find(|tag| {
let props = tag.props();
let same_tag_name = props.name.as_ref() == Some(&name);
let same_output = props.output.is_some_and(|op| {
Some(op.name)
== output
.map(|o| o.name.clone())
.or_else(|| output_module.get_focused().map(|o| o.name))
});
same_tag_name && same_output
})
}
/// Remove the given tags from their outputs.
///
/// # Examples
///
/// ```
/// let tags = tag.add(output.get_by_name("DP-1")?, ["1", "2", "Buckle", "Shoe"]);
///
/// tag.remove(tags); // "DP-1" no longer has any tags
/// ```
pub fn remove(&self, tags: impl IntoIterator<Item = TagHandle>) {
let tag_ids = tags.into_iter().map(|handle| handle.id).collect::<Vec<_>>();
let mut client = self.create_tag_client();
block_on(client.remove(RemoveRequest { tag_ids })).unwrap();
}
/// Create a [`LayoutCycler`] to cycle layouts on outputs.
///
/// This will create a `LayoutCycler` with two functions: one to cycle forward the layout for
/// the first active tag on the specified output, and one to cycle backward.
///
/// If you do not specify an output for `LayoutCycler` functions, it will default to the
/// focused output.
///
/// # Examples
///
/// ```
/// use pinnacle_api::tag::{Layout, LayoutCycler};
/// use pinnacle_api::xkbcommon::xkb::Keysym;
/// use pinnacle_api::input::Mod;
///
/// // Create a layout cycler that cycles through the listed layouts
/// let LayoutCycler {
/// prev: layout_prev,
/// next: layout_next,
/// } = tag.new_layout_cycler([
/// Layout::MasterStack,
/// Layout::Dwindle,
/// Layout::Spiral,
/// Layout::CornerTopLeft,
/// Layout::CornerTopRight,
/// Layout::CornerBottomLeft,
/// Layout::CornerBottomRight,
/// ]);
///
/// // Cycle layouts forward on the focused output
/// layout_next(None);
///
/// // Cycle layouts backward on the focused output
/// layout_prev(None);
///
/// // Cycle layouts forward on "eDP-1"
/// layout_next(output.get_by_name("eDP-1")?);
/// ```
pub fn new_layout_cycler(&self, layouts: impl IntoIterator<Item = Layout>) -> LayoutCycler {
let indices = Arc::new(Mutex::new(HashMap::<u32, usize>::new()));
let indices_clone = indices.clone();
let layouts = layouts.into_iter().collect::<Vec<_>>();
let layouts_clone = layouts.clone();
let len = layouts.len();
let output_module = Output::new(self.channel.clone(), self.fut_sender.clone());
let output_module_clone = output_module.clone();
let next = move |output: Option<&OutputHandle>| {
let Some(output) = output.cloned().or_else(|| output_module.get_focused()) else {
return;
};
let Some(first_tag) = output
.props()
.tags
.into_iter()
.find(|tag| tag.active() == Some(true))
else {
return;
};
let mut indices = indices.lock().expect("layout next mutex lock failed");
let index = indices.entry(first_tag.id).or_insert(0);
if *index + 1 >= len {
*index = 0;
} else {
*index += 1;
}
first_tag.set_layout(layouts[*index]);
};
let Some(tag) = output
.properties()
.tags
.into_iter()
.find(|tag| tag.properties().active == Some(true))
else {
return;
let prev = move |output: Option<&OutputHandle>| {
let Some(output) = output
.cloned()
.or_else(|| output_module_clone.get_focused())
else {
return;
};
let Some(first_tag) = output
.props()
.tags
.into_iter()
.find(|tag| tag.active() == Some(true))
else {
return;
};
let mut indices = indices_clone.lock().expect("layout next mutex lock failed");
let index = indices.entry(first_tag.id).or_insert(0);
if index.checked_sub(1).is_none() {
*index = len - 1;
} else {
*index -= 1;
}
first_tag.set_layout(layouts_clone[*index]);
};
let mut indices = indices.borrow_mut();
let index = indices.entry(tag.0).or_insert(0);
if *index + 1 >= len {
*index = 0;
} else {
*index += 1;
LayoutCycler {
prev: Box::new(prev),
next: Box::new(next),
}
tag.set_layout(layouts[*index]);
};
let prev = move |output: Option<&OutputHandle>| {
let Some(output) = output.cloned().or_else(crate::output::get_focused) else {
return;
};
let Some(tag) = output
.properties()
.tags
.into_iter()
.find(|tag| tag.properties().active == Some(true))
else {
return;
};
let mut indices = indices_clone.borrow_mut();
let index = indices.entry(tag.0).or_insert(0);
if index.wrapping_sub(1) == usize::MAX {
*index = len - 1;
} else {
*index -= 1;
}
tag.set_layout(layouts_clone[*index]);
};
LayoutCycler {
next: Box::new(next),
prev: Box::new(prev),
}
}
/// A layout cycler that keeps track of tags and their layouts and provides methods to cycle
/// A layout cycler that keeps track of tags and their layouts and provides functions to cycle
/// layouts on them.
#[allow(clippy::type_complexity)]
pub struct LayoutCycler {
/// Cycle to the next layout on the given output, or the focused output if `None`.
pub next: Box<dyn FnMut(Option<&OutputHandle>)>,
pub prev: Box<dyn Fn(Option<&OutputHandle>) + Send + Sync + 'static>,
/// Cycle to the previous layout on the given output, or the focused output if `None`.
pub prev: Box<dyn FnMut(Option<&OutputHandle>)>,
}
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub(crate) enum TagId {
None,
#[serde(untagged)]
Some(u32),
pub next: Box<dyn Fn(Option<&OutputHandle>) + Send + Sync + 'static>,
}
/// A handle to a tag.
pub struct TagHandle(pub(crate) TagId);
/// Properties of a tag, retrieved through [`TagHandle::properties`].
#[derive(Debug)]
pub struct TagProperties {
/// Whether or not the tag is active.
pub active: Option<bool>,
/// The tag's name.
pub name: Option<String>,
/// The output the tag is on.
pub output: Option<OutputHandle>,
///
/// This handle allows you to do things like switch to tags and get their properties.
#[derive(Debug, Clone)]
pub struct TagHandle {
pub(crate) client: TagServiceClient<Channel>,
pub(crate) output_client: OutputServiceClient<Channel>,
pub(crate) id: u32,
}
impl TagHandle {
/// Get this tag's [`TagProperties`].
pub fn properties(&self) -> TagProperties {
let RequestResponse::TagProps {
active,
name,
output_name,
} = request(Request::GetTagProps { tag_id: self.0 })
else {
unreachable!()
};
TagProperties {
active,
name,
output: output_name.map(|name| OutputHandle(OutputName(name))),
}
}
/// Toggle this tag.
pub fn toggle(&self) {
let msg = Msg::ToggleTag { tag_id: self.0 };
send_msg(msg).unwrap();
}
/// Switch to this tag, deactivating all others on its output.
pub fn switch_to(&self) {
let msg = Msg::SwitchToTag { tag_id: self.0 };
send_msg(msg).unwrap();
}
/// Set this tag's [`Layout`].
pub fn set_layout(&self, layout: Layout) {
let msg = Msg::SetLayout {
tag_id: self.0,
layout,
};
send_msg(msg).unwrap()
}
}
/// Layouts for tags.
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
/// Various static layouts.
#[repr(i32)]
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, TryFromPrimitive)]
pub enum Layout {
/// One master window on the left with all other windows stacked to the right.
MasterStack,
/// Windows split in half towards the bottom right corner.
/// One master window on the left with all other windows stacked to the right
MasterStack = 1,
/// Windows split in half towards the bottom right corner
Dwindle,
/// Windows split in half in a spiral
Spiral,
/// One main corner window in the top left with a column of windows on the right and a row on the bottom.
/// One main corner window in the top left with a column of windows on the right and a row on the bottom
CornerTopLeft,
/// One main corner window in the top right with a column of windows on the left and a row on the bottom.
/// One main corner window in the top right with a column of windows on the left and a row on the bottom
CornerTopRight,
/// One main corner window in the bottom left with a column of windows on the right and a row on the top.
CornerBottomLeft,
/// One main corner window in the bottom right with a column of windows on the left and a row on the top.
CornerBottomRight,
}
impl TagHandle {
/// Activate this tag and deactivate all other ones on the same output.
///
/// This essentially emulates what a traditional workspace is.
///
/// # Examples
///
/// ```
/// // Assume the focused output has the following inactive tags and windows:
/// // "1": Alacritty
/// // "2": Firefox, Discord
/// // "3": Steam
/// tag.get("2")?.switch_to(); // Displays Firefox and Discord
/// tag.get("3")?.switch_to(); // Displays Steam
/// ```
pub fn switch_to(&self) {
let mut client = self.client.clone();
block_on(client.switch_to(SwitchToRequest {
tag_id: Some(self.id),
}))
.unwrap();
}
/// Set this tag to active or not.
///
/// While active, windows with this tag will be displayed.
///
/// While inactive, windows with this tag will not be displayed unless they have other active
/// tags.
///
/// # Examples
///
/// ```
/// // Assume the focused output has the following inactive tags and windows:
/// // "1": Alacritty
/// // "2": Firefox, Discord
/// // "3": Steam
/// tag.get("2")?.set_active(true); // Displays Firefox and Discord
/// tag.get("3")?.set_active(true); // Displays Firefox, Discord, and Steam
/// tag.get("2")?.set_active(false); // Displays Steam
/// ```
pub fn set_active(&self, set: bool) {
let mut client = self.client.clone();
block_on(client.set_active(SetActiveRequest {
tag_id: Some(self.id),
set_or_toggle: Some(tag::v0alpha1::set_active_request::SetOrToggle::Set(set)),
}))
.unwrap();
}
/// Toggle this tag between active and inactive.
///
/// While active, windows with this tag will be displayed.
///
/// While inactive, windows with this tag will not be displayed unless they have other active
/// tags.
///
/// # Examples
///
/// ```
/// // Assume the focused output has the following inactive tags and windows:
/// // "1": Alacritty
/// // "2": Firefox, Discord
/// // "3": Steam
/// tag.get("2")?.toggle(); // Displays Firefox and Discord
/// tag.get("3")?.toggle(); // Displays Firefox, Discord, and Steam
/// tag.get("3")?.toggle(); // Displays Firefox, Discord
/// tag.get("2")?.toggle(); // Displays nothing
/// ```
pub fn toggle_active(&self) {
let mut client = self.client.clone();
block_on(client.set_active(SetActiveRequest {
tag_id: Some(self.id),
set_or_toggle: Some(tag::v0alpha1::set_active_request::SetOrToggle::Toggle(())),
}))
.unwrap();
}
/// Remove this tag from its output.
///
/// # Examples
///
/// ```
/// let tags = tag
/// .add(output.get_by_name("DP-1")?, ["1", "2", "Buckle", "Shoe"])
/// .collect::<Vec<_>>;
///
/// tags[1].remove();
/// tags[3].remove();
/// // "DP-1" now only has tags "1" and "Buckle"
/// ```
pub fn remove(mut self) {
block_on(self.client.remove(RemoveRequest {
tag_ids: vec![self.id],
}))
.unwrap();
}
/// Set this tag's layout.
///
/// Layouting only applies to tiled windows (windows that are not floating, maximized, or
/// fullscreen). If multiple tags are active on an output, the first active tag's layout will
/// determine the layout strategy.
///
/// See [`Layout`] for the different static layouts Pinnacle currently has to offer.
///
/// # Examples
///
/// ```
/// use pinnacle_api::tag::Layout;
///
/// // Set the layout of tag "1" on the focused output to "corner top left".
/// tag.get("1", None)?.set_layout(Layout::CornerTopLeft);
/// ```
pub fn set_layout(&self, layout: Layout) {
let mut client = self.client.clone();
block_on(client.set_layout(SetLayoutRequest {
tag_id: Some(self.id),
layout: Some(layout as i32),
}))
.unwrap();
}
/// Get all properties of this tag.
///
/// # Examples
///
/// ```
/// use pinnacle_api::tag::TagProperties;
///
/// let TagProperties {
/// active,
/// name,
/// output,
/// } = tag.get("1", None)?.props();
/// ```
pub fn props(&self) -> TagProperties {
let mut client = self.client.clone();
let output_client = self.output_client.clone();
let response = block_on(client.get_properties(tag::v0alpha1::GetPropertiesRequest {
tag_id: Some(self.id),
}))
.unwrap()
.into_inner();
TagProperties {
active: response.active,
name: response.name,
output: response.output_name.map(|name| OutputHandle {
client: output_client,
tag_client: client,
name,
}),
}
}
/// Get this tag's active status.
///
/// Shorthand for `self.props().active`.
pub fn active(&self) -> Option<bool> {
self.props().active
}
/// Get this tag's name.
///
/// Shorthand for `self.props().name`.
pub fn name(&self) -> Option<String> {
self.props().name
}
/// Get a handle to the output this tag is on.
///
/// Shorthand for `self.props().output`.
pub fn output(&self) -> Option<OutputHandle> {
self.props().output
}
}
/// Properties of a tag.
pub struct TagProperties {
/// Whether the tag is active or not
pub active: Option<bool>,
/// The name of the tag
pub name: Option<String>,
/// The output the tag is on
pub output: Option<OutputHandle>,
}

View file

@ -1,206 +1,530 @@
//! Window management.
//!
//! This module provides [`Window`], which allows you to get [`WindowHandle`]s and move and resize
//! windows using the mouse.
//!
//! [`WindowHandle`]s allow you to do things like resize and move windows, toggle them between
//! floating and tiled, close them, and more.
//!
//! This module also allows you to set window rules; see the [rules] module for more information.
use futures::executor::block_on;
use num_enum::TryFromPrimitive;
use pinnacle_api_defs::pinnacle::{
output::v0alpha1::output_service_client::OutputServiceClient,
tag::v0alpha1::tag_service_client::TagServiceClient,
window::v0alpha1::{
window_service_client::WindowServiceClient, AddWindowRuleRequest, CloseRequest,
MoveToTagRequest, SetTagRequest,
},
window::{
self,
v0alpha1::{
GetRequest, MoveGrabRequest, ResizeGrabRequest, SetFloatingRequest,
SetFullscreenRequest, SetMaximizedRequest,
},
},
};
use tonic::transport::Channel;
use crate::{input::MouseButton, tag::TagHandle, util::Geometry};
use self::rules::{WindowRule, WindowRuleCondition};
pub mod rules;
use crate::{
input::MouseButton,
msg::{Msg, Request, RequestResponse},
request, send_msg,
tag::TagHandle,
};
/// A unique identifier for each window.
#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub(crate) enum WindowId {
/// A config API returned an invalid window. It should be using this variant.
None,
/// A valid window id.
#[serde(untagged)]
Some(u32),
}
/// Get all windows with the class `class`.
pub fn get_by_class(class: &str) -> impl Iterator<Item = WindowHandle> + '_ {
get_all().filter(|win| win.properties().class.as_deref() == Some(class))
}
/// Get the currently focused window, or `None` if there isn't one.
pub fn get_focused() -> Option<WindowHandle> {
get_all().find(|win| win.properties().focused.is_some_and(|focused| focused))
}
/// Get all windows.
pub fn get_all() -> impl Iterator<Item = WindowHandle> {
let RequestResponse::Windows { window_ids } = request(Request::GetWindows) else {
unreachable!()
};
window_ids.into_iter().map(WindowHandle)
}
/// Begin a window move.
/// A struct containing methods that get [`WindowHandle`]s and move windows with the mouse.
///
/// This will start a window move grab with the provided button on the window the pointer
/// is currently hovering over. Once `button` is let go, the move will end.
pub fn begin_move(button: MouseButton) {
let msg = Msg::WindowMoveGrab {
button: button as u32,
};
send_msg(msg).unwrap();
/// See [`WindowHandle`] for more information.
#[derive(Debug, Clone)]
pub struct Window {
channel: Channel,
}
/// Begin a window resize.
///
/// This will start a window resize grab with the provided button on the window the
/// pointer is currently hovering over. Once `button` is let go, the resize will end.
pub fn begin_resize(button: MouseButton) {
let msg = Msg::WindowResizeGrab {
button: button as u32,
};
send_msg(msg).unwrap();
}
/// A handle to a window.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct WindowHandle(WindowId);
/// Properties of a window, retrieved through [`WindowHandle::properties`].
#[derive(Debug)]
pub struct WindowProperties {
/// The size of the window, in pixels.
pub size: Option<(i32, i32)>,
/// The location of the window in the global space.
pub loc: Option<(i32, i32)>,
/// The window's class.
pub class: Option<String>,
/// The window's title.
pub title: Option<String>,
/// Whether or not the window is focused.
pub focused: Option<bool>,
/// Whether or not the window is floating.
pub floating: Option<bool>,
/// Whether the window is fullscreen, maximized, or neither.
pub fullscreen_or_maximized: Option<FullscreenOrMaximized>,
}
impl WindowHandle {
/// Toggle this window between floating and tiled.
pub fn toggle_floating(&self) {
send_msg(Msg::ToggleFloating { window_id: self.0 }).unwrap();
impl Window {
pub(crate) fn new(channel: Channel) -> Self {
Self { channel }
}
/// Toggle this window's fullscreen status.
fn create_window_client(&self) -> WindowServiceClient<Channel> {
WindowServiceClient::new(self.channel.clone())
}
fn create_tag_client(&self) -> TagServiceClient<Channel> {
TagServiceClient::new(self.channel.clone())
}
fn create_output_client(&self) -> OutputServiceClient<Channel> {
OutputServiceClient::new(self.channel.clone())
}
/// Start moving the window with the mouse.
///
/// If used while not fullscreen, it becomes fullscreen.
/// If used while fullscreen, it becomes unfullscreen.
/// If used while maximized, it becomes fullscreen.
pub fn toggle_fullscreen(&self) {
send_msg(Msg::ToggleFullscreen { window_id: self.0 }).unwrap();
}
/// Toggle this window's maximized status.
/// This will begin moving the window under the pointer using the specified [`MouseButton`].
/// The button must be held down at the time this method is called for the move to start.
///
/// If used while not maximized, it becomes maximized.
/// If used while maximized, it becomes unmaximized.
/// If used while fullscreen, it becomes maximized.
pub fn toggle_maximized(&self) {
send_msg(Msg::ToggleMaximized { window_id: self.0 }).unwrap();
}
/// Set this window's size. None parameters will be ignored.
pub fn set_size(&self, width: Option<i32>, height: Option<i32>) {
send_msg(Msg::SetWindowSize {
window_id: self.0,
width,
height,
})
/// This is intended to be used with [`Input::keybind`][crate::input::Input::keybind].
///
/// # Examples
///
/// ```
/// use pinnacle_api::input::{Mod, MouseButton, MouseEdge};
///
/// // Set `Super + left click` to begin moving a window
/// input.mousebind([Mod::Super], MouseButton::Left, MouseEdge::Press, || {
/// window.begin_move(MouseButton::Left);
/// });
/// ```
pub fn begin_move(&self, button: MouseButton) {
let mut client = self.create_window_client();
block_on(client.move_grab(MoveGrabRequest {
button: Some(button as u32),
}))
.unwrap();
}
/// Send a close event to this window.
pub fn close(&self) {
send_msg(Msg::CloseWindow { window_id: self.0 }).unwrap();
/// Start resizing the window with the mouse.
///
/// This will begin resizing the window under the pointer using the specified [`MouseButton`].
/// The button must be held down at the time this method is called for the resize to start.
///
/// This is intended to be used with [`Input::keybind`][crate::input::Input::keybind].
///
/// # Examples
///
/// ```
/// use pinnacle_api::input::{Mod, MouseButton, MouseEdge};
///
/// // Set `Super + right click` to begin moving a window
/// input.mousebind([Mod::Super], MouseButton::Right, MouseEdge::Press, || {
/// window.begin_resize(MouseButton::Right);
/// });
/// ```
pub fn begin_resize(&self, button: MouseButton) {
let mut client = self.create_window_client();
block_on(client.resize_grab(ResizeGrabRequest {
button: Some(button as u32),
}))
.unwrap();
}
/// Get this window's [`WindowProperties`].
pub fn properties(&self) -> WindowProperties {
let RequestResponse::WindowProps {
size,
loc,
class,
title,
focused,
floating,
fullscreen_or_maximized,
} = request(Request::GetWindowProps { window_id: self.0 })
else {
unreachable!()
};
/// Get all windows.
///
/// # Examples
///
/// ```
/// let windows = window.get_all();
/// ```
pub fn get_all(&self) -> impl Iterator<Item = WindowHandle> {
let mut client = self.create_window_client();
let tag_client = self.create_tag_client();
let output_client = self.create_output_client();
block_on(client.get(GetRequest {}))
.unwrap()
.into_inner()
.window_ids
.into_iter()
.map(move |id| WindowHandle {
client: client.clone(),
id,
tag_client: tag_client.clone(),
output_client: output_client.clone(),
})
}
/// Get the currently focused window.
///
/// # Examples
///
/// ```
/// let focused_window = window.get_focused()?;
/// ```
pub fn get_focused(&self) -> Option<WindowHandle> {
self.get_all()
.find(|window| matches!(window.props().focused, Some(true)))
}
/// Add a window rule.
///
/// A window rule is a set of criteria that a window must open with.
/// For it to apply, a [`WindowRuleCondition`] must evaluate to true for the window in question.
///
/// TODO:
pub fn add_window_rule(&self, cond: WindowRuleCondition, rule: WindowRule) {
let mut client = self.create_window_client();
block_on(client.add_window_rule(AddWindowRuleRequest {
cond: Some(cond.0),
rule: Some(rule.0),
}))
.unwrap();
}
}
/// A handle to a window.
///
/// This allows you to manipulate the window and get its properties.
#[derive(Debug, Clone)]
pub struct WindowHandle {
pub(crate) client: WindowServiceClient<Channel>,
pub(crate) id: u32,
pub(crate) tag_client: TagServiceClient<Channel>,
pub(crate) output_client: OutputServiceClient<Channel>,
}
/// Whether a window is fullscreen, maximized, or neither.
#[repr(i32)]
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, TryFromPrimitive)]
pub enum FullscreenOrMaximized {
/// The window is neither fullscreen nor maximized
Neither = 1,
/// The window is fullscreen
Fullscreen,
/// The window is maximized
Maximized,
}
/// Properties of a window.
#[derive(Debug, Clone)]
pub struct WindowProperties {
/// The location and size of the window
pub geometry: Option<Geometry>,
/// The window's class
pub class: Option<String>,
/// The window's title
pub title: Option<String>,
/// Whether the window is focused or not
pub focused: Option<bool>,
/// Whether the window is floating or not
///
/// Note that a window can still be floating even if it's fullscreen or maximized; those two
/// state will just override the floating state.
pub floating: Option<bool>,
/// Whether the window is fullscreen, maximized, or neither
pub fullscreen_or_maximized: Option<FullscreenOrMaximized>,
/// All the tags on the window
pub tags: Vec<TagHandle>,
}
impl WindowHandle {
/// Send a close request to this window.
///
/// If the window is unresponsive, it may not close.
///
/// # Examples
///
/// ```
/// // Close the focused window
/// window.get_focused()?.close()
/// ```
pub fn close(mut self) {
block_on(self.client.close(CloseRequest {
window_id: Some(self.id),
}))
.unwrap();
}
/// Set this window to fullscreen or not.
///
/// If it is maximized, setting it to fullscreen will remove the maximized state.
///
/// # Examples
///
/// ```
/// // Set the focused window to fullscreen.
/// window.get_focused()?.set_fullscreen(true);
/// ```
pub fn set_fullscreen(&self, set: bool) {
let mut client = self.client.clone();
block_on(client.set_fullscreen(SetFullscreenRequest {
window_id: Some(self.id),
set_or_toggle: Some(window::v0alpha1::set_fullscreen_request::SetOrToggle::Set(
set,
)),
}))
.unwrap();
}
/// Toggle this window between fullscreen and not.
///
/// If it is maximized, toggling it to fullscreen will remove the maximized state.
///
/// # Examples
///
/// ```
/// // Toggle the focused window to and from fullscreen.
/// window.get_focused()?.toggle_fullscreen();
/// ```
pub fn toggle_fullscreen(&self) {
let mut client = self.client.clone();
block_on(client.set_fullscreen(SetFullscreenRequest {
window_id: Some(self.id),
set_or_toggle: Some(window::v0alpha1::set_fullscreen_request::SetOrToggle::Toggle(())),
}))
.unwrap();
}
/// Set this window to maximized or not.
///
/// If it is fullscreen, setting it to maximized will remove the fullscreen state.
///
/// # Examples
///
/// ```
/// // Set the focused window to maximized.
/// window.get_focused()?.set_maximized(true);
/// ```
pub fn set_maximized(&self, set: bool) {
let mut client = self.client.clone();
block_on(client.set_maximized(SetMaximizedRequest {
window_id: Some(self.id),
set_or_toggle: Some(window::v0alpha1::set_maximized_request::SetOrToggle::Set(
set,
)),
}))
.unwrap();
}
/// Toggle this window between maximized and not.
///
/// If it is fullscreen, setting it to maximized will remove the fullscreen state.
///
/// # Examples
///
/// ```
/// // Toggle the focused window to and from maximized.
/// window.get_focused()?.toggle_maximized();
/// ```
pub fn toggle_maximized(&self) {
let mut client = self.client.clone();
block_on(client.set_maximized(SetMaximizedRequest {
window_id: Some(self.id),
set_or_toggle: Some(window::v0alpha1::set_maximized_request::SetOrToggle::Toggle(())),
}))
.unwrap();
}
/// Set this window to floating or not.
///
/// Floating windows will not be tiled and can be moved around and resized freely.
///
/// Note that fullscreen and maximized windows can still be floating; those two states will
/// just override the floating state.
///
/// # Examples
///
/// ```
/// // Set the focused window to floating.
/// window.get_focused()?.set_floating(true);
/// ```
pub fn set_floating(&self, set: bool) {
let mut client = self.client.clone();
block_on(client.set_floating(SetFloatingRequest {
window_id: Some(self.id),
set_or_toggle: Some(window::v0alpha1::set_floating_request::SetOrToggle::Set(
set,
)),
}))
.unwrap();
}
/// Toggle this window to and from floating.
///
/// Floating windows will not be tiled and can be moved around and resized freely.
///
/// Note that fullscreen and maximized windows can still be floating; those two states will
/// just override the floating state.
///
/// # Examples
///
/// ```
/// // Toggle the focused window to and from floating.
/// window.get_focused()?.toggle_floating();
/// ```
pub fn toggle_floating(&self) {
let mut client = self.client.clone();
block_on(client.set_floating(SetFloatingRequest {
window_id: Some(self.id),
set_or_toggle: Some(window::v0alpha1::set_floating_request::SetOrToggle::Toggle(
(),
)),
}))
.unwrap();
}
/// Move this window to the given `tag`.
///
/// This will remove all tags from this window then tag it with `tag`, essentially moving the
/// window to that tag.
///
/// # Examples
///
/// ```
/// // Move the focused window to tag "Code" on the focused output
/// window.get_focused()?.move_to_tag(&tag.get("Code", None)?);
/// ```
pub fn move_to_tag(&self, tag: &TagHandle) {
let mut client = self.client.clone();
block_on(client.move_to_tag(MoveToTagRequest {
window_id: Some(self.id),
tag_id: Some(tag.id),
}))
.unwrap();
}
/// Set or unset a tag on this window.
///
/// # Examples
///
/// ```
/// let focused = window.get_focused()?;
/// let tg = tag.get("Potato", None)?;
///
/// focused.set_tag(&tg, true); // `focused` now has tag "Potato"
/// focused.set_tag(&tg, false); // `focused` no longer has tag "Potato"
/// ```
pub fn set_tag(&self, tag: &TagHandle, set: bool) {
let mut client = self.client.clone();
block_on(client.set_tag(SetTagRequest {
window_id: Some(self.id),
tag_id: Some(tag.id),
set_or_toggle: Some(window::v0alpha1::set_tag_request::SetOrToggle::Set(set)),
}))
.unwrap();
}
/// Toggle a tag on this window.
///
/// # Examples
///
/// ```
/// let focused = window.get_focused()?;
/// let tg = tag.get("Potato", None)?;
///
/// // Assume `focused` does not have tag `tg`
///
/// focused.toggle_tag(&tg); // `focused` now has tag "Potato"
/// focused.toggle_tag(&tg); // `focused` no longer has tag "Potato"
/// ```
pub fn toggle_tag(&self, tag: &TagHandle) {
let mut client = self.client.clone();
block_on(client.set_tag(SetTagRequest {
window_id: Some(self.id),
tag_id: Some(tag.id),
set_or_toggle: Some(window::v0alpha1::set_tag_request::SetOrToggle::Toggle(())),
}))
.unwrap();
}
/// Get all properties of this window.
///
/// # Examples
///
/// ```
/// use pinnacle_api::window::WindowProperties;
///
/// let WindowProperties {
/// geometry,
/// class,
/// title,
/// focused,
/// floating,
/// fullscreen_or_maximized,
/// tags,
/// } = window.get_focused()?.props();
/// ```
pub fn props(&self) -> WindowProperties {
let mut client = self.client.clone();
let tag_client = self.tag_client.clone();
let response = block_on(
client.get_properties(window::v0alpha1::GetPropertiesRequest {
window_id: Some(self.id),
}),
)
.unwrap()
.into_inner();
let fullscreen_or_maximized = response
.fullscreen_or_maximized
.unwrap_or_default()
.try_into()
.ok();
let geometry = response.geometry.map(|geo| Geometry {
x: geo.x(),
y: geo.y(),
width: geo.width() as u32,
height: geo.height() as u32,
});
WindowProperties {
size,
loc,
class,
title,
focused,
floating,
geometry,
class: response.class,
title: response.title,
focused: response.focused,
floating: response.floating,
fullscreen_or_maximized,
tags: response
.tag_ids
.into_iter()
.map(|id| TagHandle {
client: tag_client.clone(),
output_client: self.output_client.clone(),
id,
})
.collect(),
}
}
/// Toggle `tag` on this window.
pub fn toggle_tag(&self, tag: &TagHandle) {
let msg = Msg::ToggleTagOnWindow {
window_id: self.0,
tag_id: tag.0,
};
send_msg(msg).unwrap();
/// Get this window's location and size.
///
/// Shorthand for `self.props().geometry`.
pub fn geometry(&self) -> Option<Geometry> {
self.props().geometry
}
/// Move this window to `tag`.
/// Get this window's class.
///
/// This will remove all other tags on this window.
pub fn move_to_tag(&self, tag: &TagHandle) {
let msg = Msg::MoveWindowToTag {
window_id: self.0,
tag_id: tag.0,
};
/// Shorthand for `self.props().class`.
pub fn class(&self) -> Option<String> {
self.props().class
}
send_msg(msg).unwrap();
/// Get this window's title.
///
/// Shorthand for `self.props().title`.
pub fn title(&self) -> Option<String> {
self.props().title
}
/// Get whether or not this window is focused.
///
/// Shorthand for `self.props().focused`.
pub fn focused(&self) -> Option<bool> {
self.props().focused
}
/// Get whether or not this window is floating.
///
/// Shorthand for `self.props().floating`.
pub fn floating(&self) -> Option<bool> {
self.props().floating
}
/// Get whether this window is fullscreen, maximized, or neither.
///
/// Shorthand for `self.props().fullscreen_or_maximized`.
pub fn fullscreen_or_maximized(&self) -> Option<FullscreenOrMaximized> {
self.props().fullscreen_or_maximized
}
/// Get all the tags on this window.
///
/// Shorthand for `self.props().tags`.
pub fn tags(&self) -> Vec<TagHandle> {
self.props().tags
}
}
/// Whether or not a window is floating or tiled.
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize)]
pub enum FloatingOrTiled {
/// The window is floating.
///
/// It can be freely moved around and resized and will not respond to layouts.
Floating,
/// The window is tiled.
///
/// It cannot be resized and can only move by swapping places with other tiled windows.
Tiled,
}
/// Whether the window is fullscreen, maximized, or neither.
///
/// These three states are mutually exclusive. Setting a window to maximized while it is fullscreen
/// will make it stop being fullscreen and vice versa.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum FullscreenOrMaximized {
/// The window is not fullscreen or maximized.
Neither,
/// The window is fullscreen.
///
/// It will be the only rendered window on screen and will fill the output it resides on.
/// Layer surfaces will also not be rendered while a window is fullscreen.
Fullscreen,
/// The window is maximized.
///
/// It will fill up as much space on its output as it can, respecting any layer surfaces.
Maximized,
}

View file

@ -1,83 +1,222 @@
//! Window rules.
//! Types for window rules.
//!
//! A window rule is a way to set the properties of a window on open.
//!
//! They are comprised of two parts: the [condition][WindowRuleCondition] and the actual [rule][WindowRule].
//!
//! # [`WindowRuleCondition`]s
//! `WindowRuleCondition`s are conditions that the window needs to open with in order to apply a
//! rule. For example, you may want to set a window to maximized if it has the class "steam", or
//! you might want to open all Firefox instances on tag "3".
//!
//! To do this, you must build a `WindowRuleCondition` to tell the compositor when to apply any
//! rules.
//!
//! ## Building `WindowRuleCondition`s
//! A condition is created through [`WindowRuleCondition::new`]:
//! ```
//! let cond = WindowRuleCondition::new();
//! ```
//!
//! In order to understand conditions, you must understand the concept of "any" and "all".
//!
//! **"Any"**
//!
//! "Any" conditions only need one of their constituent items to be true for the whole condition to
//! evaluate to true. Think of it as one big `if a || b || c || d || ... {}` block.
//!
//! **"All"**
//!
//! "All" conditions need *all* of their constituent items to be true for the condition to evaluate
//! to true. This is like a big `if a && b && c && d && ... {}` block.
//!
//! Note that any items in a top level `WindowRuleCondition` fall under "all", so all those items
//! must be true.
//!
//! With that out of the way, we can get started building conditions.
//!
//! ### `WindowRuleCondition::classes`
//! With [`WindowRuleCondition::classes`], you can specify what classes a window needs to have for
//! a rule to apply.
//!
//! The following will apply to windows with the class "firefox":
//! ```
//! let cond = WindowRuleCondition::new().classes(["firefox"]);
//! ```
//!
//! Note that you pass in some `impl IntoIterator<Item = impl Into<String>>`. This means you can
//! pass in more than one class here:
//! ```
//! let failing_cond = WindowRuleCondition::new().classes(["firefox", "steam"]);
//! ```
//! *HOWEVER*: this will not work. Recall that top level conditions are implicitly "all". This
//! means the above would require windows to have *both classes*, which is impossible. Thus, the
//! condition above will never be true.
//!
//! ### `WindowRuleCondition::titles`
//! Like `classes`, you can use `titles` to specify that the window needs to open with a specific
//! title for the condition to apply.
//!
//! ```
//! let cond = WindowRuleCondition::new().titles(["Steam"]);
//! ```
//!
//! Like `classes`, passing in multiple titles at the top level will cause the condition to always
//! fail.
//!
//! ### `WindowRuleCondition::tags`
//! You can specify that the window needs to open on the given tags in order to apply a rule.
//!
//! ```
//! let cond = WindowRuleCondition::new().tags([&tag.get("3", output.get_by_name("HDMI-1")?)?]);
//! ```
//!
//! Here, if you have tag "3" active on "HDMI-1" and spawn a window on that output, this condition
//! will apply.
//!
//! Unlike `classes` and `titles`, you can specify multiple tags at the top level:
//!
//! ```
//! let op = output.get_by_name("HDMI-1")?;
//! let tag1 = tag.get("1", &op)?;
//! let tag2 = tag.get("2", &op)?;
//!
//! let cond = WindowRuleCondition::new().tags([&tag1, &tag2]);
//! ```
//!
//! Now, you must have both tags "1" and "2" active and spawn a window for the condition to apply.
//!
//! ### `WindowRuleCondition::any`
//! Now we can get to ways to compose more complex conditions.
//!
//! `WindowRuleCondition::any` takes in conditions and will evaluate to true if *anything* in those
//! conditions are true.
//!
//! ```
//! let cond = WindowRuleCondition::new()
//! .any([
//! WindowRuleCondition::new().classes(["Alacritty"]),
//! WindowRuleCondition::new().tags([&tag.get("2", None)?]),
//! ]);
//! ```
//!
//! This condition will apply if the window is *either* "Alacritty" *or* opens on tag "2".
//!
//! ### `WindowRuleCondition::all`
//! With `WindowRuleCondition::all`, *all* specified conditions must be true for the condition to
//! be true.
//!
//! ```
//! let cond = WindowRuleCondition::new()
//! .all([
//! WindowRuleCondition::new().classes(["Alacritty"]),
//! WindowRuleCondition::new().tags([&tag.get("2", None)?]),
//! ]);
//! ```
//!
//! This condition applies if the window has the class "Alacritty" *and* opens on tag "2".
//!
//! You can write the above a bit shorter, as top level conditions are already "all":
//!
//! ```
//! let cond = WindowRuleCondition::new()
//! .classes(["Alacritty"])
//! .tags([&tag.get("2", None)?]);
//! ```
//!
//! ## Complex condition composition
//! You can arbitrarily nest `any` and `all` to achieve desired logic.
//!
//! ```
//! let op = output.get_by_name("HDMI-1")?;
//! let tag1 = tag.get("1", &op)?;
//! let tag2 = tag.get("2", &op)?;
//!
//! let complex_cond = WindowRuleCondition::new()
//! .any([
//! WindowRuleCondition::new().all([
//! WindowRuleCondition::new()
//! .classes("Alacritty")
//! .tags([&tag1, &tag2])
//! ]),
//! WindowRuleCondition::new().all([
//! WindowRuleCondition::new().any([
//! WindowRuleCondition::new().titles(["nvim", "emacs", "nano"]),
//! ]),
//! WindowRuleCondition::new().any([
//! WindowRuleCondition::new().tags([&tag1, &tag2]),
//! ]),
//! ])
//! ])
//! ```
//!
//! The above is true if either of the following are true:
//! - The window has class "Alacritty" and opens on both tags "1" and "2", or
//! - The window's class is either "nvim", "emacs", or "nano" *and* it opens on either tag "1" or
//! "2".
//!
//! # [`WindowRule`]s
//! `WindowRuleCondition`s are half of a window rule. The other half is the [`WindowRule`] itself.
//!
//! A `WindowRule` is what will apply to a window if a condition is true.
//!
//! ## Building `WindowRule`s
//!
//! Create a new window rule with [`WindowRule::new`]:
//!
//! ```
//! let rule = WindowRule::new();
//! ```
//!
//! There are several rules you can set currently.
//!
//! ### [`WindowRule::output`]
//! This will cause the window to open on the specified output.
//!
//! ### [`WindowRule::tags`]
//! This will cause the window to open with the given tags.
//!
//! ### [`WindowRule::floating`]
//! This will cause the window to open either floating or tiled.
//!
//! ### [`WindowRule::fullscreen_or_maximized`]
//! This will cause the window to open either fullscreen, maximized, or neither.
//!
//! ### [`WindowRule::x`]
//! This will cause the window to open at the given x-coordinate.
//!
//! Note: this only applies to floating windows; tiled windows' geometry will be overridden by
//! layouting.
//!
//! ### [`WindowRule::y`]
//! This will cause the window to open at the given y-coordinate.
//!
//! Note: this only applies to floating windows; tiled windows' geometry will be overridden by
//! layouting.
//!
//! ### [`WindowRule::width`]
//! This will cause the window to open with the given width in pixels.
//!
//! Note: this only applies to floating windows; tiled windows' geometry will be overridden by
//! layouting.
//!
//! ### [`WindowRule::height`]
//! This will cause the window to open with the given height in pixels.
//!
//! Note: this only applies to floating windows; tiled windows' geometry will be overridden by
//! layouting.
use std::num::NonZeroU32;
use pinnacle_api_defs::pinnacle::window;
use crate::{msg::Msg, output::OutputHandle, send_msg, tag::TagHandle};
use crate::{output::OutputHandle, tag::TagHandle};
use super::{FloatingOrTiled, FullscreenOrMaximized};
/// Add a window rule.
pub fn add(cond: WindowRuleCondition, rule: WindowRule) {
let msg = Msg::AddWindowRule {
cond: cond.0,
rule: rule.0,
};
send_msg(msg).unwrap();
}
/// A window rule.
///
/// This is what will be applied to a window if it meets a [`WindowRuleCondition`].
///
/// `WindowRule`s are built using the builder pattern.
/// // TODO: show example
#[derive(Default)]
pub struct WindowRule(crate::msg::WindowRule);
impl WindowRule {
/// Create a new, empty window rule.
pub fn new() -> Self {
Default::default()
}
/// This rule will force windows to open on the provided `output`.
pub fn output(mut self, output: &OutputHandle) -> Self {
self.0.output = Some(output.0.clone());
self
}
/// This rule will force windows to open with the provided `tags`.
pub fn tags(mut self, tags: &[TagHandle]) -> Self {
self.0.tags = Some(tags.iter().map(|tag| tag.0).collect());
self
}
/// This rule will force windows to open either floating or tiled.
pub fn floating_or_tiled(mut self, floating_or_tiled: FloatingOrTiled) -> Self {
self.0.floating_or_tiled = Some(floating_or_tiled);
self
}
/// This rule will force windows to open either fullscreen, maximized, or neither.
pub fn fullscreen_or_maximized(
mut self,
fullscreen_or_maximized: FullscreenOrMaximized,
) -> Self {
self.0.fullscreen_or_maximized = Some(fullscreen_or_maximized);
self
}
/// This rule will force windows to open with a specific size.
///
/// This will only actually be visible if the window is also floating.
pub fn size(mut self, width: NonZeroU32, height: NonZeroU32) -> Self {
self.0.size = Some((width, height));
self
}
/// This rule will force windows to open at a specific location.
///
/// This will only actually be visible if the window is also floating.
pub fn location(mut self, x: i32, y: i32) -> Self {
self.0.location = Some((x, y));
self
}
}
use super::FullscreenOrMaximized;
/// A condition for a [`WindowRule`] to apply to a window.
#[derive(Default, Debug)]
pub struct WindowRuleCondition(crate::msg::WindowRuleCondition);
///
/// `WindowRuleCondition`s are built using the builder pattern.
#[derive(Default, Debug, Clone)]
pub struct WindowRuleCondition(pub(super) window::v0alpha1::WindowRuleCondition);
impl WindowRuleCondition {
/// Create a new, empty `WindowRuleCondition`.
@ -86,14 +225,41 @@ impl WindowRuleCondition {
}
/// This condition requires that at least one provided condition is true.
pub fn any(mut self, conds: &[WindowRuleCondition]) -> Self {
self.0.cond_any = Some(conds.iter().map(|cond| cond.0.clone()).collect());
///
/// # Examples
///
/// ```
/// use pinnacle_api::window::rules::WindowRuleCondition;
///
/// // `cond` will be true if the window opens with *either* class "Alacritty" or "firefox"
/// // *or* with title "Steam"
/// let cond = WindowRuleCondition::new()
/// .any([
/// WindowRuleCondition::new().classes(["Alacritty", "firefox"]),
/// WindowRuleCondition::new().titles(["Steam"]).
/// ]);
/// ```
pub fn any(mut self, conds: impl IntoIterator<Item = WindowRuleCondition>) -> Self {
self.0.any = conds.into_iter().map(|cond| cond.0).collect();
self
}
/// This condition requires that all provided conditions are true.
pub fn all(mut self, conds: &[WindowRuleCondition]) -> Self {
self.0.cond_all = Some(conds.iter().map(|cond| cond.0.clone()).collect());
///
/// # Examples
///
/// ```
/// use pinnacle_api::window::rules::WindowRuleCondition;
///
/// // `cond` will be true if the window opens with class "Alacritty" *and* on tag "1"
/// let cond = WindowRuleCondition::new()
/// .any([
/// WindowRuleCondition::new().tags([tag.get("1", None)?]),
/// WindowRuleCondition::new().titles(["Alacritty"]).
/// ]);
/// ```
pub fn all(mut self, conds: impl IntoIterator<Item = WindowRuleCondition>) -> Self {
self.0.all = conds.into_iter().map(|cond| cond.0).collect();
self
}
@ -104,8 +270,26 @@ impl WindowRuleCondition {
///
/// When used in [`WindowRuleCondition::any`], at least one of the
/// provided classes must match.
pub fn class(mut self, classes: &[&str]) -> Self {
self.0.class = Some(classes.iter().map(|s| s.to_string()).collect());
///
/// # Examples
///
/// ```
/// use pinnacle_api::window::rules::WindowRuleCondition;
///
/// // `cond` will be true if the window opens with class "Alacritty"
/// let cond = WindowRuleCondition::new().classes(["Alacritty"]);
///
/// // Top level conditions need all items to be true,
/// // so the following will never be true as windows can't have two classes at once
/// let always_false = WindowRuleCondition::new().classes(["Alacritty", "firefox"]);
///
/// // To make the above work, use [`WindowRuleCondition::any`].
/// // The following will be true if the window is "Alacritty" or "firefox"
/// let any_class = WindowRuleCondition::new()
/// .any([ WindowRuleCondition::new().classes(["Alacritty", "firefox"]) ]);
/// ```
pub fn classes(mut self, classes: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.0.classes = classes.into_iter().map(Into::into).collect();
self
}
@ -116,8 +300,26 @@ impl WindowRuleCondition {
///
/// When used in [`WindowRuleCondition::any`], at least one of the
/// provided titles must match.
pub fn title(mut self, titles: &[&str]) -> Self {
self.0.title = Some(titles.iter().map(|s| s.to_string()).collect());
///
/// # Examples
///
/// ```
/// use pinnacle_api::window::rules::WindowRuleCondition;
///
/// // `cond` will be true if the window opens with title "vim"
/// let cond = WindowRuleCondition::new().titles(["vim"]);
///
/// // Top level conditions need all items to be true,
/// // so the following will never be true as windows can't have two titles at once
/// let always_false = WindowRuleCondition::new().titles(["vim", "emacs"]);
///
/// // To make the above work, use [`WindowRuleCondition::any`].
/// // The following will be true if the window has the title "vim" or "emacs"
/// let any_title = WindowRuleCondition::new()
/// .any([WindowRuleCondition::new().titles(["vim", "emacs"])]);
/// ```
pub fn titles(mut self, titles: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.0.titles = titles.into_iter().map(Into::into).collect();
self
}
@ -128,8 +330,192 @@ impl WindowRuleCondition {
///
/// When used in [`WindowRuleCondition::any`], the window must open on at least
/// one of the given tags.
pub fn tag(mut self, tags: &[TagHandle]) -> Self {
self.0.tag = Some(tags.iter().map(|tag| tag.0).collect());
///
/// # Examples
///
/// ```
/// use pinnacle_api::window::rules::WindowRuleCondition;
///
/// let tag1 = tag.get("1", None)?;
/// let tag2 = tag.get("2", None)?;
///
/// // `cond` will be true if the window opens with tag "1"
/// let cond = WindowRuleCondition::new().tags([&tag1]);
///
/// // Top level conditions need all items to be true,
/// // so the following will be true if the window opens with both tags "1" and "2"
/// let all_tags = WindowRuleCondition::new().tags([&tag1, &tag2]);
///
/// // This does the same as the above
/// let all_tags = WindowRuleCondition::new()
/// .all([WindowRuleCondition::new().tags([&tag1, &tag2])]);
///
/// // The following will be true if the window opens with *either* tag "1" or "2"
/// let any_tag = WindowRuleCondition::new()
/// .any([WindowRuleCondition::new().tags([&tag1, &tag2])]);
/// ```
pub fn tags<'a>(mut self, tags: impl IntoIterator<Item = &'a TagHandle>) -> Self {
self.0.tags = tags.into_iter().map(|tag| tag.id).collect();
self
}
}
/// A window rule.
///
/// This is what will be applied to a window if it meets a [`WindowRuleCondition`].
///
/// `WindowRule`s are built using the builder pattern.
#[derive(Clone, Debug, Default)]
pub struct WindowRule(pub(super) window::v0alpha1::WindowRule);
impl WindowRule {
/// Create a new, empty window rule.
pub fn new() -> Self {
Default::default()
}
/// This rule will force windows to open on the provided `output`.
///
/// # Examples
///
/// ```
/// use pinnacle_api::window::rules::WindowRule;
///
/// // Force the window to open on "HDMI-1"
/// let rule = WindowRule::new().output(output.get_by_name("HDMI-1")?);
/// ```
pub fn output(mut self, output: &OutputHandle) -> Self {
self.0.output = Some(output.name.clone());
self
}
/// This rule will force windows to open with the provided `tags`.
///
/// # Examples
///
/// ```
/// use pinnacle_api::window::rules::WindowRule;
///
/// let op = output.get_by_name("HDMI-1")?;
/// let tag1 = tag.get("1", &op)?;
/// let tag2 = tag.get("2", &op)?;
///
/// // Force the window to open with tags "1" and "2"
/// let rule = WindowRule::new().tags([&tag1, &tag2]);
/// ```
pub fn tags<'a>(mut self, tags: impl IntoIterator<Item = &'a TagHandle>) -> Self {
self.0.tags = tags.into_iter().map(|tag| tag.id).collect();
self
}
/// This rule will force windows to open either floating or not.
///
/// # Examples
///
/// ```
/// use pinnacle_api::window::rules::WindowRule;
///
/// // Force the window to open floating
/// let rule = WindowRule::new().floating(true);
///
/// // Force the window to open tiled
/// let rule = WindowRule::new().floating(false);
/// ```
pub fn floating(mut self, floating: bool) -> Self {
self.0.floating = Some(floating);
self
}
/// This rule will force windows to open either fullscreen, maximized, or neither.
///
/// # Examples
///
/// ```
/// use pinnacle_api::window::rules::WindowRule;
/// use pinnacle_api::window::FullscreenOrMaximized;
///
/// // Force the window to open fullscreen
/// let rule = WindowRule::new().fullscreen_or_maximized(FullscreenOrMaximized::Fullscreen);
///
/// // Force the window to open maximized
/// let rule = WindowRule::new().fullscreen_or_maximized(FullscreenOrMaximized::Maximized);
///
/// // Force the window to open not fullscreen nor maximized
/// let rule = WindowRule::new().fullscreen_or_maximized(FullscreenOrMaximized::Neither);
/// ```
pub fn fullscreen_or_maximized(
mut self,
fullscreen_or_maximized: FullscreenOrMaximized,
) -> Self {
self.0.fullscreen_or_maximized = Some(fullscreen_or_maximized as i32);
self
}
/// This rule will force windows to open at a specific x-coordinate.
///
/// This will only actually be visible if the window is also floating.
///
/// # Examples
///
/// ```
/// use pinnacle_api::window::rules::WindowRule;
///
/// // Force the window to open at x = 480
/// let rule = WindowRule::new().x(480);
/// ```
pub fn x(mut self, x: i32) -> Self {
self.0.x = Some(x);
self
}
/// This rule will force windows to open at a specific y-coordinate.
///
/// This will only actually be visible if the window is also floating.
///
/// # Examples
///
/// ```
/// use pinnacle_api::window::rules::WindowRule;
///
/// // Force the window to open at y = 240
/// let rule = WindowRule::new().y(240);
/// ```
pub fn y(mut self, y: i32) -> Self {
self.0.y = Some(y);
self
}
/// This rule will force windows to open with a specific width.
///
/// This will only actually be visible if the window is also floating.
///
/// # Examples
///
/// ```
/// use pinnacle_api::window::rules::WindowRule;
///
/// // Force the window to open with a width of 500 pixels
/// let rule = WindowRule::new().width(500);
/// ```
pub fn width(mut self, width: u32) -> Self {
self.0.width = Some(width as i32);
self
}
/// This rule will force windows to open with a specific height.
///
/// This will only actually be visible if the window is also floating.
///
/// # Examples
///
/// ```
/// use pinnacle_api::window::rules::WindowRule;
///
/// // Force the window to open with a height of 250 pixels
/// let rule = WindowRule::new().height(250);
/// ```
pub fn height(mut self, height: u32) -> Self {
self.0.height = Some(height as i32);
self
}
}

View file

@ -1,25 +0,0 @@
[package]
name = "pinnacle-api"
version = "0.0.2"
edition = "2021"
authors = ["Ottatop <ottatop1227@gmail.com>"]
description = "The Rust implementation of the Pinnacle compositor's configuration API"
license = "MPL-2.0"
repository = "https://github.com/pinnacle-comp/pinnacle"
keywords = ["compositor", "pinnacle", "api", "config"]
categories = ["api-bindings", "config"]
[dependencies]
pinnacle-api-defs = { path = "../../pinnacle-api-defs" }
pinnacle-api-macros = { path = "./pinnacle-api-macros" }
tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread", "net"] }
async-net = "2.0.0"
async-compat = "0.2.3"
tonic = "0.10.2"
tower = { version = "0.4.13", features = ["util"] }
futures = "0.3.30"
num_enum = "0.7.2"
xkbcommon = "0.7.0"
[workspace]
members = ["pinnacle-api-macros"]

View file

@ -1,383 +0,0 @@
//! 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;
/// A mouse button.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum MouseButton {
/// The left mouse button
Left = 0x110,
/// 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,
}
/// 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 {
/// Perform actions on button press
Press = 1,
/// Perform actions on button release
Release,
}
/// 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>,
}
/// The `Input` struct.
///
/// This struct contains methods that allow you to set key- and mousebinds,
/// change xkeyboard and libinput settings, and change the keyboard's repeat rate.
#[derive(Debug, Clone)]
pub struct Input {
channel: Channel,
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
}
impl Input {
pub(crate) fn new(
channel: Channel,
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
) -> Self {
Self {
channel,
fut_sender,
}
}
fn create_input_client(&self) -> InputServiceClient<Channel> {
InputServiceClient::new(self.channel.clone())
}
/// Set a keybind.
///
/// If called with an already set keybind, it gets replaced.
///
/// You must supply:
/// - `mods`: A list of [`Mod`]s. These must be held down for the keybind to trigger.
/// - `key`: The key that needs to be pressed. This can be anything that implements the [Key] trait:
/// - `char`
/// - `&str` and `String`: This is any name from
/// [xkbcommon-keysyms.h](https://xkbcommon.org/doc/current/xkbcommon-keysyms_8h.html)
/// without the `XKB_KEY_` prefix.
/// - `u32`: The numerical key code from the website above.
/// - A [`keysym`][Keysym] from the [`xkbcommon`] re-export.
/// - `action`: A closure that will be run when the keybind is triggered.
/// - Currently, any captures must be both `Send` and `'static`. If you want to mutate
/// something, consider using channels or [`Box::leak`].
///
/// # Examples
///
/// ```
/// use pinnacle_api::input::Mod;
///
/// // Set `Super + Shift + c` to close the focused window
/// input.keybind([Mod::Super, Mod::Shift], 'c', || {
/// if let Some(win) = window.get_focused() {
/// win.close();
/// }
/// });
///
/// // With a string key
/// input.keybind([], "BackSpace", || { /* ... */ });
///
/// // With a numeric key
/// input.keybind([], 65, || { /* ... */ }); // 65 = 'A'
///
/// // With a `Keysym`
/// input.keybind([], pinnacle_api::xkbcommon::xkb::Keysym::Return, || { /* ... */ });
/// ```
pub fn keybind(
&self,
mods: impl IntoIterator<Item = Mod>,
key: impl Key + Send + 'static,
mut action: impl FnMut() + Send + 'static,
) {
let mut client = self.create_input_client();
let modifiers = mods.into_iter().map(|modif| modif as i32).collect();
self.fut_sender
.unbounded_send(
async move {
let mut stream = client
.set_keybind(SetKeybindRequest {
modifiers,
key: Some(input::v0alpha1::set_keybind_request::Key::RawCode(
key.into_keysym().raw(),
)),
})
.await
.unwrap()
.into_inner();
while let Some(Ok(_response)) = stream.next().await {
action();
}
}
.boxed(),
)
.unwrap();
}
/// Set a mousebind.
///
/// If called with an already set mousebind, it gets replaced.
///
/// You must supply:
/// - `mods`: A list of [`Mod`]s. These must be held down for the keybind to trigger.
/// - `button`: A [`MouseButton`].
/// - `edge`: A [`MouseEdge`]. This allows you to trigger the bind on either mouse press or release.
/// - `action`: A closure that will be run when the mousebind is triggered.
/// - Currently, any captures must be both `Send` and `'static`. If you want to mutate
/// something, consider using channels or [`Box::leak`].
///
/// # Examples
///
/// ```
/// use pinnacle_api::input::{Mod, MouseButton, MouseEdge};
///
/// // Set `Super + left click` to start moving a window
/// input.mousebind([Mod::Super], MouseButton::Left, MouseEdge::Press, || {
/// window.begin_move(MouseButton::Press);
/// });
/// ```
pub fn mousebind(
&self,
mods: impl IntoIterator<Item = Mod>,
button: MouseButton,
edge: MouseEdge,
mut action: impl FnMut() + 'static + Send,
) {
let mut client = self.create_input_client();
let modifiers = mods.into_iter().map(|modif| modif as i32).collect();
self.fut_sender
.unbounded_send(
async move {
let mut stream = client
.set_mousebind(SetMousebindRequest {
modifiers,
button: Some(button as u32),
edge: Some(edge as i32),
})
.await
.unwrap()
.into_inner();
while let Some(Ok(_response)) = stream.next().await {
action();
}
}
.boxed(),
)
.unwrap();
}
/// Set the xkeyboard config.
///
/// This allows you to set several xkeyboard options like `layout` and `rules`.
///
/// See `xkeyboard-config(7)` for more information.
///
/// # Examples
///
/// ```
/// use pinnacle_api::input::XkbConfig;
///
/// input.set_xkb_config(Xkbconfig {
/// layout: Some("us,fr,ge"),
/// options: Some("ctrl:swapcaps,caps:shift"),
/// ..Default::default()
/// });
/// ```
pub fn set_xkb_config(&self, xkb_config: XkbConfig) {
let mut client = self.create_input_client();
block_on(client.set_xkb_config(SetXkbConfigRequest {
rules: xkb_config.rules.map(String::from),
variant: xkb_config.variant.map(String::from),
layout: xkb_config.layout.map(String::from),
model: xkb_config.model.map(String::from),
options: xkb_config.options.map(String::from),
}))
.unwrap();
}
/// Set the keyboard's repeat rate.
///
/// This allows you to set the time between holding down a key and it repeating
/// as well as the time between each repeat.
///
/// Units are in milliseconds.
///
/// # Examples
///
/// ```
/// // Set keyboard to repeat after holding down for half a second,
/// // and repeat once every 25ms (40 times a second)
/// input.set_repeat_rate(25, 500);
/// ```
pub fn set_repeat_rate(&self, rate: i32, delay: i32) {
let mut client = self.create_input_client();
block_on(client.set_repeat_rate(SetRepeatRateRequest {
rate: Some(rate),
delay: Some(delay),
}))
.unwrap();
}
/// Set a libinput setting.
///
/// From [freedesktop.org](https://www.freedesktop.org/wiki/Software/libinput/):
/// > libinput is a library to handle input devices in Wayland compositors
///
/// As such, this method allows you to set various settings related to input devices.
/// This includes things like pointer acceleration and natural scrolling.
///
/// See [`LibinputSetting`] for all the settings you can change.
///
/// Note: currently Pinnacle applies anything set here to *every* device, regardless of what it
/// actually is. This will be fixed in the future.
///
/// # Examples
///
/// ```
/// use pinnacle_api::input::libinput::*;
///
/// // Set pointer acceleration to flat
/// input.set_libinput_setting(LibinputSetting::AccelProfile(AccelProfile::Flat));
///
/// // Enable natural scrolling (reverses scroll direction; usually used with trackpads)
/// input.set_libinput_setting(LibinputSetting::NaturalScroll(true));
/// ```
pub fn set_libinput_setting(&self, setting: LibinputSetting) {
let mut client = self.create_input_client();
let setting = match setting {
LibinputSetting::AccelProfile(profile) => Setting::AccelProfile(profile as i32),
LibinputSetting::AccelSpeed(speed) => Setting::AccelSpeed(speed),
LibinputSetting::CalibrationMatrix(matrix) => {
Setting::CalibrationMatrix(CalibrationMatrix {
matrix: matrix.to_vec(),
})
}
LibinputSetting::ClickMethod(method) => Setting::ClickMethod(method as i32),
LibinputSetting::DisableWhileTyping(disable) => Setting::DisableWhileTyping(disable),
LibinputSetting::LeftHanded(enable) => Setting::LeftHanded(enable),
LibinputSetting::MiddleEmulation(enable) => Setting::MiddleEmulation(enable),
LibinputSetting::RotationAngle(angle) => Setting::RotationAngle(angle),
LibinputSetting::ScrollButton(button) => Setting::RotationAngle(button),
LibinputSetting::ScrollButtonLock(enable) => Setting::ScrollButtonLock(enable),
LibinputSetting::ScrollMethod(method) => Setting::ScrollMethod(method as i32),
LibinputSetting::NaturalScroll(enable) => Setting::NaturalScroll(enable),
LibinputSetting::TapButtonMap(map) => Setting::TapButtonMap(map as i32),
LibinputSetting::TapDrag(enable) => Setting::TapDrag(enable),
LibinputSetting::TapDragLock(enable) => Setting::TapDragLock(enable),
LibinputSetting::Tap(enable) => Setting::Tap(enable),
};
block_on(client.set_libinput_setting(SetLibinputSettingRequest {
setting: Some(setting),
}))
.unwrap();
}
}
/// A trait that designates anything that can be converted into a [`Keysym`].
pub trait Key {
/// Convert this into a [`Keysym`].
fn into_keysym(self) -> Keysym;
}
impl Key for Keysym {
fn into_keysym(self) -> Keysym {
self
}
}
impl Key for char {
fn into_keysym(self) -> Keysym {
Keysym::from_char(self)
}
}
impl Key for &str {
fn into_keysym(self) -> Keysym {
xkbcommon::xkb::keysym_from_name(self, xkbcommon::xkb::KEYSYM_NO_FLAGS)
}
}
impl Key for String {
fn into_keysym(self) -> Keysym {
xkbcommon::xkb::keysym_from_name(&self, xkbcommon::xkb::KEYSYM_NO_FLAGS)
}
}
impl Key for u32 {
fn into_keysym(self) -> Keysym {
Keysym::from(self)
}
}

View file

@ -1,85 +0,0 @@
//! Types for libinput configuration.
/// Pointer acceleration profile
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AccelProfile {
/// A flat acceleration profile.
///
/// 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 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 = 1,
/// The number of fingers decides which button press to generate.
Clickfinger,
}
/// 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 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.
Edge,
/// Send scroll events when a button is down and the device moves along a scroll-capable axis.
OnButtonDown,
}
/// Map 1/2/3 finger tips to buttons.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TapButtonMap {
/// 1/2/3 finger tap maps to left/right/middle
LeftRightMiddle,
/// 1/2/3 finger tap maps to left/middle/right
LeftMiddleRight,
}
/// Possible settings for libinput.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LibinputSetting {
/// Set the pointer acceleration profile
AccelProfile(AccelProfile),
/// Set pointer acceleration speed
AccelSpeed(f64),
/// Set the calibration matrix
CalibrationMatrix([f32; 6]),
/// Set the [`ClickMethod`]
ClickMethod(ClickMethod),
/// Set whether the device gets disabled while typing
DisableWhileTyping(bool),
/// Set left handed mode
LeftHanded(bool),
/// Allow or disallow middle mouse button emulation
MiddleEmulation(bool),
/// Set the rotation angle
RotationAngle(u32),
/// Set the scroll button
ScrollButton(u32),
/// 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),
/// Enable or disable tap-to-drag
TapDrag(bool),
/// Enable or disable a timeout where lifting a finger off the device will not stop dragging
TapDragLock(bool),
/// Enable or disable tap-to-click
Tap(bool),
}

View file

@ -1,202 +0,0 @@
#![warn(missing_docs)]
//! The Rust implementation of [Pinnacle](https://github.com/pinnacle-comp/pinnacle)'s
//! configuration API.
//!
//! This library allows to 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 output;
pub mod pinnacle;
pub mod process;
pub mod tag;
pub mod util;
pub mod window;
pub use pinnacle_api_macros::config;
pub use tokio;
pub use xkbcommon;
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 function is inserted at the top of your config through the [`config`] macro.
/// You should use that macro instead of this function directly.
pub async fn connect(
) -> Result<(ApiModules, UnboundedReceiver<BoxFuture<'static, ()>>), Box<dyn std::error::Error>> {
let channel = Endpoint::try_from("http://[::]:50051")? // port doesn't matter, we use a unix socket
.connect_with_connector(service_fn(|_: Uri| {
tokio::net::UnixStream::connect(
std::env::var("PINNACLE_GRPC_SOCKET")
.expect("PINNACLE_GRPC_SOCKET was not set; is Pinnacle running?"),
)
}))
.await?;
let (fut_sender, fut_recv) = futures::channel::mpsc::unbounded::<BoxFuture<()>>();
let output = Output::new(channel.clone(), fut_sender.clone());
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);
let modules = ApiModules {
pinnacle,
process,
window,
input,
output,
tag,
};
Ok((modules, fut_recv))
}
/// Listen to Pinnacle for incoming messages.
///
/// This will run all futures returned by configuration methods that take in callbacks in order to
/// call them.
///
/// This function is inserted at the end of your config through the [`config`] macro.
/// You should use the macro instead of this function directly.
pub async fn listen(
fut_recv: UnboundedReceiver<BoxFuture<'static, ()>>, // api_modules: ApiModules<'a>,
) {
let mut future_set = FuturesUnordered::<
BoxFuture<(
Option<BoxFuture<()>>,
Option<UnboundedReceiver<BoxFuture<()>>>,
)>,
>::new();
future_set.push(Box::pin(async move {
let (fut, stream) = fut_recv.into_future().await;
(fut, Some(stream))
}));
while let Some((fut, stream)) = future_set.next().await {
if let Some(fut) = fut {
future_set.push(Box::pin(async move {
fut.await;
(None, None)
}));
}
if let Some(stream) = stream {
future_set.push(Box::pin(async move {
let (fut, stream) = stream.into_future().await;
(fut, Some(stream))
}))
}
}
}

View file

@ -1,515 +0,0 @@
//! 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 futures::{
channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture, FutureExt, StreamExt,
};
use pinnacle_api_defs::pinnacle::{
output::{
self,
v0alpha1::{
output_service_client::OutputServiceClient, ConnectForAllRequest, SetLocationRequest,
},
},
tag::v0alpha1::tag_service_client::TagServiceClient,
};
use tonic::transport::Channel;
use crate::tag::TagHandle;
/// A struct that allows you to get handles to connected outputs and set them up.
///
/// See [`OutputHandle`] for more information.
#[derive(Debug, Clone)]
pub struct Output {
channel: Channel,
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
}
impl Output {
pub(crate) fn new(
channel: Channel,
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
) -> Self {
Self {
channel,
fut_sender,
}
}
fn create_output_client(&self) -> OutputServiceClient<Channel> {
OutputServiceClient::new(self.channel.clone())
}
fn create_tag_client(&self) -> TagServiceClient<Channel> {
TagServiceClient::new(self.channel.clone())
}
/// 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();
}
}
/// A handle to an output.
///
/// This allows you to manipulate outputs and get their properties.
#[derive(Clone, Debug)]
pub struct OutputHandle {
pub(crate) client: OutputServiceClient<Channel>,
pub(crate) tag_client: TagServiceClient<Channel>,
pub(crate) name: String,
}
/// The alignment to use for [`OutputHandle::set_loc_adj_to`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Alignment {
/// Set above, align left borders
TopAlignLeft,
/// Set above, align centers
TopAlignCenter,
/// Set above, align right borders
TopAlignRight,
/// Set below, align left borders
BottomAlignLeft,
/// Set below, align centers
BottomAlignCenter,
/// Set below, align right borders
BottomAlignRight,
/// Set to left, align top borders
LeftAlignTop,
/// Set to left, align centers
LeftAlignCenter,
/// Set to left, align bottom borders
LeftAlignBottom,
/// Set to right, align top borders
RightAlignTop,
/// Set to right, align centers
RightAlignCenter,
/// Set to right, align bottom borders
RightAlignBottom,
}
impl OutputHandle {
/// Set the location of this output in the global space.
///
/// On startup, Pinnacle will lay out all connected outputs starting at (0, 0)
/// and going to the right, with their top borders aligned.
///
/// This method allows you to move outputs where necessary.
///
/// Note: If you leave space between two outputs when setting their locations,
/// the pointer will not be able to move between them.
///
/// # Examples
///
/// ```
/// // Assume two monitors in order, "DP-1" and "HDMI-1", with the following dimensions:
/// // - "DP-1": ┌─────┐
/// // │ │1920x1080
/// // └─────┘
/// // - "HDMI-1": ┌───────┐
/// // │ 2560x │
/// // │ 1440 │
/// // └───────┘
///
/// output.get_by_name("DP-1")?.set_location(0, 0);
/// output.get_by_name("HDMI-1")?.set_location(1920, -360);
///
/// // Results in:
/// // x=0 ┌───────┐y=-360
/// // y=0┌─────┤ │
/// // │DP-1 │HDMI-1 │
/// // └─────┴───────┘
/// // ^x=1920
/// ```
pub fn set_location(&self, x: impl Into<Option<i32>>, y: impl Into<Option<i32>>) {
let mut client = self.client.clone();
block_on(client.set_location(SetLocationRequest {
output_name: Some(self.name.clone()),
x: x.into(),
y: y.into(),
}))
.unwrap();
}
/// Set this output adjacent to another one.
///
/// This is a helper method over [`OutputHandle::set_location`] to make laying out outputs
/// easier.
///
/// `alignment` is an [`Alignment`] of how you want this output to be placed.
/// For example, [`TopAlignLeft`][Alignment::TopAlignLeft] will place this output
/// above `other` and align the left borders.
/// Similarly, [`RightAlignCenter`][Alignment::RightAlignCenter] will place this output
/// to the right of `other` and align their centers.
///
/// # Examples
///
/// ```
/// use pinnacle_api::output::Alignment;
///
/// // Assume two monitors in order, "DP-1" and "HDMI-1", with the following dimensions:
/// // - "DP-1": ┌─────┐
/// // │ │1920x1080
/// // └─────┘
/// // - "HDMI-1": ┌───────┐
/// // │ 2560x │
/// // │ 1440 │
/// // └───────┘
///
/// output.get_by_name("DP-1")?.set_loc_adj_to(output.get_by_name("HDMI-1")?, Alignment::BottomAlignRight);
///
/// // Results in:
/// // ┌───────┐
/// // │ │
/// // │HDMI-1 │
/// // └──┬────┤
/// // │DP-1│
/// // └────┘
/// // Notice that "DP-1" now has the coordinates (2280, 1440) because "DP-1" is getting moved, not "HDMI-1".
/// // "HDMI-1" was placed at (1920, 0) during the compositor's initial output layout.
/// ```
pub fn set_loc_adj_to(&self, other: &OutputHandle, alignment: Alignment) {
let self_props = self.props();
let other_props = other.props();
// poor man's try {}
let attempt_set_loc = || -> Option<()> {
let other_x = other_props.x?;
let other_y = other_props.y?;
let other_width = other_props.pixel_width? as i32;
let other_height = other_props.pixel_height? as i32;
let self_width = self_props.pixel_width? as i32;
let self_height = self_props.pixel_height? as i32;
use Alignment::*;
let x: i32;
let y: i32;
if let TopAlignLeft | TopAlignCenter | TopAlignRight | BottomAlignLeft
| BottomAlignCenter | BottomAlignRight = alignment
{
if let TopAlignLeft | TopAlignCenter | TopAlignRight = alignment {
y = other_y - self_height;
} else {
// bottom
y = other_y + other_height;
}
match alignment {
TopAlignLeft | BottomAlignLeft => x = other_x,
TopAlignCenter | BottomAlignCenter => {
x = other_x + (other_width - self_width) / 2;
}
TopAlignRight | BottomAlignRight => x = other_x + (other_width - self_width),
_ => unreachable!(),
}
} else {
if let LeftAlignTop | LeftAlignCenter | LeftAlignBottom = alignment {
x = other_x - self_width;
} else {
x = other_x + other_width;
}
match alignment {
LeftAlignTop | RightAlignTop => y = other_y,
LeftAlignCenter | RightAlignCenter => {
y = other_y + (other_height - self_height) / 2;
}
LeftAlignBottom | RightAlignBottom => {
y = other_y + (other_height - self_height);
}
_ => unreachable!(),
}
}
self.set_location(Some(x), Some(y));
Some(())
};
attempt_set_loc();
}
/// Get all properties of this output.
///
/// # Examples
///
/// ```
/// use pinnacle_api::output::OutputProperties;
///
/// let OutputProperties {
/// make,
/// model,
/// x,
/// y,
/// pixel_width,
/// pixel_height,
/// refresh_rate,
/// physical_width,
/// physical_height,
/// focused,
/// tags,
/// } = output.get_focused()?.props();
/// ```
pub fn props(&self) -> OutputProperties {
let mut client = self.client.clone();
let response = block_on(
client.get_properties(output::v0alpha1::GetPropertiesRequest {
output_name: Some(self.name.clone()),
}),
)
.unwrap()
.into_inner();
OutputProperties {
make: response.make,
model: response.model,
x: response.x,
y: response.y,
pixel_width: response.pixel_width,
pixel_height: response.pixel_height,
refresh_rate: response.refresh_rate,
physical_width: response.physical_width,
physical_height: response.physical_height,
focused: response.focused,
tags: response
.tag_ids
.into_iter()
.map(|id| TagHandle {
client: self.tag_client.clone(),
output_client: self.client.clone(),
id,
})
.collect(),
}
}
// TODO: make a macro for the following or something
/// Get this output's make.
///
/// Shorthand for `self.props().make`.
pub fn make(&self) -> Option<String> {
self.props().make
}
/// Get this output's model.
///
/// Shorthand for `self.props().make`.
pub fn model(&self) -> Option<String> {
self.props().model
}
/// Get this output's x position in the global space.
///
/// Shorthand for `self.props().x`.
pub fn x(&self) -> Option<i32> {
self.props().x
}
/// Get this output's y position in the global space.
///
/// Shorthand for `self.props().y`.
pub fn y(&self) -> Option<i32> {
self.props().y
}
/// Get this output's screen width in pixels.
///
/// Shorthand for `self.props().pixel_width`.
pub fn pixel_width(&self) -> Option<u32> {
self.props().pixel_width
}
/// Get this output's screen height in pixels.
///
/// Shorthand for `self.props().pixel_height`.
pub fn pixel_height(&self) -> Option<u32> {
self.props().pixel_height
}
/// Get this output's refresh rate in millihertz.
///
/// For example, 144Hz will be returned as 144000.
///
/// Shorthand for `self.props().refresh_rate`.
pub fn refresh_rate(&self) -> Option<u32> {
self.props().refresh_rate
}
/// Get this output's physical width in millimeters.
///
/// Shorthand for `self.props().physical_width`.
pub fn physical_width(&self) -> Option<u32> {
self.props().physical_width
}
/// Get this output's physical height in millimeters.
///
/// Shorthand for `self.props().physical_height`.
pub fn physical_height(&self) -> Option<u32> {
self.props().physical_height
}
/// 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
}
}
/// The properties of an output.
#[derive(Clone, Debug)]
pub struct OutputProperties {
/// The make of the output
pub make: Option<String>,
/// The model of the output
///
/// This is something like "27GL83A" or whatever crap monitor manufacturers name their monitors
/// these days.
pub model: Option<String>,
/// The x position of the output in the global space
pub x: Option<i32>,
/// The y position of the output in the global space
pub y: Option<i32>,
/// The output's screen width in pixels
pub pixel_width: Option<u32>,
/// The output's screen height in pixels
pub pixel_height: Option<u32>,
/// The output's refresh rate in millihertz
pub refresh_rate: Option<u32>,
/// The output's physical width in millimeters
pub physical_width: Option<u32>,
/// The output's physical height in millimeters
pub physical_height: Option<u32>,
/// Whether this output is focused or not
///
/// This is currently implemented as the output with the most recent pointer motion.
pub focused: Option<bool>,
/// The tags this output has
pub tags: Vec<TagHandle>,
}

View file

@ -1,178 +0,0 @@
//! Process management.
//!
//! This module provides [`Process`], which allows you to spawn processes and set environment
//! variables.
use futures::{
channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture, FutureExt, StreamExt,
};
use pinnacle_api_defs::pinnacle::process::v0alpha1::{
process_service_client::ProcessServiceClient, SetEnvRequest, SpawnRequest,
};
use tonic::transport::Channel;
/// A struct containing methods to spawn processes with optional callbacks and set environment
/// variables.
#[derive(Debug, Clone)]
pub struct Process {
channel: Channel,
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
}
/// Optional callbacks to be run when a spawned process prints to stdout or stderr or exits.
pub struct SpawnCallbacks {
/// A callback that will be run when a process prints to stdout with a line
pub stdout: Option<Box<dyn FnMut(String) + Send>>,
/// A callback that will be run when a process prints to stderr with a line
pub stderr: Option<Box<dyn FnMut(String) + Send>>,
/// A callback that will be run when a process exits with a status code and message
#[allow(clippy::type_complexity)]
pub exit: Option<Box<dyn FnMut(Option<i32>, String) + Send>>,
}
impl Process {
pub(crate) fn new(
channel: Channel,
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
) -> Process {
Self {
channel,
fut_sender,
}
}
fn create_process_client(&self) -> ProcessServiceClient<Channel> {
ProcessServiceClient::new(self.channel.clone())
}
/// Spawn a process.
///
/// Note that windows spawned *before* tags are added will not be displayed.
/// This will be changed in the future to be more like Awesome, where windows with no tags are
/// displayed on every tag instead.
///
/// # Examples
///
/// ```
/// process.spawn(["alacritty"]);
/// process.spawn(["bash", "-c", "swaybg -i ~/path_to_wallpaper"]);
/// ```
pub fn spawn(&self, args: impl IntoIterator<Item = impl Into<String>>) {
self.spawn_inner(args, false, None);
}
/// Spawn a process with callbacks for its stdout, stderr, and exit information.
///
/// See [`SpawnCallbacks`] for the passed in struct.
///
/// Note that windows spawned *before* tags are added will not be displayed.
/// This will be changed in the future to be more like Awesome, where windows with no tags are
/// displayed on every tag instead.
///
/// # Examples
///
/// ```
/// use pinnacle_api::process::SpawnCallbacks;
///
/// process.spawn_with_callbacks(["alacritty"], SpawnCallbacks {
/// stdout: Some(Box::new(|line| println!("stdout: {line}"))),
/// stderr: Some(Box::new(|line| println!("stderr: {line}"))),
/// stdout: Some(Box::new(|code, msg| println!("exit code: {code:?}, exit_msg: {msg}"))),
/// });
/// ```
pub fn spawn_with_callbacks(
&self,
args: impl IntoIterator<Item = impl Into<String>>,
callbacks: SpawnCallbacks,
) {
self.spawn_inner(args, false, Some(callbacks));
}
/// Spawn a process only if it isn't already running.
///
/// This is useful for startup programs.
///
/// See [`Process::spawn`] for details.
pub fn spawn_once(&self, args: impl IntoIterator<Item = impl Into<String>>) {
self.spawn_inner(args, true, None);
}
/// Spawn a process only if it isn't already running with optional callbacks for its stdout,
/// stderr, and exit information.
///
/// This is useful for startup programs.
///
/// See [`Process::spawn_with_callbacks`] for details.
pub fn spawn_once_with_callbacks(
&self,
args: impl IntoIterator<Item = impl Into<String>>,
callbacks: SpawnCallbacks,
) {
self.spawn_inner(args, true, Some(callbacks));
}
fn spawn_inner(
&self,
args: impl IntoIterator<Item = impl Into<String>>,
once: bool,
callbacks: Option<SpawnCallbacks>,
) {
let mut client = self.create_process_client();
let args = args.into_iter().map(Into::into).collect::<Vec<_>>();
let request = SpawnRequest {
args,
once: Some(once),
has_callback: Some(callbacks.is_some()),
};
self.fut_sender
.unbounded_send(
async move {
let mut stream = client.spawn(request).await.unwrap().into_inner();
let Some(mut callbacks) = callbacks else { return };
while let Some(Ok(response)) = stream.next().await {
if let Some(line) = response.stdout {
if let Some(stdout) = callbacks.stdout.as_mut() {
stdout(line);
}
}
if let Some(line) = response.stderr {
if let Some(stderr) = callbacks.stderr.as_mut() {
stderr(line);
}
}
if let Some(exit_msg) = response.exit_message {
if let Some(exit) = callbacks.exit.as_mut() {
exit(response.exit_code, exit_msg);
}
}
}
}
.boxed(),
)
.unwrap();
}
/// Set an environment variable for the compositor.
/// This will cause any future spawned processes to have this environment variable.
///
/// # Examples
///
/// ```
/// process.set_env("ENV", "a value lalala");
/// ```
pub fn set_env(&self, key: impl Into<String>, value: impl Into<String>) {
let key = key.into();
let value = value.into();
let mut client = self.create_process_client();
block_on(client.set_env(SetEnvRequest {
key: Some(key),
value: Some(value),
}))
.unwrap();
}
}

View file

@ -1,528 +0,0 @@
//! 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,
sync::{Arc, Mutex},
};
use futures::{channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture};
use num_enum::TryFromPrimitive;
use pinnacle_api_defs::pinnacle::{
output::v0alpha1::output_service_client::OutputServiceClient,
tag::{
self,
v0alpha1::{
tag_service_client::TagServiceClient, AddRequest, RemoveRequest, SetActiveRequest,
SetLayoutRequest, SwitchToRequest,
},
},
};
use tonic::transport::Channel;
use crate::output::{Output, OutputHandle};
/// A struct that allows you to add and remove tags and get [`TagHandle`]s.
#[derive(Clone, Debug)]
pub struct Tag {
channel: Channel,
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
}
impl Tag {
pub(crate) fn new(
channel: Channel,
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
) -> Self {
Self {
channel,
fut_sender,
}
}
fn create_tag_client(&self) -> TagServiceClient<Channel> {
TagServiceClient::new(self.channel.clone())
}
fn create_output_client(&self) -> OutputServiceClient<Channel> {
OutputServiceClient::new(self.channel.clone())
}
/// Add tags to the specified output.
///
/// This will add tags with the given names to `output` and return [`TagHandle`]s to all of
/// them.
///
/// # Examples
///
/// ```
/// // Add tags 1-5 to the focused output
/// if let Some(op) = output.get_focused() {
/// let tags = tag.add(&op, ["1", "2", "3", "4", "5"]);
/// }
/// ```
pub fn add(
&self,
output: &OutputHandle,
tag_names: impl IntoIterator<Item = impl Into<String>>,
) -> impl Iterator<Item = TagHandle> {
let mut client = self.create_tag_client();
let output_client = self.create_output_client();
let tag_names = tag_names.into_iter().map(Into::into).collect();
let response = block_on(client.add(AddRequest {
output_name: Some(output.name.clone()),
tag_names,
}))
.unwrap()
.into_inner();
response.tag_ids.into_iter().map(move |id| TagHandle {
client: client.clone(),
output_client: output_client.clone(),
id,
})
}
/// Get handles to all tags across all outputs.
///
/// # Examples
///
/// ```
/// let all_tags = tag.get_all();
/// ```
pub fn get_all(&self) -> impl Iterator<Item = TagHandle> {
let mut client = self.create_tag_client();
let output_client = self.create_output_client();
let response = block_on(client.get(tag::v0alpha1::GetRequest {}))
.unwrap()
.into_inner();
response.tag_ids.into_iter().map(move |id| TagHandle {
client: client.clone(),
output_client: output_client.clone(),
id,
})
}
/// Get a handle to the first tag with the given name on `output`.
///
/// If `output` is `None`, the focused output will be used.
///
/// # Examples
///
/// ```
/// // Get tag "1" on output "HDMI-1"
/// if let Some(op) = output.get_by_name("HDMI-1") {
/// let tg = tag.get("1", &op);
/// }
///
/// // Get tag "Thing" on the focused output
/// let tg = tag.get("Thing", None);
/// ```
pub fn get<'a>(
&self,
name: impl Into<String>,
output: impl Into<Option<&'a OutputHandle>>,
) -> Option<TagHandle> {
let name = name.into();
let output: Option<&OutputHandle> = output.into();
let output_module = Output::new(self.channel.clone(), self.fut_sender.clone());
self.get_all().find(|tag| {
let props = tag.props();
let same_tag_name = props.name.as_ref() == Some(&name);
let same_output = props.output.is_some_and(|op| {
Some(op.name)
== output
.map(|o| o.name.clone())
.or_else(|| output_module.get_focused().map(|o| o.name))
});
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 prev = move |output: Option<&OutputHandle>| {
let Some(output) = output
.cloned()
.or_else(|| output_module_clone.get_focused())
else {
return;
};
let Some(first_tag) = output
.props()
.tags
.into_iter()
.find(|tag| tag.active() == Some(true))
else {
return;
};
let mut indices = indices_clone.lock().expect("layout next mutex lock failed");
let index = indices.entry(first_tag.id).or_insert(0);
if index.checked_sub(1).is_none() {
*index = len - 1;
} else {
*index -= 1;
}
first_tag.set_layout(layouts_clone[*index]);
};
LayoutCycler {
prev: Box::new(prev),
next: Box::new(next),
}
}
}
/// A layout cycler that keeps track of tags and their layouts and provides functions to cycle
/// layouts on them.
#[allow(clippy::type_complexity)]
pub struct LayoutCycler {
/// Cycle to the next layout on the given output, or the focused output if `None`.
pub prev: Box<dyn Fn(Option<&OutputHandle>) + Send + Sync + 'static>,
/// Cycle to the previous layout on the given output, or the focused output if `None`.
pub next: Box<dyn Fn(Option<&OutputHandle>) + Send + Sync + 'static>,
}
/// A handle to a tag.
///
/// This handle allows you to do things like switch to tags and get their properties.
#[derive(Debug, Clone)]
pub struct TagHandle {
pub(crate) client: TagServiceClient<Channel>,
pub(crate) output_client: OutputServiceClient<Channel>,
pub(crate) id: u32,
}
/// Various static layouts.
#[repr(i32)]
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, TryFromPrimitive)]
pub enum Layout {
/// One master window on the left with all other windows stacked to the right
MasterStack = 1,
/// Windows split in half towards the bottom right corner
Dwindle,
/// Windows split in half in a spiral
Spiral,
/// One main corner window in the top left with a column of windows on the right and a row on the bottom
CornerTopLeft,
/// One main corner window in the top right with a column of windows on the left and a row on the bottom
CornerTopRight,
/// One main corner window in the bottom left with a column of windows on the right and a row on the top.
CornerBottomLeft,
/// One main corner window in the bottom right with a column of windows on the left and a row on the top.
CornerBottomRight,
}
impl TagHandle {
/// Activate this tag and deactivate all other ones on the same output.
///
/// This essentially emulates what a traditional workspace is.
///
/// # Examples
///
/// ```
/// // Assume the focused output has the following inactive tags and windows:
/// // "1": Alacritty
/// // "2": Firefox, Discord
/// // "3": Steam
/// tag.get("2")?.switch_to(); // Displays Firefox and Discord
/// tag.get("3")?.switch_to(); // Displays Steam
/// ```
pub fn switch_to(&self) {
let mut client = self.client.clone();
block_on(client.switch_to(SwitchToRequest {
tag_id: Some(self.id),
}))
.unwrap();
}
/// Set this tag to active or not.
///
/// While active, windows with this tag will be displayed.
///
/// While inactive, windows with this tag will not be displayed unless they have other active
/// tags.
///
/// # Examples
///
/// ```
/// // Assume the focused output has the following inactive tags and windows:
/// // "1": Alacritty
/// // "2": Firefox, Discord
/// // "3": Steam
/// tag.get("2")?.set_active(true); // Displays Firefox and Discord
/// tag.get("3")?.set_active(true); // Displays Firefox, Discord, and Steam
/// tag.get("2")?.set_active(false); // Displays Steam
/// ```
pub fn set_active(&self, set: bool) {
let mut client = self.client.clone();
block_on(client.set_active(SetActiveRequest {
tag_id: Some(self.id),
set_or_toggle: Some(tag::v0alpha1::set_active_request::SetOrToggle::Set(set)),
}))
.unwrap();
}
/// Toggle this tag between active and inactive.
///
/// While active, windows with this tag will be displayed.
///
/// While inactive, windows with this tag will not be displayed unless they have other active
/// tags.
///
/// # Examples
///
/// ```
/// // Assume the focused output has the following inactive tags and windows:
/// // "1": Alacritty
/// // "2": Firefox, Discord
/// // "3": Steam
/// tag.get("2")?.toggle(); // Displays Firefox and Discord
/// tag.get("3")?.toggle(); // Displays Firefox, Discord, and Steam
/// tag.get("3")?.toggle(); // Displays Firefox, Discord
/// tag.get("2")?.toggle(); // Displays nothing
/// ```
pub fn toggle_active(&self) {
let mut client = self.client.clone();
block_on(client.set_active(SetActiveRequest {
tag_id: Some(self.id),
set_or_toggle: Some(tag::v0alpha1::set_active_request::SetOrToggle::Toggle(())),
}))
.unwrap();
}
/// Remove this tag from its output.
///
/// # Examples
///
/// ```
/// let tags = tag
/// .add(output.get_by_name("DP-1")?, ["1", "2", "Buckle", "Shoe"])
/// .collect::<Vec<_>>;
///
/// tags[1].remove();
/// tags[3].remove();
/// // "DP-1" now only has tags "1" and "Buckle"
/// ```
pub fn remove(mut self) {
block_on(self.client.remove(RemoveRequest {
tag_ids: vec![self.id],
}))
.unwrap();
}
/// Set this tag's layout.
///
/// Layouting only applies to tiled windows (windows that are not floating, maximized, or
/// fullscreen). If multiple tags are active on an output, the first active tag's layout will
/// determine the layout strategy.
///
/// See [`Layout`] for the different static layouts Pinnacle currently has to offer.
///
/// # Examples
///
/// ```
/// use pinnacle_api::tag::Layout;
///
/// // Set the layout of tag "1" on the focused output to "corner top left".
/// tag.get("1", None)?.set_layout(Layout::CornerTopLeft);
/// ```
pub fn set_layout(&self, layout: Layout) {
let mut client = self.client.clone();
block_on(client.set_layout(SetLayoutRequest {
tag_id: Some(self.id),
layout: Some(layout as i32),
}))
.unwrap();
}
/// Get all properties of this tag.
///
/// # Examples
///
/// ```
/// use pinnacle_api::tag::TagProperties;
///
/// let TagProperties {
/// active,
/// name,
/// output,
/// } = tag.get("1", None)?.props();
/// ```
pub fn props(&self) -> TagProperties {
let mut client = self.client.clone();
let output_client = self.output_client.clone();
let response = block_on(client.get_properties(tag::v0alpha1::GetPropertiesRequest {
tag_id: Some(self.id),
}))
.unwrap()
.into_inner();
TagProperties {
active: response.active,
name: response.name,
output: response.output_name.map(|name| OutputHandle {
client: output_client,
tag_client: client,
name,
}),
}
}
/// Get this tag's active status.
///
/// Shorthand for `self.props().active`.
pub fn active(&self) -> Option<bool> {
self.props().active
}
/// Get this tag's name.
///
/// Shorthand for `self.props().name`.
pub fn name(&self) -> Option<String> {
self.props().name
}
/// Get a handle to the output this tag is on.
///
/// Shorthand for `self.props().output`.
pub fn output(&self) -> Option<OutputHandle> {
self.props().output
}
}
/// Properties of a tag.
pub struct TagProperties {
/// Whether the tag is active or not
pub active: Option<bool>,
/// The name of the tag
pub name: Option<String>,
/// The output the tag is on
pub output: Option<OutputHandle>,
}

View file

@ -1,530 +0,0 @@
//! 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;
/// A struct containing methods that get [`WindowHandle`]s and move windows with the mouse.
///
/// See [`WindowHandle`] for more information.
#[derive(Debug, Clone)]
pub struct Window {
channel: Channel,
}
impl Window {
pub(crate) fn new(channel: Channel) -> Self {
Self { channel }
}
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.
///
/// This will begin moving the window under the pointer using the specified [`MouseButton`].
/// The button must be held down at the time this method is called for the move to start.
///
/// This is intended to be used with [`Input::keybind`][crate::input::Input::keybind].
///
/// # Examples
///
/// ```
/// use pinnacle_api::input::{Mod, MouseButton, MouseEdge};
///
/// // Set `Super + left click` to begin moving a window
/// input.mousebind([Mod::Super], MouseButton::Left, MouseEdge::Press, || {
/// window.begin_move(MouseButton::Left);
/// });
/// ```
pub fn begin_move(&self, button: MouseButton) {
let mut client = self.create_window_client();
block_on(client.move_grab(MoveGrabRequest {
button: Some(button as u32),
}))
.unwrap();
}
/// Start resizing the window with the mouse.
///
/// This will begin resizing the window under the pointer using the specified [`MouseButton`].
/// The button must be held down at the time this method is called for the resize to start.
///
/// This is intended to be used with [`Input::keybind`][crate::input::Input::keybind].
///
/// # Examples
///
/// ```
/// use pinnacle_api::input::{Mod, MouseButton, MouseEdge};
///
/// // Set `Super + right click` to begin moving a window
/// input.mousebind([Mod::Super], MouseButton::Right, MouseEdge::Press, || {
/// window.begin_resize(MouseButton::Right);
/// });
/// ```
pub fn begin_resize(&self, button: MouseButton) {
let mut client = self.create_window_client();
block_on(client.resize_grab(ResizeGrabRequest {
button: Some(button as u32),
}))
.unwrap();
}
/// Get all windows.
///
/// # Examples
///
/// ```
/// let windows = window.get_all();
/// ```
pub fn get_all(&self) -> impl Iterator<Item = WindowHandle> {
let mut client = self.create_window_client();
let tag_client = self.create_tag_client();
let output_client = self.create_output_client();
block_on(client.get(GetRequest {}))
.unwrap()
.into_inner()
.window_ids
.into_iter()
.map(move |id| WindowHandle {
client: client.clone(),
id,
tag_client: tag_client.clone(),
output_client: output_client.clone(),
})
}
/// Get the currently focused window.
///
/// # Examples
///
/// ```
/// let focused_window = window.get_focused()?;
/// ```
pub fn get_focused(&self) -> Option<WindowHandle> {
self.get_all()
.find(|window| matches!(window.props().focused, Some(true)))
}
/// Add a window rule.
///
/// A window rule is a set of criteria that a window must open with.
/// For it to apply, a [`WindowRuleCondition`] must evaluate to true for the window in question.
///
/// TODO:
pub fn add_window_rule(&self, cond: WindowRuleCondition, rule: WindowRule) {
let mut client = self.create_window_client();
block_on(client.add_window_rule(AddWindowRuleRequest {
cond: Some(cond.0),
rule: Some(rule.0),
}))
.unwrap();
}
}
/// A handle to a window.
///
/// This allows you to manipulate the window and get its properties.
#[derive(Debug, Clone)]
pub struct WindowHandle {
pub(crate) client: WindowServiceClient<Channel>,
pub(crate) id: u32,
pub(crate) tag_client: TagServiceClient<Channel>,
pub(crate) output_client: OutputServiceClient<Channel>,
}
/// 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 {
geometry,
class: response.class,
title: response.title,
focused: response.focused,
floating: response.floating,
fullscreen_or_maximized,
tags: response
.tag_ids
.into_iter()
.map(|id| TagHandle {
client: tag_client.clone(),
output_client: self.output_client.clone(),
id,
})
.collect(),
}
}
/// Get this window's location and size.
///
/// Shorthand for `self.props().geometry`.
pub fn geometry(&self) -> Option<Geometry> {
self.props().geometry
}
/// Get this window's class.
///
/// Shorthand for `self.props().class`.
pub fn class(&self) -> Option<String> {
self.props().class
}
/// 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
}
}

View file

@ -1,521 +0,0 @@
//! 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 pinnacle_api_defs::pinnacle::window;
use crate::{output::OutputHandle, tag::TagHandle};
use super::FullscreenOrMaximized;
/// A condition for a [`WindowRule`] to apply to a window.
///
/// `WindowRuleCondition`s are built using the builder pattern.
#[derive(Default, Debug, Clone)]
pub struct WindowRuleCondition(pub(super) window::v0alpha1::WindowRuleCondition);
impl WindowRuleCondition {
/// Create a new, empty `WindowRuleCondition`.
pub fn new() -> Self {
Default::default()
}
/// This condition requires that at least one provided condition is true.
///
/// # Examples
///
/// ```
/// use pinnacle_api::window::rules::WindowRuleCondition;
///
/// // `cond` will be true if the window opens with *either* class "Alacritty" or "firefox"
/// // *or* with title "Steam"
/// let cond = WindowRuleCondition::new()
/// .any([
/// WindowRuleCondition::new().classes(["Alacritty", "firefox"]),
/// WindowRuleCondition::new().titles(["Steam"]).
/// ]);
/// ```
pub fn any(mut self, conds: impl IntoIterator<Item = WindowRuleCondition>) -> Self {
self.0.any = conds.into_iter().map(|cond| cond.0).collect();
self
}
/// This condition requires that all provided conditions are true.
///
/// # 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
}
/// This condition requires that the window's class matches.
///
/// When used in a top level condition or inside of [`WindowRuleCondition::all`],
/// *all* classes must match (this is impossible).
///
/// When used in [`WindowRuleCondition::any`], at least one of the
/// provided classes must match.
///
/// # 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
}
/// This condition requires that the window's title matches.
///
/// When used in a top level condition or inside of [`WindowRuleCondition::all`],
/// *all* titles must match (this is impossible).
///
/// When used in [`WindowRuleCondition::any`], at least one of the
/// provided titles must match.
///
/// # 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
}
/// This condition requires that the window's is opened on the given tags.
///
/// When used in a top level condition or inside of [`WindowRuleCondition::all`],
/// the window must open on *all* given tags.
///
/// When used in [`WindowRuleCondition::any`], the window must open on at least
/// one of the given tags.
///
/// # 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
}
}