mirror of
https://github.com/pinnacle-comp/pinnacle.git
synced 2025-01-18 22:26:12 +01:00
Nuke the old Rust API form orbit
This commit is contained in:
parent
0b88ad298b
commit
1cdeb59a38
28 changed files with 2698 additions and 4750 deletions
|
@ -1,14 +1,25 @@
|
||||||
[package]
|
[package]
|
||||||
name = "pinnacle_api"
|
name = "pinnacle-api"
|
||||||
version = "0.0.1"
|
version = "0.0.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
authors = ["Ottatop <ottatop1227@gmail.com>"]
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
description = "The Rust implementation of the Pinnacle compositor's configuration API"
|
||||||
|
license = "MPL-2.0"
|
||||||
|
repository = "https://github.com/pinnacle-comp/pinnacle"
|
||||||
|
keywords = ["compositor", "pinnacle", "api", "config"]
|
||||||
|
categories = ["api-bindings", "config"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1.0.188", features = ["derive"] }
|
pinnacle-api-defs = { path = "../../pinnacle-api-defs" }
|
||||||
rmp = { version = "0.8.12" }
|
pinnacle-api-macros = { path = "./pinnacle-api-macros" }
|
||||||
rmp-serde = { version = "1.1.2" }
|
tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread", "net"] }
|
||||||
anyhow = { version = "1.0.75", features = ["backtrace"] }
|
async-net = "2.0.0"
|
||||||
lazy_static = "1.4.0"
|
async-compat = "0.2.3"
|
||||||
|
tonic = "0.10.2"
|
||||||
|
tower = { version = "0.4.13", features = ["util"] }
|
||||||
|
futures = "0.3.30"
|
||||||
|
num_enum = "0.7.2"
|
||||||
xkbcommon = "0.7.0"
|
xkbcommon = "0.7.0"
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["pinnacle-api-macros"]
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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"
|
|
|
@ -1,165 +1,383 @@
|
||||||
//! Input management.
|
//! Input management.
|
||||||
|
//!
|
||||||
|
//! This module provides [`Input`], a struct that gives you several different
|
||||||
|
//! methods for setting key- and mousebinds, changing xkeyboard settings, and more.
|
||||||
|
//! View the struct's documentation for more information.
|
||||||
|
|
||||||
|
use futures::{
|
||||||
|
channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture, FutureExt, StreamExt,
|
||||||
|
};
|
||||||
|
use num_enum::TryFromPrimitive;
|
||||||
|
use pinnacle_api_defs::pinnacle::input::{
|
||||||
|
self,
|
||||||
|
v0alpha1::{
|
||||||
|
input_service_client::InputServiceClient,
|
||||||
|
set_libinput_setting_request::{CalibrationMatrix, Setting},
|
||||||
|
SetKeybindRequest, SetLibinputSettingRequest, SetMousebindRequest, SetRepeatRateRequest,
|
||||||
|
SetXkbConfigRequest,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use tonic::transport::Channel;
|
||||||
|
use xkbcommon::xkb::Keysym;
|
||||||
|
|
||||||
|
use self::libinput::LibinputSetting;
|
||||||
|
|
||||||
pub mod libinput;
|
pub mod libinput;
|
||||||
|
|
||||||
use xkbcommon::xkb::Keysym;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
msg::{Args, CallbackId, KeyIntOrString, Msg},
|
|
||||||
send_msg, CallbackVec,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Set a keybind.
|
|
||||||
///
|
|
||||||
/// This function takes in four parameters:
|
|
||||||
/// - `modifiers`: A slice of the modifiers you want held for the keybind to trigger.
|
|
||||||
/// - `key`: The key that needs to be pressed. This takes `impl Into<KeyIntOrString>` and can
|
|
||||||
/// take the following three types:
|
|
||||||
/// - [`char`]: A character of the key you want. This can be `a`, `~`, `@`, and so on.
|
|
||||||
/// - [`u32`]: The key in numeric form. You can use the keys defined in [`xkbcommon::xkb::keysyms`] for this.
|
|
||||||
/// - [`Keysym`]: The key in `Keysym` form, from [xkbcommon::xkb::Keysym].
|
|
||||||
/// - `action`: What you want to run.
|
|
||||||
/// - `callback_vec`: Your [`CallbackVec`] to insert `action` into.
|
|
||||||
///
|
|
||||||
/// `action` takes in a `&mut `[`CallbackVec`] for use in the closure.
|
|
||||||
pub fn keybind<'a, F>(
|
|
||||||
modifiers: &[Modifier],
|
|
||||||
key: impl Into<KeyIntOrString>,
|
|
||||||
mut action: F,
|
|
||||||
callback_vec: &mut CallbackVec<'a>,
|
|
||||||
) where
|
|
||||||
F: FnMut(&mut CallbackVec) + 'a,
|
|
||||||
{
|
|
||||||
let args_callback = move |_: Option<Args>, callback_vec: &mut CallbackVec<'_>| {
|
|
||||||
action(callback_vec);
|
|
||||||
};
|
|
||||||
|
|
||||||
let len = callback_vec.callbacks.len();
|
|
||||||
callback_vec.callbacks.push(Box::new(args_callback));
|
|
||||||
|
|
||||||
let key = key.into();
|
|
||||||
|
|
||||||
let msg = Msg::SetKeybind {
|
|
||||||
key,
|
|
||||||
modifiers: modifiers.to_vec(),
|
|
||||||
callback_id: CallbackId(len as u32),
|
|
||||||
};
|
|
||||||
|
|
||||||
send_msg(msg).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set a mousebind. If called with an already existing mousebind, it gets replaced.
|
|
||||||
///
|
|
||||||
/// The mousebind can happen either on button press or release, so you must
|
|
||||||
/// specify which edge you desire.
|
|
||||||
///
|
|
||||||
/// `action` takes in a `&mut `[`CallbackVec`] for use in the closure.
|
|
||||||
pub fn mousebind<'a, F>(
|
|
||||||
modifiers: &[Modifier],
|
|
||||||
button: MouseButton,
|
|
||||||
edge: MouseEdge,
|
|
||||||
mut action: F,
|
|
||||||
callback_vec: &mut CallbackVec<'a>,
|
|
||||||
) where
|
|
||||||
F: FnMut(&mut CallbackVec) + 'a,
|
|
||||||
{
|
|
||||||
let args_callback = move |_: Option<Args>, callback_vec: &mut CallbackVec<'_>| {
|
|
||||||
action(callback_vec);
|
|
||||||
};
|
|
||||||
|
|
||||||
let len = callback_vec.callbacks.len();
|
|
||||||
callback_vec.callbacks.push(Box::new(args_callback));
|
|
||||||
|
|
||||||
let msg = Msg::SetMousebind {
|
|
||||||
modifiers: modifiers.to_vec(),
|
|
||||||
button: button as u32,
|
|
||||||
edge,
|
|
||||||
callback_id: CallbackId(len as u32),
|
|
||||||
};
|
|
||||||
|
|
||||||
send_msg(msg).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the xkbconfig for your keyboard.
|
|
||||||
///
|
|
||||||
/// Parameters set to `None` will be set to their default values.
|
|
||||||
///
|
|
||||||
/// Read `xkeyboard-config(7)` for more information.
|
|
||||||
pub fn set_xkb_config(
|
|
||||||
rules: Option<&str>,
|
|
||||||
model: Option<&str>,
|
|
||||||
layout: Option<&str>,
|
|
||||||
variant: Option<&str>,
|
|
||||||
options: Option<&str>,
|
|
||||||
) {
|
|
||||||
let msg = Msg::SetXkbConfig {
|
|
||||||
rules: rules.map(|s| s.to_string()),
|
|
||||||
variant: variant.map(|s| s.to_string()),
|
|
||||||
layout: layout.map(|s| s.to_string()),
|
|
||||||
model: model.map(|s| s.to_string()),
|
|
||||||
options: options.map(|s| s.to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
send_msg(msg).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A mouse button.
|
/// A mouse button.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||||
pub enum MouseButton {
|
pub enum MouseButton {
|
||||||
/// The left mouse button.
|
/// The left mouse button
|
||||||
Left = 0x110,
|
Left = 0x110,
|
||||||
/// The right mouse button.
|
/// The right mouse button
|
||||||
Right,
|
Right = 0x111,
|
||||||
/// The middle mouse button, pressed usually by clicking the scroll wheel.
|
/// The middle mouse button
|
||||||
Middle,
|
Middle = 0x112,
|
||||||
///
|
/// The side mouse button
|
||||||
Side,
|
Side = 0x113,
|
||||||
///
|
/// The extra mouse button
|
||||||
Extra,
|
Extra = 0x114,
|
||||||
///
|
/// The forward mouse button
|
||||||
Forward,
|
Forward = 0x115,
|
||||||
///
|
/// The backward mouse button
|
||||||
Back,
|
Back = 0x116,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The edge on which you want things to trigger.
|
/// Keyboard modifiers.
|
||||||
#[derive(Debug, Hash, serde::Serialize, serde::Deserialize, Clone, Copy, PartialEq, Eq)]
|
#[repr(i32)]
|
||||||
|
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, TryFromPrimitive)]
|
||||||
|
pub enum Mod {
|
||||||
|
/// The shift key
|
||||||
|
Shift = 1,
|
||||||
|
/// The ctrl key
|
||||||
|
Ctrl,
|
||||||
|
/// The alt key
|
||||||
|
Alt,
|
||||||
|
/// The super key, aka meta, win, mod4
|
||||||
|
Super,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Press or release.
|
||||||
|
#[repr(i32)]
|
||||||
|
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, TryFromPrimitive)]
|
||||||
pub enum MouseEdge {
|
pub enum MouseEdge {
|
||||||
/// Actions will be triggered on button press.
|
/// Perform actions on button press
|
||||||
Press,
|
Press = 1,
|
||||||
/// Actions will be triggered on button release.
|
/// Perform actions on button release
|
||||||
Release,
|
Release,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<char> for KeyIntOrString {
|
/// A struct that lets you define xkeyboard config options.
|
||||||
fn from(value: char) -> Self {
|
|
||||||
Self::String(value.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<u32> for KeyIntOrString {
|
|
||||||
fn from(value: u32) -> Self {
|
|
||||||
Self::Int(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Keysym> for KeyIntOrString {
|
|
||||||
fn from(value: Keysym) -> Self {
|
|
||||||
Self::Int(value.raw())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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.
|
|
||||||
///
|
///
|
||||||
/// This is also known as the Windows key, meta, or Mod4 for those coming from Xorg.
|
/// See `xkeyboard-config(7)` for more information.
|
||||||
Super,
|
#[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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +1,35 @@
|
||||||
//! Libinput settings.
|
//! Types for libinput configuration.
|
||||||
|
|
||||||
use crate::{msg::Msg, send_msg};
|
/// Pointer acceleration profile
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
/// Set a libinput setting.
|
|
||||||
///
|
|
||||||
/// This takes a [`LibinputSetting`] containing what you want set.
|
|
||||||
pub fn set(setting: LibinputSetting) {
|
|
||||||
let msg = Msg::SetLibinputSetting(setting);
|
|
||||||
send_msg(msg).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The acceleration profile.
|
|
||||||
#[derive(Debug, PartialEq, Copy, Clone, serde::Serialize)]
|
|
||||||
pub enum AccelProfile {
|
pub enum AccelProfile {
|
||||||
/// Flat pointer acceleration.
|
/// A flat acceleration profile.
|
||||||
Flat,
|
|
||||||
/// Adaptive pointer acceleration.
|
|
||||||
///
|
///
|
||||||
/// This is the default for most devices.
|
/// Pointer motion is accelerated by a constant (device-specific) factor, depending on the current speed.
|
||||||
|
Flat = 1,
|
||||||
|
/// An adaptive acceleration profile.
|
||||||
|
///
|
||||||
|
/// Pointer acceleration depends on the input speed. This is the default profile for most devices.
|
||||||
Adaptive,
|
Adaptive,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The click method for a touchpad.
|
/// The click method defines when to generate software-emulated buttons, usually on a device
|
||||||
#[derive(Debug, PartialEq, Copy, Clone, serde::Serialize)]
|
/// that does not have a specific physical button available.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub enum ClickMethod {
|
pub enum ClickMethod {
|
||||||
/// Use software-button areas to generate button events.
|
/// Use software-button areas to generate button events.
|
||||||
ButtonAreas,
|
ButtonAreas = 1,
|
||||||
/// The number of fingers decides which button press to generate.
|
/// The number of fingers decides which button press to generate.
|
||||||
Clickfinger,
|
Clickfinger,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The scroll method for a touchpad.
|
/// The scroll method of a device selects when to generate scroll axis events instead of pointer motion events.
|
||||||
#[derive(Debug, PartialEq, Copy, Clone, serde::Serialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub enum ScrollMethod {
|
pub enum ScrollMethod {
|
||||||
/// Never send scroll events.
|
/// Never send scroll events instead of pointer motion events.
|
||||||
NoScroll,
|
///
|
||||||
|
/// This has no effect on events generated by scroll wheels.
|
||||||
|
NoScroll = 1,
|
||||||
/// Send scroll events when two fingers are logically down on the device.
|
/// Send scroll events when two fingers are logically down on the device.
|
||||||
TwoFinger,
|
TwoFinger,
|
||||||
/// Send scroll events when a finger moves along the bottom or right edge of a device.
|
/// Send scroll events when a finger moves along the bottom or right edge of a device.
|
||||||
|
@ -43,63 +38,48 @@ pub enum ScrollMethod {
|
||||||
OnButtonDown,
|
OnButtonDown,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The mapping between finger count and button event for a touchpad.
|
/// Map 1/2/3 finger tips to buttons.
|
||||||
#[derive(Debug, PartialEq, Copy, Clone, serde::Serialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub enum TapButtonMap {
|
pub enum TapButtonMap {
|
||||||
/// 1/2/3 finger tap is mapped to left/right/middle click.
|
/// 1/2/3 finger tap maps to left/right/middle
|
||||||
LeftRightMiddle,
|
LeftRightMiddle,
|
||||||
/// 1/2/3 finger tap is mapped to left/middle/right click.
|
/// 1/2/3 finger tap maps to left/middle/right
|
||||||
LeftMiddleRight,
|
LeftMiddleRight,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Libinput settings.
|
/// Possible settings for libinput.
|
||||||
#[derive(Debug, PartialEq, Copy, Clone, serde::Serialize)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub enum LibinputSetting {
|
pub enum LibinputSetting {
|
||||||
/// Set the acceleration profile.
|
/// Set the pointer acceleration profile
|
||||||
AccelProfile(AccelProfile),
|
AccelProfile(AccelProfile),
|
||||||
/// Set the acceleration speed.
|
/// Set pointer acceleration speed
|
||||||
///
|
|
||||||
/// This should be a float from -1.0 to 1.0.
|
|
||||||
AccelSpeed(f64),
|
AccelSpeed(f64),
|
||||||
/// Set the calibration matrix.
|
/// Set the calibration matrix
|
||||||
CalibrationMatrix([f32; 6]),
|
CalibrationMatrix([f32; 6]),
|
||||||
/// Set the click method.
|
/// Set the [`ClickMethod`]
|
||||||
///
|
|
||||||
/// The click method defines when to generate software-emulated buttons, usually on a device
|
|
||||||
/// that does not have a specific physical button available.
|
|
||||||
ClickMethod(ClickMethod),
|
ClickMethod(ClickMethod),
|
||||||
/// Set whether or not the device will be disabled while typing.
|
/// Set whether the device gets disabled while typing
|
||||||
DisableWhileTypingEnabled(bool),
|
DisableWhileTyping(bool),
|
||||||
/// Set device left-handedness.
|
/// Set left handed mode
|
||||||
LeftHanded(bool),
|
LeftHanded(bool),
|
||||||
/// Set whether or not the middle click can be emulated.
|
/// Allow or disallow middle mouse button emulation
|
||||||
MiddleEmulationEnabled(bool),
|
MiddleEmulation(bool),
|
||||||
/// Set the rotation angle of a device.
|
/// Set the rotation angle
|
||||||
RotationAngle(u32),
|
RotationAngle(u32),
|
||||||
/// Set the scroll method.
|
/// Set the scroll button
|
||||||
ScrollMethod(ScrollMethod),
|
|
||||||
/// Set whether or not natural scroll is enabled.
|
|
||||||
///
|
|
||||||
/// This reverses the direction of scrolling and is mainly used with touchpads.
|
|
||||||
NaturalScrollEnabled(bool),
|
|
||||||
/// Set the scroll button.
|
|
||||||
ScrollButton(u32),
|
ScrollButton(u32),
|
||||||
/// Set the tap button map,
|
/// Set whether the scroll button should be a drag or toggle
|
||||||
///
|
ScrollButtonLock(bool),
|
||||||
/// This determines whether taps with 2 and 3 fingers register as right and middle clicks or
|
/// Set the [`ScrollMethod`]
|
||||||
/// the reverse.
|
ScrollMethod(ScrollMethod),
|
||||||
|
/// Enable or disable natural scrolling
|
||||||
|
NaturalScroll(bool),
|
||||||
|
/// Set the [`TapButtonMap`]
|
||||||
TapButtonMap(TapButtonMap),
|
TapButtonMap(TapButtonMap),
|
||||||
/// Set whether or not tap-and-drag is enabled.
|
/// Enable or disable tap-to-drag
|
||||||
///
|
TapDrag(bool),
|
||||||
/// When enabled, a single-finger tap immediately followed by a finger down results in
|
/// Enable or disable a timeout where lifting a finger off the device will not stop dragging
|
||||||
/// a button down event, and subsequent finger motion thus triggers a drag.
|
TapDragLock(bool),
|
||||||
/// The button is released on finger up.
|
/// Enable or disable tap-to-click
|
||||||
TapDragEnabled(bool),
|
Tap(bool),
|
||||||
/// Set whether or not tap drag lock is enabled.
|
|
||||||
///
|
|
||||||
/// When enabled, a finger may be lifted and put back on the touchpad within a timeout and the drag process
|
|
||||||
/// continues. When disabled, lifting the finger during a tap-and-drag will immediately stop the drag.
|
|
||||||
TapDragLockEnabled(bool),
|
|
||||||
/// Set whether or not tap-to-click is enabled.
|
|
||||||
TapEnabled(bool),
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,234 +1,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)]
|
#![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 input;
|
||||||
mod msg;
|
|
||||||
pub mod output;
|
pub mod output;
|
||||||
|
pub mod pinnacle;
|
||||||
pub mod process;
|
pub mod process;
|
||||||
pub mod tag;
|
pub mod tag;
|
||||||
|
pub mod util;
|
||||||
pub mod window;
|
pub mod window;
|
||||||
|
|
||||||
/// The xkbcommon crate, re-exported for your convenience.
|
pub use pinnacle_api_macros::config;
|
||||||
|
pub use tokio;
|
||||||
pub use xkbcommon;
|
pub use xkbcommon;
|
||||||
|
|
||||||
/// The prelude for the Pinnacle API.
|
static PINNACLE: OnceLock<Pinnacle> = OnceLock::new();
|
||||||
///
|
static PROCESS: OnceLock<Process> = OnceLock::new();
|
||||||
/// This contains useful imports that you will likely need.
|
static WINDOW: OnceLock<Window> = OnceLock::new();
|
||||||
/// To that end, you can do `use pinnacle_api::prelude::*` to
|
static INPUT: OnceLock<Input> = OnceLock::new();
|
||||||
/// prevent your config file from being cluttered with imports.
|
static OUTPUT: OnceLock<Output> = OnceLock::new();
|
||||||
pub mod prelude {
|
static TAG: OnceLock<Tag> = OnceLock::new();
|
||||||
pub use crate::input::libinput::*;
|
|
||||||
pub use crate::input::Modifier;
|
/// A struct containing static references to all of the configuration structs.
|
||||||
pub use crate::input::MouseButton;
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub use crate::input::MouseEdge;
|
pub struct ApiModules {
|
||||||
pub use crate::output::AlignmentHorizontal;
|
/// The [`Pinnacle`] struct
|
||||||
pub use crate::output::AlignmentVertical;
|
pub pinnacle: &'static Pinnacle,
|
||||||
pub use crate::tag::Layout;
|
/// The [`Process`] struct
|
||||||
pub use crate::window::rules::WindowRule;
|
pub process: &'static Process,
|
||||||
pub use crate::window::rules::WindowRuleCondition;
|
/// The [`Window`] struct
|
||||||
pub use crate::window::FloatingOrTiled;
|
pub window: &'static Window,
|
||||||
pub use crate::window::FullscreenOrMaximized;
|
/// The [`Input`] struct
|
||||||
|
pub input: &'static Input,
|
||||||
|
/// The [`Output`] struct
|
||||||
|
pub output: &'static Output,
|
||||||
|
/// The [`Tag`] struct
|
||||||
|
pub tag: &'static Tag,
|
||||||
}
|
}
|
||||||
|
|
||||||
use std::{
|
/// Connects to Pinnacle and builds the configuration structs.
|
||||||
collections::{hash_map::Entry, HashMap},
|
///
|
||||||
convert::Infallible,
|
/// This function is inserted at the top of your config through the [`config`] macro.
|
||||||
io::{Read, Write},
|
/// You should use that macro instead of this function directly.
|
||||||
os::unix::net::UnixStream,
|
pub async fn connect(
|
||||||
path::PathBuf,
|
) -> Result<(ApiModules, UnboundedReceiver<BoxFuture<'static, ()>>), Box<dyn std::error::Error>> {
|
||||||
sync::{atomic::AtomicU32, Mutex, OnceLock},
|
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,
|
||||||
};
|
};
|
||||||
|
|
||||||
use msg::{Args, CallbackId, IncomingMsg, Msg, Request, RequestResponse};
|
Ok((modules, fut_recv))
|
||||||
|
|
||||||
use crate::msg::RequestId;
|
|
||||||
|
|
||||||
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);
|
/// Listen to Pinnacle for incoming messages.
|
||||||
|
|
||||||
fn send_msg(msg: Msg) -> anyhow::Result<()> {
|
|
||||||
let mut msg = rmp_serde::encode::to_vec_named(&msg)?;
|
|
||||||
let mut msg_len = (msg.len() as u32).to_ne_bytes();
|
|
||||||
|
|
||||||
let mut stream = STREAM.get().unwrap().lock().unwrap();
|
|
||||||
|
|
||||||
stream.write_all(msg_len.as_mut_slice())?;
|
|
||||||
stream.write_all(msg.as_mut_slice())?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_msg(request_id: Option<RequestId>) -> IncomingMsg {
|
|
||||||
loop {
|
|
||||||
if let Some(request_id) = request_id {
|
|
||||||
if let Some(msg) = UNREAD_REQUEST_MSGS.lock().unwrap().remove(&request_id) {
|
|
||||||
return msg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut stream = STREAM.get().unwrap().lock().unwrap();
|
|
||||||
let mut msg_len_bytes = [0u8; 4];
|
|
||||||
stream.read_exact(msg_len_bytes.as_mut_slice()).unwrap();
|
|
||||||
|
|
||||||
let msg_len = u32::from_ne_bytes(msg_len_bytes);
|
|
||||||
let mut msg_bytes = vec![0u8; msg_len as usize];
|
|
||||||
stream.read_exact(msg_bytes.as_mut_slice()).unwrap();
|
|
||||||
|
|
||||||
let incoming_msg: IncomingMsg = rmp_serde::from_slice(msg_bytes.as_slice()).unwrap();
|
|
||||||
|
|
||||||
if let Some(request_id) = request_id {
|
|
||||||
match &incoming_msg {
|
|
||||||
IncomingMsg::CallCallback {
|
|
||||||
callback_id,
|
|
||||||
args: _,
|
|
||||||
} => {
|
|
||||||
UNREAD_CALLBACK_MSGS
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.insert(*callback_id, incoming_msg);
|
|
||||||
}
|
|
||||||
IncomingMsg::RequestResponse {
|
|
||||||
request_id: req_id,
|
|
||||||
response: _,
|
|
||||||
} => {
|
|
||||||
if req_id != &request_id {
|
|
||||||
UNREAD_REQUEST_MSGS
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.insert(*req_id, incoming_msg);
|
|
||||||
} else {
|
|
||||||
return incoming_msg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return incoming_msg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn request(request: Request) -> RequestResponse {
|
|
||||||
use std::sync::atomic::Ordering;
|
|
||||||
let request_id = REQUEST_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
|
|
||||||
|
|
||||||
let msg = Msg::Request {
|
|
||||||
request_id: RequestId(request_id),
|
|
||||||
request,
|
|
||||||
};
|
|
||||||
send_msg(msg).unwrap(); // TODO: propogate
|
|
||||||
|
|
||||||
let IncomingMsg::RequestResponse {
|
|
||||||
request_id: _,
|
|
||||||
response,
|
|
||||||
} = read_msg(Some(RequestId(request_id)))
|
|
||||||
else {
|
|
||||||
unreachable!()
|
|
||||||
};
|
|
||||||
|
|
||||||
response
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Connect to Pinnacle. This needs to be called before you begin calling config functions.
|
|
||||||
///
|
///
|
||||||
/// This will open up a connection to the Unix socket at `$PINNACLE_SOCKET`,
|
/// This will run all futures returned by configuration methods that take in callbacks in order to
|
||||||
/// which should be set when you start the compositor.
|
/// call them.
|
||||||
pub fn connect() -> anyhow::Result<()> {
|
|
||||||
STREAM
|
|
||||||
.set(Mutex::new(
|
|
||||||
UnixStream::connect(PathBuf::from(
|
|
||||||
std::env::var("PINNACLE_SOCKET").unwrap_or("/tmp/pinnacle_socket".to_string()),
|
|
||||||
))
|
|
||||||
.unwrap(),
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Begin listening for messages coming from Pinnacle.
|
|
||||||
///
|
///
|
||||||
/// This needs to be called at the very end of your `setup` function.
|
/// This function is inserted at the end of your config through the [`config`] macro.
|
||||||
pub fn listen(mut callback_vec: CallbackVec) -> Infallible {
|
/// You should use the macro instead of this function directly.
|
||||||
loop {
|
pub async fn listen(
|
||||||
let mut unread_callback_msgs = UNREAD_CALLBACK_MSGS.lock().unwrap();
|
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<_>>() {
|
future_set.push(Box::pin(async move {
|
||||||
let Entry::Occupied(entry) = unread_callback_msgs.entry(cb_id) else {
|
let (fut, stream) = fut_recv.into_future().await;
|
||||||
unreachable!();
|
(fut, Some(stream))
|
||||||
};
|
}));
|
||||||
let IncomingMsg::CallCallback { callback_id, args } = entry.remove() else {
|
|
||||||
unreachable!();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Take the callback out and replace it with a dummy callback
|
while let Some((fut, stream)) = future_set.next().await {
|
||||||
// to allow callback_vec to be used mutably below.
|
if let Some(fut) = fut {
|
||||||
let mut callback = std::mem::replace(
|
future_set.push(Box::pin(async move {
|
||||||
&mut callback_vec.callbacks[callback_id.0 as usize],
|
fut.await;
|
||||||
Box::new(|_, _| {}),
|
(None, None)
|
||||||
);
|
}));
|
||||||
|
|
||||||
callback(args, &mut callback_vec);
|
|
||||||
|
|
||||||
// Put it back.
|
|
||||||
callback_vec.callbacks[callback_id.0 as usize] = callback;
|
|
||||||
}
|
}
|
||||||
|
if let Some(stream) = stream {
|
||||||
let incoming_msg = read_msg(None);
|
future_set.push(Box::pin(async move {
|
||||||
|
let (fut, stream) = stream.into_future().await;
|
||||||
let IncomingMsg::CallCallback { callback_id, args } = incoming_msg else {
|
(fut, Some(stream))
|
||||||
unreachable!();
|
}))
|
||||||
};
|
|
||||||
|
|
||||||
let mut callback = std::mem::replace(
|
|
||||||
&mut callback_vec.callbacks[callback_id.0 as usize],
|
|
||||||
Box::new(|_, _| {}),
|
|
||||||
);
|
|
||||||
|
|
||||||
callback(args, &mut callback_vec);
|
|
||||||
|
|
||||||
callback_vec.callbacks[callback_id.0 as usize] = callback;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Quit Pinnacle.
|
|
||||||
pub fn quit() {
|
|
||||||
send_msg(Msg::Quit).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A wrapper around a vector that holds all of your callbacks.
|
|
||||||
///
|
|
||||||
/// You will need to create this before you can start calling config functions
|
|
||||||
/// that require callbacks.
|
|
||||||
///
|
|
||||||
/// Because your callbacks can capture things, we need a non-static way to hold them.
|
|
||||||
/// That's where this struct comes in.
|
|
||||||
///
|
|
||||||
/// Every function that needs you to provide a callback will also need you to
|
|
||||||
/// provide a `&mut CallbackVec`. This will insert the callback for use in [`listen`].
|
|
||||||
///
|
|
||||||
/// Additionally, all callbacks will also take in `&mut CallbackVec`. This is so you can
|
|
||||||
/// call functions that need it inside of other callbacks.
|
|
||||||
///
|
|
||||||
/// At the end of your config, you will need to call [`listen`] to begin listening for
|
|
||||||
/// messages from Pinnacle that will call your callbacks. Here, you must in pass your
|
|
||||||
/// `CallbackVec`.
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct CallbackVec<'a> {
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
pub(crate) callbacks: Vec<Box<dyn FnMut(Option<Args>, &mut CallbackVec) + 'a>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> CallbackVec<'a> {
|
|
||||||
/// Create a new, empty `CallbackVec`.
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Default::default()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,290 +0,0 @@
|
||||||
use std::num::NonZeroU32;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
input::{libinput::LibinputSetting, Modifier, MouseEdge},
|
|
||||||
output::OutputName,
|
|
||||||
tag::{Layout, TagId},
|
|
||||||
window::{FloatingOrTiled, FullscreenOrMaximized, WindowId},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize, Clone, Copy)]
|
|
||||||
pub(crate) struct CallbackId(pub u32);
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub(crate) struct WindowRuleCondition {
|
|
||||||
/// This condition is met when any of the conditions provided is met.
|
|
||||||
#[serde(default)]
|
|
||||||
pub cond_any: Option<Vec<WindowRuleCondition>>,
|
|
||||||
/// This condition is met when all of the conditions provided are met.
|
|
||||||
#[serde(default)]
|
|
||||||
pub cond_all: Option<Vec<WindowRuleCondition>>,
|
|
||||||
/// This condition is met when the class matches.
|
|
||||||
#[serde(default)]
|
|
||||||
pub class: Option<Vec<String>>,
|
|
||||||
/// This condition is met when the title matches.
|
|
||||||
#[serde(default)]
|
|
||||||
pub title: Option<Vec<String>>,
|
|
||||||
/// This condition is met when the tag matches.
|
|
||||||
#[serde(default)]
|
|
||||||
pub tag: Option<Vec<TagId>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize)]
|
|
||||||
pub(crate) struct WindowRule {
|
|
||||||
/// Set the output the window will open on.
|
|
||||||
#[serde(default)]
|
|
||||||
pub output: Option<OutputName>,
|
|
||||||
/// Set the tags the output will have on open.
|
|
||||||
#[serde(default)]
|
|
||||||
pub tags: Option<Vec<TagId>>,
|
|
||||||
/// Set the window to floating or tiled on open.
|
|
||||||
#[serde(default)]
|
|
||||||
pub floating_or_tiled: Option<FloatingOrTiled>,
|
|
||||||
/// Set the window to fullscreen, maximized, or force it to neither.
|
|
||||||
#[serde(default)]
|
|
||||||
pub fullscreen_or_maximized: Option<FullscreenOrMaximized>,
|
|
||||||
/// Set the window's initial size.
|
|
||||||
#[serde(default)]
|
|
||||||
pub size: Option<(NonZeroU32, NonZeroU32)>,
|
|
||||||
/// Set the window's initial location. If the window is tiled, it will snap to this position
|
|
||||||
/// when set to floating.
|
|
||||||
#[serde(default)]
|
|
||||||
pub location: Option<(i32, i32)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Hash, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub struct RequestId(pub u32);
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Serialize)]
|
|
||||||
pub(crate) enum Msg {
|
|
||||||
// Input
|
|
||||||
SetKeybind {
|
|
||||||
key: KeyIntOrString,
|
|
||||||
modifiers: Vec<Modifier>,
|
|
||||||
callback_id: CallbackId,
|
|
||||||
},
|
|
||||||
SetMousebind {
|
|
||||||
modifiers: Vec<Modifier>,
|
|
||||||
button: u32,
|
|
||||||
edge: MouseEdge,
|
|
||||||
callback_id: CallbackId,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Window management
|
|
||||||
CloseWindow {
|
|
||||||
window_id: WindowId,
|
|
||||||
},
|
|
||||||
SetWindowSize {
|
|
||||||
window_id: WindowId,
|
|
||||||
#[serde(default)]
|
|
||||||
width: Option<i32>,
|
|
||||||
#[serde(default)]
|
|
||||||
height: Option<i32>,
|
|
||||||
},
|
|
||||||
MoveWindowToTag {
|
|
||||||
window_id: WindowId,
|
|
||||||
tag_id: TagId,
|
|
||||||
},
|
|
||||||
ToggleTagOnWindow {
|
|
||||||
window_id: WindowId,
|
|
||||||
tag_id: TagId,
|
|
||||||
},
|
|
||||||
ToggleFloating {
|
|
||||||
window_id: WindowId,
|
|
||||||
},
|
|
||||||
ToggleFullscreen {
|
|
||||||
window_id: WindowId,
|
|
||||||
},
|
|
||||||
ToggleMaximized {
|
|
||||||
window_id: WindowId,
|
|
||||||
},
|
|
||||||
AddWindowRule {
|
|
||||||
cond: WindowRuleCondition,
|
|
||||||
rule: WindowRule,
|
|
||||||
},
|
|
||||||
WindowMoveGrab {
|
|
||||||
button: u32,
|
|
||||||
},
|
|
||||||
WindowResizeGrab {
|
|
||||||
button: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Tag management
|
|
||||||
ToggleTag {
|
|
||||||
tag_id: TagId,
|
|
||||||
},
|
|
||||||
SwitchToTag {
|
|
||||||
tag_id: TagId,
|
|
||||||
},
|
|
||||||
AddTags {
|
|
||||||
/// The name of the output you want these tags on.
|
|
||||||
output_name: OutputName,
|
|
||||||
tag_names: Vec<String>,
|
|
||||||
},
|
|
||||||
// TODO:
|
|
||||||
RemoveTags {
|
|
||||||
/// The name of the output you want these tags removed from.
|
|
||||||
tag_ids: Vec<TagId>,
|
|
||||||
},
|
|
||||||
SetLayout {
|
|
||||||
tag_id: TagId,
|
|
||||||
layout: Layout,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Output management
|
|
||||||
ConnectForAllOutputs {
|
|
||||||
callback_id: CallbackId,
|
|
||||||
},
|
|
||||||
SetOutputLocation {
|
|
||||||
output_name: OutputName,
|
|
||||||
#[serde(default)]
|
|
||||||
x: Option<i32>,
|
|
||||||
#[serde(default)]
|
|
||||||
y: Option<i32>,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Process management
|
|
||||||
/// Spawn a program with an optional callback.
|
|
||||||
Spawn {
|
|
||||||
command: Vec<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
callback_id: Option<CallbackId>,
|
|
||||||
},
|
|
||||||
/// Spawn a program with an optional callback only if it isn't running.
|
|
||||||
SpawnOnce {
|
|
||||||
command: Vec<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
callback_id: Option<CallbackId>,
|
|
||||||
},
|
|
||||||
SetEnv {
|
|
||||||
key: String,
|
|
||||||
value: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Pinnacle management
|
|
||||||
/// Quit the compositor.
|
|
||||||
Quit,
|
|
||||||
|
|
||||||
// Input management
|
|
||||||
SetXkbConfig {
|
|
||||||
#[serde(default)]
|
|
||||||
rules: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
variant: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
layout: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
model: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
options: Option<String>,
|
|
||||||
},
|
|
||||||
|
|
||||||
SetLibinputSetting(LibinputSetting),
|
|
||||||
|
|
||||||
Request {
|
|
||||||
request_id: RequestId,
|
|
||||||
request: Request,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::enum_variant_names)]
|
|
||||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
|
||||||
/// Messages that require a server response, usually to provide some data.
|
|
||||||
pub(crate) enum Request {
|
|
||||||
// Windows
|
|
||||||
GetWindows,
|
|
||||||
GetWindowProps { window_id: WindowId },
|
|
||||||
// Outputs
|
|
||||||
GetOutputs,
|
|
||||||
GetOutputProps { output_name: String },
|
|
||||||
// Tags
|
|
||||||
GetTags,
|
|
||||||
GetTagProps { tag_id: TagId },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
|
|
||||||
pub enum KeyIntOrString {
|
|
||||||
Int(u32),
|
|
||||||
String(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
|
|
||||||
pub enum Args {
|
|
||||||
/// Send a message with lines from the spawned process.
|
|
||||||
Spawn {
|
|
||||||
#[serde(default)]
|
|
||||||
stdout: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
stderr: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
exit_code: Option<i32>,
|
|
||||||
#[serde(default)]
|
|
||||||
exit_msg: Option<String>,
|
|
||||||
},
|
|
||||||
ConnectForAllOutputs {
|
|
||||||
output_name: String,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub(crate) enum IncomingMsg {
|
|
||||||
CallCallback {
|
|
||||||
callback_id: CallbackId,
|
|
||||||
#[serde(default)]
|
|
||||||
args: Option<Args>,
|
|
||||||
},
|
|
||||||
RequestResponse {
|
|
||||||
request_id: RequestId,
|
|
||||||
response: RequestResponse,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub(crate) enum RequestResponse {
|
|
||||||
Window {
|
|
||||||
window_id: Option<WindowId>,
|
|
||||||
},
|
|
||||||
Windows {
|
|
||||||
window_ids: Vec<WindowId>,
|
|
||||||
},
|
|
||||||
WindowProps {
|
|
||||||
size: Option<(i32, i32)>,
|
|
||||||
loc: Option<(i32, i32)>,
|
|
||||||
class: Option<String>,
|
|
||||||
title: Option<String>,
|
|
||||||
focused: Option<bool>,
|
|
||||||
floating: Option<bool>,
|
|
||||||
fullscreen_or_maximized: Option<FullscreenOrMaximized>,
|
|
||||||
},
|
|
||||||
Output {
|
|
||||||
output_name: Option<String>,
|
|
||||||
},
|
|
||||||
Outputs {
|
|
||||||
output_names: Vec<String>,
|
|
||||||
},
|
|
||||||
OutputProps {
|
|
||||||
/// The make of the output.
|
|
||||||
make: Option<String>,
|
|
||||||
/// The model of the output.
|
|
||||||
model: Option<String>,
|
|
||||||
/// The location of the output in the space.
|
|
||||||
loc: Option<(i32, i32)>,
|
|
||||||
/// The resolution of the output.
|
|
||||||
res: Option<(i32, i32)>,
|
|
||||||
/// The refresh rate of the output.
|
|
||||||
refresh_rate: Option<i32>,
|
|
||||||
/// The size of the output, in millimeters.
|
|
||||||
physical_size: Option<(i32, i32)>,
|
|
||||||
/// Whether the output is focused or not.
|
|
||||||
focused: Option<bool>,
|
|
||||||
tag_ids: Option<Vec<TagId>>,
|
|
||||||
},
|
|
||||||
Tags {
|
|
||||||
tag_ids: Vec<TagId>,
|
|
||||||
},
|
|
||||||
TagProps {
|
|
||||||
active: Option<bool>,
|
|
||||||
name: Option<String>,
|
|
||||||
output_name: Option<String>,
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -1,301 +1,515 @@
|
||||||
//! Output management.
|
//! Output management.
|
||||||
|
//!
|
||||||
|
//! An output is Pinnacle's terminology for a monitor.
|
||||||
|
//!
|
||||||
|
//! This module provides [`Output`], which allows you to get [`OutputHandle`]s for different
|
||||||
|
//! connected monitors and set them up.
|
||||||
|
|
||||||
use crate::{
|
use futures::{
|
||||||
msg::{Args, CallbackId, Msg, Request, RequestResponse},
|
channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture, FutureExt, StreamExt,
|
||||||
request, send_msg,
|
|
||||||
tag::TagHandle,
|
|
||||||
CallbackVec,
|
|
||||||
};
|
};
|
||||||
|
use pinnacle_api_defs::pinnacle::{
|
||||||
/// A unique identifier for an output.
|
output::{
|
||||||
///
|
self,
|
||||||
/// An empty string represents an invalid output.
|
v0alpha1::{
|
||||||
#[derive(Debug, Hash, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
output_service_client::OutputServiceClient, ConnectForAllRequest, SetLocationRequest,
|
||||||
pub(crate) struct OutputName(pub String);
|
},
|
||||||
|
},
|
||||||
/// Get an [`OutputHandle`] by its name.
|
tag::v0alpha1::tag_service_client::TagServiceClient,
|
||||||
///
|
|
||||||
/// `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!()
|
|
||||||
};
|
};
|
||||||
|
use tonic::transport::Channel;
|
||||||
|
|
||||||
output_names
|
use crate::tag::TagHandle;
|
||||||
.into_iter()
|
|
||||||
.find(|s| s == name)
|
/// A struct that allows you to get handles to connected outputs and set them up.
|
||||||
.map(|s| OutputHandle(OutputName(s)))
|
///
|
||||||
|
/// 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.
|
/// Get a handle to all connected outputs.
|
||||||
pub fn get_all() -> impl Iterator<Item = OutputHandle> {
|
///
|
||||||
let RequestResponse::Outputs { output_names } = request(Request::GetOutputs) else {
|
/// # Examples
|
||||||
unreachable!()
|
///
|
||||||
};
|
/// ```
|
||||||
|
/// let outputs = output.get_all();
|
||||||
output_names
|
/// ```
|
||||||
|
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()
|
.into_iter()
|
||||||
.map(|name| OutputHandle(OutputName(name)))
|
.map(move |name| OutputHandle {
|
||||||
|
client: client.clone(),
|
||||||
|
tag_client: tag_client.clone(),
|
||||||
|
name,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the currently focused output.
|
/// Get a handle to the output with the given name.
|
||||||
///
|
///
|
||||||
/// This is currently defined as the one with the cursor on it.
|
/// By "name", we mean the name of the connector the output is connected to.
|
||||||
pub fn get_focused() -> Option<OutputHandle> {
|
///
|
||||||
let RequestResponse::Outputs { output_names } = request(Request::GetOutputs) else {
|
/// # Examples
|
||||||
unreachable!()
|
///
|
||||||
|
/// ```
|
||||||
|
/// 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
output_names
|
let output = OutputHandle {
|
||||||
.into_iter()
|
client: client.clone(),
|
||||||
.map(|s| OutputHandle(OutputName(s)))
|
tag_client: tag_client.clone(),
|
||||||
.find(|op| op.properties().focused == Some(true))
|
name: output_name,
|
||||||
}
|
|
||||||
|
|
||||||
/// Connect a function to be run on all current and future outputs.
|
|
||||||
///
|
|
||||||
/// When called, `connect_for_all` will run `func` with all currently connected outputs.
|
|
||||||
/// If a new output is connected, `func` will also be called with it.
|
|
||||||
///
|
|
||||||
/// `func` takes in two parameters:
|
|
||||||
/// - `0`: An [`OutputHandle`] you can act on.
|
|
||||||
/// - `1`: A `&mut `[`CallbackVec`] for use in the closure.
|
|
||||||
///
|
|
||||||
/// This will *not* be called if it has already been called for a given connector.
|
|
||||||
/// This means turning your monitor off and on or unplugging and replugging it *to the same port*
|
|
||||||
/// won't trigger `func`. Plugging it in to a new port *will* trigger `func`.
|
|
||||||
/// This is intended to prevent duplicate setup.
|
|
||||||
///
|
|
||||||
/// Please note: this function will be run *after* Pinnacle processes your entire config.
|
|
||||||
/// For example, if you define tags in `func` but toggle them directly after `connect_for_all`,
|
|
||||||
/// nothing will happen as the tags haven't been added yet.
|
|
||||||
pub fn connect_for_all<'a, F>(mut func: F, callback_vec: &mut CallbackVec<'a>)
|
|
||||||
where
|
|
||||||
F: FnMut(OutputHandle, &mut CallbackVec) + 'a,
|
|
||||||
{
|
|
||||||
let args_callback = move |args: Option<Args>, callback_vec: &mut CallbackVec<'_>| {
|
|
||||||
if let Some(Args::ConnectForAllOutputs { output_name }) = args {
|
|
||||||
func(OutputHandle(OutputName(output_name)), callback_vec);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let len = callback_vec.callbacks.len();
|
for_all(output);
|
||||||
callback_vec.callbacks.push(Box::new(args_callback));
|
}
|
||||||
|
}
|
||||||
let msg = Msg::ConnectForAllOutputs {
|
.boxed(),
|
||||||
callback_id: CallbackId(len as u32),
|
)
|
||||||
};
|
.unwrap();
|
||||||
|
}
|
||||||
send_msg(msg).unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An output handle.
|
/// A handle to an output.
|
||||||
///
|
///
|
||||||
/// This is a handle to one of your monitors.
|
/// This allows you to manipulate outputs and get their properties.
|
||||||
/// It serves to make it easier to deal with them, defining methods for getting properties and
|
#[derive(Clone, Debug)]
|
||||||
/// helpers for things like positioning multiple monitors.
|
pub struct OutputHandle {
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
pub(crate) client: OutputServiceClient<Channel>,
|
||||||
pub struct OutputHandle(pub(crate) OutputName);
|
pub(crate) tag_client: TagServiceClient<Channel>,
|
||||||
|
pub(crate) name: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// Properties of an output.
|
/// The alignment to use for [`OutputHandle::set_loc_adj_to`].
|
||||||
pub struct OutputProperties {
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
/// The make.
|
pub enum Alignment {
|
||||||
pub make: Option<String>,
|
/// Set above, align left borders
|
||||||
/// The model.
|
TopAlignLeft,
|
||||||
///
|
/// Set above, align centers
|
||||||
/// This is something like `27GL850` or whatever gibberish monitor manufacturers name their
|
TopAlignCenter,
|
||||||
/// displays.
|
/// Set above, align right borders
|
||||||
pub model: Option<String>,
|
TopAlignRight,
|
||||||
/// The location of the output in the global space.
|
/// Set below, align left borders
|
||||||
pub loc: Option<(i32, i32)>,
|
BottomAlignLeft,
|
||||||
/// The resolution of the output in pixels, where `res.0` is the width and `res.1` is the
|
/// Set below, align centers
|
||||||
/// height.
|
BottomAlignCenter,
|
||||||
pub res: Option<(i32, i32)>,
|
/// Set below, align right borders
|
||||||
/// The refresh rate of the output in millihertz.
|
BottomAlignRight,
|
||||||
///
|
/// Set to left, align top borders
|
||||||
/// For example, 60Hz is returned as 60000.
|
LeftAlignTop,
|
||||||
pub refresh_rate: Option<i32>,
|
/// Set to left, align centers
|
||||||
/// The physical size of the output in millimeters.
|
LeftAlignCenter,
|
||||||
pub physical_size: Option<(i32, i32)>,
|
/// Set to left, align bottom borders
|
||||||
/// Whether or not the output is focused.
|
LeftAlignBottom,
|
||||||
pub focused: Option<bool>,
|
/// Set to right, align top borders
|
||||||
/// The tags on this output.
|
RightAlignTop,
|
||||||
pub tags: Vec<TagHandle>,
|
/// Set to right, align centers
|
||||||
|
RightAlignCenter,
|
||||||
|
/// Set to right, align bottom borders
|
||||||
|
RightAlignBottom,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OutputHandle {
|
impl OutputHandle {
|
||||||
/// Get this output's name.
|
/// Set the location of this output in the global space.
|
||||||
pub fn name(&self) -> String {
|
///
|
||||||
self.0 .0.clone()
|
/// On startup, Pinnacle will lay out all connected outputs starting at (0, 0)
|
||||||
|
/// and going to the right, with their top borders aligned.
|
||||||
|
///
|
||||||
|
/// This method allows you to move outputs where necessary.
|
||||||
|
///
|
||||||
|
/// Note: If you leave space between two outputs when setting their locations,
|
||||||
|
/// the pointer will not be able to move between them.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// // Assume two monitors in order, "DP-1" and "HDMI-1", with the following dimensions:
|
||||||
|
/// // - "DP-1": ┌─────┐
|
||||||
|
/// // │ │1920x1080
|
||||||
|
/// // └─────┘
|
||||||
|
/// // - "HDMI-1": ┌───────┐
|
||||||
|
/// // │ 2560x │
|
||||||
|
/// // │ 1440 │
|
||||||
|
/// // └───────┘
|
||||||
|
///
|
||||||
|
/// output.get_by_name("DP-1")?.set_location(0, 0);
|
||||||
|
/// output.get_by_name("HDMI-1")?.set_location(1920, -360);
|
||||||
|
///
|
||||||
|
/// // Results in:
|
||||||
|
/// // x=0 ┌───────┐y=-360
|
||||||
|
/// // y=0┌─────┤ │
|
||||||
|
/// // │DP-1 │HDMI-1 │
|
||||||
|
/// // └─────┴───────┘
|
||||||
|
/// // ^x=1920
|
||||||
|
/// ```
|
||||||
|
pub fn set_location(&self, x: impl Into<Option<i32>>, y: impl Into<Option<i32>>) {
|
||||||
|
let mut client = self.client.clone();
|
||||||
|
block_on(client.set_location(SetLocationRequest {
|
||||||
|
output_name: Some(self.name.clone()),
|
||||||
|
x: x.into(),
|
||||||
|
y: y.into(),
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Make OutputProperties an option, make non null fields not options
|
/// Set this output adjacent to another one.
|
||||||
/// Get all properties of this output.
|
///
|
||||||
pub fn properties(&self) -> OutputProperties {
|
/// This is a helper method over [`OutputHandle::set_location`] to make laying out outputs
|
||||||
let RequestResponse::OutputProps {
|
/// easier.
|
||||||
make,
|
///
|
||||||
model,
|
/// `alignment` is an [`Alignment`] of how you want this output to be placed.
|
||||||
loc,
|
/// For example, [`TopAlignLeft`][Alignment::TopAlignLeft] will place this output
|
||||||
res,
|
/// above `other` and align the left borders.
|
||||||
refresh_rate,
|
/// Similarly, [`RightAlignCenter`][Alignment::RightAlignCenter] will place this output
|
||||||
physical_size,
|
/// to the right of `other` and align their centers.
|
||||||
focused,
|
///
|
||||||
tag_ids,
|
/// # Examples
|
||||||
} = request(Request::GetOutputProps {
|
///
|
||||||
output_name: self.0 .0.clone(),
|
/// ```
|
||||||
})
|
/// use pinnacle_api::output::Alignment;
|
||||||
else {
|
///
|
||||||
unreachable!()
|
/// // Assume two monitors in order, "DP-1" and "HDMI-1", with the following dimensions:
|
||||||
|
/// // - "DP-1": ┌─────┐
|
||||||
|
/// // │ │1920x1080
|
||||||
|
/// // └─────┘
|
||||||
|
/// // - "HDMI-1": ┌───────┐
|
||||||
|
/// // │ 2560x │
|
||||||
|
/// // │ 1440 │
|
||||||
|
/// // └───────┘
|
||||||
|
///
|
||||||
|
/// output.get_by_name("DP-1")?.set_loc_adj_to(output.get_by_name("HDMI-1")?, Alignment::BottomAlignRight);
|
||||||
|
///
|
||||||
|
/// // Results in:
|
||||||
|
/// // ┌───────┐
|
||||||
|
/// // │ │
|
||||||
|
/// // │HDMI-1 │
|
||||||
|
/// // └──┬────┤
|
||||||
|
/// // │DP-1│
|
||||||
|
/// // └────┘
|
||||||
|
/// // Notice that "DP-1" now has the coordinates (2280, 1440) because "DP-1" is getting moved, not "HDMI-1".
|
||||||
|
/// // "HDMI-1" was placed at (1920, 0) during the compositor's initial output layout.
|
||||||
|
/// ```
|
||||||
|
pub fn set_loc_adj_to(&self, other: &OutputHandle, alignment: Alignment) {
|
||||||
|
let self_props = self.props();
|
||||||
|
let other_props = other.props();
|
||||||
|
|
||||||
|
// poor man's try {}
|
||||||
|
let attempt_set_loc = || -> Option<()> {
|
||||||
|
let other_x = other_props.x?;
|
||||||
|
let other_y = other_props.y?;
|
||||||
|
let other_width = other_props.pixel_width? as i32;
|
||||||
|
let other_height = other_props.pixel_height? as i32;
|
||||||
|
|
||||||
|
let self_width = self_props.pixel_width? as i32;
|
||||||
|
let self_height = self_props.pixel_height? as i32;
|
||||||
|
|
||||||
|
use Alignment::*;
|
||||||
|
|
||||||
|
let x: i32;
|
||||||
|
let y: i32;
|
||||||
|
|
||||||
|
if let TopAlignLeft | TopAlignCenter | TopAlignRight | BottomAlignLeft
|
||||||
|
| BottomAlignCenter | BottomAlignRight = alignment
|
||||||
|
{
|
||||||
|
if let TopAlignLeft | TopAlignCenter | TopAlignRight = alignment {
|
||||||
|
y = other_y - self_height;
|
||||||
|
} else {
|
||||||
|
// bottom
|
||||||
|
y = other_y + other_height;
|
||||||
|
}
|
||||||
|
|
||||||
|
match alignment {
|
||||||
|
TopAlignLeft | BottomAlignLeft => x = other_x,
|
||||||
|
TopAlignCenter | BottomAlignCenter => {
|
||||||
|
x = other_x + (other_width - self_width) / 2;
|
||||||
|
}
|
||||||
|
TopAlignRight | BottomAlignRight => x = other_x + (other_width - self_width),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let LeftAlignTop | LeftAlignCenter | LeftAlignBottom = alignment {
|
||||||
|
x = other_x - self_width;
|
||||||
|
} else {
|
||||||
|
x = other_x + other_width;
|
||||||
|
}
|
||||||
|
|
||||||
|
match alignment {
|
||||||
|
LeftAlignTop | RightAlignTop => y = other_y,
|
||||||
|
LeftAlignCenter | RightAlignCenter => {
|
||||||
|
y = other_y + (other_height - self_height) / 2;
|
||||||
|
}
|
||||||
|
LeftAlignBottom | RightAlignBottom => {
|
||||||
|
y = other_y + (other_height - self_height);
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.set_location(Some(x), Some(y));
|
||||||
|
|
||||||
|
Some(())
|
||||||
};
|
};
|
||||||
|
|
||||||
|
attempt_set_loc();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all properties of this output.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use pinnacle_api::output::OutputProperties;
|
||||||
|
///
|
||||||
|
/// let OutputProperties {
|
||||||
|
/// make,
|
||||||
|
/// model,
|
||||||
|
/// x,
|
||||||
|
/// y,
|
||||||
|
/// pixel_width,
|
||||||
|
/// pixel_height,
|
||||||
|
/// refresh_rate,
|
||||||
|
/// physical_width,
|
||||||
|
/// physical_height,
|
||||||
|
/// focused,
|
||||||
|
/// tags,
|
||||||
|
/// } = output.get_focused()?.props();
|
||||||
|
/// ```
|
||||||
|
pub fn props(&self) -> OutputProperties {
|
||||||
|
let mut client = self.client.clone();
|
||||||
|
let response = block_on(
|
||||||
|
client.get_properties(output::v0alpha1::GetPropertiesRequest {
|
||||||
|
output_name: Some(self.name.clone()),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.into_inner();
|
||||||
|
|
||||||
OutputProperties {
|
OutputProperties {
|
||||||
make,
|
make: response.make,
|
||||||
model,
|
model: response.model,
|
||||||
loc,
|
x: response.x,
|
||||||
res,
|
y: response.y,
|
||||||
refresh_rate,
|
pixel_width: response.pixel_width,
|
||||||
physical_size,
|
pixel_height: response.pixel_height,
|
||||||
focused,
|
refresh_rate: response.refresh_rate,
|
||||||
tags: tag_ids
|
physical_width: response.physical_width,
|
||||||
.unwrap_or(vec![])
|
physical_height: response.physical_height,
|
||||||
|
focused: response.focused,
|
||||||
|
tags: response
|
||||||
|
.tag_ids
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(TagHandle)
|
.map(|id| TagHandle {
|
||||||
|
client: self.tag_client.clone(),
|
||||||
|
output_client: self.client.clone(),
|
||||||
|
id,
|
||||||
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add tags with the given `names` to this output.
|
// TODO: make a macro for the following or something
|
||||||
pub fn add_tags(&self, names: &[&str]) {
|
|
||||||
crate::tag::add(self, names);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set this output's location in the global space.
|
/// Get this output's make.
|
||||||
pub fn set_loc(&self, x: Option<i32>, y: Option<i32>) {
|
|
||||||
let msg = Msg::SetOutputLocation {
|
|
||||||
output_name: self.0.clone(),
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
};
|
|
||||||
|
|
||||||
send_msg(msg).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set this output's location to the right of `other`.
|
|
||||||
///
|
///
|
||||||
/// It will be aligned vertically based on the given `alignment`.
|
/// Shorthand for `self.props().make`.
|
||||||
pub fn set_loc_right_of(&self, other: &OutputHandle, alignment: AlignmentVertical) {
|
pub fn make(&self) -> Option<String> {
|
||||||
self.set_loc_horizontal(other, LeftOrRight::Right, alignment);
|
self.props().make
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set this output's location to the left of `other`.
|
/// Get this output's model.
|
||||||
///
|
///
|
||||||
/// It will be aligned vertically based on the given `alignment`.
|
/// Shorthand for `self.props().make`.
|
||||||
pub fn set_loc_left_of(&self, other: &OutputHandle, alignment: AlignmentVertical) {
|
pub fn model(&self) -> Option<String> {
|
||||||
self.set_loc_horizontal(other, LeftOrRight::Left, alignment);
|
self.props().model
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set this output's location to the top of `other`.
|
/// Get this output's x position in the global space.
|
||||||
///
|
///
|
||||||
/// It will be aligned horizontally based on the given `alignment`.
|
/// Shorthand for `self.props().x`.
|
||||||
pub fn set_loc_top_of(&self, other: &OutputHandle, alignment: AlignmentHorizontal) {
|
pub fn x(&self) -> Option<i32> {
|
||||||
self.set_loc_vertical(other, TopOrBottom::Top, alignment);
|
self.props().x
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set this output's location to the bottom of `other`.
|
/// Get this output's y position in the global space.
|
||||||
///
|
///
|
||||||
/// It will be aligned horizontally based on the given `alignment`.
|
/// Shorthand for `self.props().y`.
|
||||||
pub fn set_loc_bottom_of(&self, other: &OutputHandle, alignment: AlignmentHorizontal) {
|
pub fn y(&self) -> Option<i32> {
|
||||||
self.set_loc_vertical(other, TopOrBottom::Bottom, alignment);
|
self.props().y
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_loc_horizontal(
|
/// Get this output's screen width in pixels.
|
||||||
&self,
|
///
|
||||||
other: &OutputHandle,
|
/// Shorthand for `self.props().pixel_width`.
|
||||||
left_or_right: LeftOrRight,
|
pub fn pixel_width(&self) -> Option<u32> {
|
||||||
alignment: AlignmentVertical,
|
self.props().pixel_width
|
||||||
) {
|
|
||||||
let op1_props = self.properties();
|
|
||||||
let op2_props = other.properties();
|
|
||||||
|
|
||||||
let (Some(_self_loc), Some(self_res), Some(other_loc), Some(other_res)) =
|
|
||||||
(op1_props.loc, op1_props.res, op2_props.loc, op2_props.res)
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let x = match left_or_right {
|
|
||||||
LeftOrRight::Left => other_loc.0 - self_res.0,
|
|
||||||
LeftOrRight::Right => other_loc.0 + self_res.0,
|
|
||||||
};
|
|
||||||
|
|
||||||
let y = match alignment {
|
|
||||||
AlignmentVertical::Top => other_loc.1,
|
|
||||||
AlignmentVertical::Center => other_loc.1 + (other_res.1 - self_res.1) / 2,
|
|
||||||
AlignmentVertical::Bottom => other_loc.1 + (other_res.1 - self_res.1),
|
|
||||||
};
|
|
||||||
|
|
||||||
self.set_loc(Some(x), Some(y));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_loc_vertical(
|
/// Get this output's screen height in pixels.
|
||||||
&self,
|
///
|
||||||
other: &OutputHandle,
|
/// Shorthand for `self.props().pixel_height`.
|
||||||
top_or_bottom: TopOrBottom,
|
pub fn pixel_height(&self) -> Option<u32> {
|
||||||
alignment: AlignmentHorizontal,
|
self.props().pixel_height
|
||||||
) {
|
}
|
||||||
let op1_props = self.properties();
|
|
||||||
let op2_props = other.properties();
|
|
||||||
|
|
||||||
let (Some(_self_loc), Some(self_res), Some(other_loc), Some(other_res)) =
|
/// Get this output's refresh rate in millihertz.
|
||||||
(op1_props.loc, op1_props.res, op2_props.loc, op2_props.res)
|
///
|
||||||
else {
|
/// For example, 144Hz will be returned as 144000.
|
||||||
return;
|
///
|
||||||
};
|
/// Shorthand for `self.props().refresh_rate`.
|
||||||
|
pub fn refresh_rate(&self) -> Option<u32> {
|
||||||
|
self.props().refresh_rate
|
||||||
|
}
|
||||||
|
|
||||||
let y = match top_or_bottom {
|
/// Get this output's physical width in millimeters.
|
||||||
TopOrBottom::Top => other_loc.1 - self_res.1,
|
///
|
||||||
TopOrBottom::Bottom => other_loc.1 + other_res.1,
|
/// Shorthand for `self.props().physical_width`.
|
||||||
};
|
pub fn physical_width(&self) -> Option<u32> {
|
||||||
|
self.props().physical_width
|
||||||
|
}
|
||||||
|
|
||||||
let x = match alignment {
|
/// Get this output's physical height in millimeters.
|
||||||
AlignmentHorizontal::Left => other_loc.0,
|
///
|
||||||
AlignmentHorizontal::Center => other_loc.0 + (other_res.0 - self_res.0) / 2,
|
/// Shorthand for `self.props().physical_height`.
|
||||||
AlignmentHorizontal::Right => other_loc.0 + (other_res.0 - self_res.0),
|
pub fn physical_height(&self) -> Option<u32> {
|
||||||
};
|
self.props().physical_height
|
||||||
|
}
|
||||||
|
|
||||||
self.set_loc(Some(x), Some(y));
|
/// Get whether this output is focused or not.
|
||||||
|
///
|
||||||
|
/// This is currently implemented as the output with the most recent pointer motion.
|
||||||
|
///
|
||||||
|
/// Shorthand for `self.props().focused`.
|
||||||
|
pub fn focused(&self) -> Option<bool> {
|
||||||
|
self.props().focused
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the tags this output has.
|
||||||
|
///
|
||||||
|
/// Shorthand for `self.props().tags`
|
||||||
|
pub fn tags(&self) -> Vec<TagHandle> {
|
||||||
|
self.props().tags
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get this output's unique name (the name of its connector).
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
&self.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TopOrBottom {
|
/// The properties of an output.
|
||||||
Top,
|
#[derive(Clone, Debug)]
|
||||||
Bottom,
|
pub struct OutputProperties {
|
||||||
}
|
/// The make of the output
|
||||||
|
pub make: Option<String>,
|
||||||
enum LeftOrRight {
|
/// The model of the output
|
||||||
Left,
|
///
|
||||||
Right,
|
/// This is something like "27GL83A" or whatever crap monitor manufacturers name their monitors
|
||||||
}
|
/// these days.
|
||||||
|
pub model: Option<String>,
|
||||||
/// Horizontal alignment.
|
/// The x position of the output in the global space
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
pub x: Option<i32>,
|
||||||
pub enum AlignmentHorizontal {
|
/// The y position of the output in the global space
|
||||||
/// Align the outputs such that the left edges are in line.
|
pub y: Option<i32>,
|
||||||
Left,
|
/// The output's screen width in pixels
|
||||||
/// Center the outputs horizontally.
|
pub pixel_width: Option<u32>,
|
||||||
Center,
|
/// The output's screen height in pixels
|
||||||
/// Align the outputs such that the right edges are in line.
|
pub pixel_height: Option<u32>,
|
||||||
Right,
|
/// The output's refresh rate in millihertz
|
||||||
}
|
pub refresh_rate: Option<u32>,
|
||||||
|
/// The output's physical width in millimeters
|
||||||
/// Vertical alignment.
|
pub physical_width: Option<u32>,
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
/// The output's physical height in millimeters
|
||||||
pub enum AlignmentVertical {
|
pub physical_height: Option<u32>,
|
||||||
/// Align the outputs such that the top edges are in line.
|
/// Whether this output is focused or not
|
||||||
Top,
|
///
|
||||||
/// Center the outputs vertically.
|
/// This is currently implemented as the output with the most recent pointer motion.
|
||||||
Center,
|
pub focused: Option<bool>,
|
||||||
/// Align the outputs such that the bottom edges are in line.
|
/// The tags this output has
|
||||||
Bottom,
|
pub tags: Vec<TagHandle>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,132 +1,178 @@
|
||||||
//! Process management.
|
//! Process management.
|
||||||
|
//!
|
||||||
|
//! This module provides [`Process`], which allows you to spawn processes and set environment
|
||||||
|
//! variables.
|
||||||
|
|
||||||
use crate::{
|
use futures::{
|
||||||
msg::{Args, CallbackId, Msg},
|
channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture, FutureExt, StreamExt,
|
||||||
send_msg, CallbackVec,
|
|
||||||
};
|
};
|
||||||
|
use pinnacle_api_defs::pinnacle::process::v0alpha1::{
|
||||||
|
process_service_client::ProcessServiceClient, SetEnvRequest, SpawnRequest,
|
||||||
|
};
|
||||||
|
use tonic::transport::Channel;
|
||||||
|
|
||||||
|
/// 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.
|
/// Spawn a process.
|
||||||
///
|
///
|
||||||
/// This will use Rust's (more specifically `async_process`'s) `Command` to spawn the provided
|
/// Note that windows spawned *before* tags are added will not be displayed.
|
||||||
/// arguments. If you are using any shell syntax like `~`, you may need to spawn a shell
|
/// This will be changed in the future to be more like Awesome, where windows with no tags are
|
||||||
/// instead. If so, you may *also* need to correctly escape the input.
|
/// displayed on every tag instead.
|
||||||
pub fn spawn(command: Vec<&str>) -> anyhow::Result<()> {
|
///
|
||||||
let msg = Msg::Spawn {
|
/// # Examples
|
||||||
command: command.into_iter().map(|s| s.to_string()).collect(),
|
///
|
||||||
callback_id: None,
|
/// ```
|
||||||
};
|
/// process.spawn(["alacritty"]);
|
||||||
|
/// process.spawn(["bash", "-c", "swaybg -i ~/path_to_wallpaper"]);
|
||||||
|
/// ```
|
||||||
|
pub fn spawn(&self, args: impl IntoIterator<Item = impl Into<String>>) {
|
||||||
|
self.spawn_inner(args, false, None);
|
||||||
|
}
|
||||||
|
|
||||||
send_msg(msg)
|
/// Spawn a process with callbacks for its stdout, stderr, and exit information.
|
||||||
|
///
|
||||||
|
/// See [`SpawnCallbacks`] for the passed in struct.
|
||||||
|
///
|
||||||
|
/// 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.
|
/// 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
|
/// This is useful for startup programs.
|
||||||
/// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Spawn a process with an optional callback for its stdout, stderr, and exit information.
|
|
||||||
///
|
///
|
||||||
/// `callback` has the following parameters:
|
/// See [`Process::spawn`] for details.
|
||||||
/// - `0`: The process's stdout printed this line.
|
pub fn spawn_once(&self, args: impl IntoIterator<Item = impl Into<String>>) {
|
||||||
/// - `1`: The process's stderr printed this line.
|
self.spawn_inner(args, true, None);
|
||||||
/// - `2`: The process exited with this code.
|
}
|
||||||
/// - `3`: The process exited with this message.
|
|
||||||
/// - `4`: A `&mut `[`CallbackVec`] for use inside the closure.
|
/// Spawn a process only if it isn't already running with optional callbacks for its stdout,
|
||||||
|
/// stderr, and exit information.
|
||||||
///
|
///
|
||||||
/// You must also pass in a mutable reference to a [`CallbackVec`] in order to store your callback.
|
/// This is useful for startup programs.
|
||||||
pub fn spawn_with_callback<'a, F>(
|
|
||||||
command: Vec<&str>,
|
|
||||||
mut callback: F,
|
|
||||||
callback_vec: &mut CallbackVec<'a>,
|
|
||||||
) -> anyhow::Result<()>
|
|
||||||
where
|
|
||||||
F: FnMut(Option<String>, Option<String>, Option<i32>, Option<String>, &mut CallbackVec) + 'a,
|
|
||||||
{
|
|
||||||
let args_callback = move |args: Option<Args>, callback_vec: &mut CallbackVec<'_>| {
|
|
||||||
if let Some(Args::Spawn {
|
|
||||||
stdout,
|
|
||||||
stderr,
|
|
||||||
exit_code,
|
|
||||||
exit_msg,
|
|
||||||
}) = args
|
|
||||||
{
|
|
||||||
callback(stdout, stderr, exit_code, exit_msg, callback_vec);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let len = callback_vec.callbacks.len();
|
|
||||||
callback_vec.callbacks.push(Box::new(args_callback));
|
|
||||||
|
|
||||||
let msg = Msg::Spawn {
|
|
||||||
command: command.into_iter().map(|s| s.to_string()).collect(),
|
|
||||||
callback_id: Some(CallbackId(len as u32)),
|
|
||||||
};
|
|
||||||
|
|
||||||
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:
|
/// See [`Process::spawn_with_callbacks`] for details.
|
||||||
/// - `0`: The process's stdout printed this line.
|
pub fn spawn_once_with_callbacks(
|
||||||
/// - `1`: The process's stderr printed this line.
|
&self,
|
||||||
/// - `2`: The process exited with this code.
|
args: impl IntoIterator<Item = impl Into<String>>,
|
||||||
/// - `3`: The process exited with this message.
|
callbacks: SpawnCallbacks,
|
||||||
/// - `4`: A `&mut `[`CallbackVec`] for use inside the closure.
|
) {
|
||||||
|
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.
|
||||||
///
|
///
|
||||||
/// You must also pass in a mutable reference to a [`CallbackVec`] in order to store your callback.
|
/// # Examples
|
||||||
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
|
/// process.set_env("ENV", "a value lalala");
|
||||||
/// or use [`std::env::set_var`].
|
/// ```
|
||||||
pub fn set_env(key: &str, value: &str) {
|
pub fn set_env(&self, key: impl Into<String>, value: impl Into<String>) {
|
||||||
let msg = Msg::SetEnv {
|
let key = key.into();
|
||||||
key: key.to_string(),
|
let value = value.into();
|
||||||
value: value.to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
send_msg(msg).unwrap();
|
let mut client = self.create_process_client();
|
||||||
|
|
||||||
|
block_on(client.set_env(SetEnvRequest {
|
||||||
|
key: Some(key),
|
||||||
|
value: Some(value),
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,79 +1,257 @@
|
||||||
//! Tag management.
|
//! Tag management.
|
||||||
|
//!
|
||||||
|
//! This module allows you to interact with Pinnacle's tag system.
|
||||||
|
//!
|
||||||
|
//! # The Tag System
|
||||||
|
//! Many Wayland compositors use workspaces for window management.
|
||||||
|
//! Each window is assigned to a workspace and will only show up if that workspace is being
|
||||||
|
//! viewed. This is a find way to manage windows, but it's not that powerful.
|
||||||
|
//!
|
||||||
|
//! Instead, Pinnacle works with a tag system similar to window managers like [dwm](https://dwm.suckless.org/)
|
||||||
|
//! and, the window manager Pinnacle takes inspiration from, [awesome](https://awesomewm.org/).
|
||||||
|
//!
|
||||||
|
//! In a tag system, there are no workspaces. Instead, each window can be tagged with zero or more
|
||||||
|
//! tags, and zero or more tags can be displayed on a monitor at once. This allows you to, for
|
||||||
|
//! example, bring in your browsers on the same screen as your IDE by toggling the "Browser" tag.
|
||||||
|
//!
|
||||||
|
//! Workspaces can be emulated by only displaying one tag at a time. Combining this feature with
|
||||||
|
//! the ability to tag windows with multiple tags allows you to have one window show up on multiple
|
||||||
|
//! different "workspaces". As you can see, this system is much more powerful than workspaces
|
||||||
|
//! alone.
|
||||||
|
//!
|
||||||
|
//! # Configuration
|
||||||
|
//! `tag` contains the [`Tag`] struct, which allows you to add new tags
|
||||||
|
//! and get handles to already defined ones.
|
||||||
|
//!
|
||||||
|
//! These [`TagHandle`]s allow you to manipulate individual tags and get their properties.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
use crate::{
|
sync::{Arc, Mutex},
|
||||||
msg::{Msg, Request, RequestResponse},
|
|
||||||
output::{OutputHandle, OutputName},
|
|
||||||
request, send_msg,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Get a tag by its name and output. If `output` is `None`, the currently focused output will
|
use futures::{channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture};
|
||||||
/// be used instead.
|
use num_enum::TryFromPrimitive;
|
||||||
///
|
use pinnacle_api_defs::pinnacle::{
|
||||||
/// If multiple tags have the same name, this returns the first one.
|
output::v0alpha1::output_service_client::OutputServiceClient,
|
||||||
pub fn get(name: &str, output: Option<&OutputHandle>) -> Option<TagHandle> {
|
tag::{
|
||||||
get_all()
|
self,
|
||||||
.filter(|tag| {
|
v0alpha1::{
|
||||||
tag.properties().output.is_some_and(|op| match output {
|
tag_service_client::TagServiceClient, AddRequest, RemoveRequest, SetActiveRequest,
|
||||||
Some(output) => &op == output,
|
SetLayoutRequest, SwitchToRequest,
|
||||||
None => Some(op) == crate::output::get_focused(),
|
},
|
||||||
})
|
},
|
||||||
})
|
|
||||||
.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!()
|
|
||||||
};
|
};
|
||||||
|
use tonic::transport::Channel;
|
||||||
|
|
||||||
tag_ids.into_iter().map(TagHandle)
|
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, ()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: return taghandles here
|
impl Tag {
|
||||||
/// Add tags with the names from `names` to `output`.
|
pub(crate) fn new(
|
||||||
pub fn add(output: &OutputHandle, names: &[&str]) {
|
channel: Channel,
|
||||||
let msg = Msg::AddTags {
|
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
|
||||||
output_name: output.0.clone(),
|
) -> Self {
|
||||||
tag_names: names.iter().map(|s| s.to_string()).collect(),
|
Self {
|
||||||
};
|
channel,
|
||||||
|
fut_sender,
|
||||||
send_msg(msg).unwrap();
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a `LayoutCycler` to cycle layouts on tags.
|
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.
|
||||||
///
|
///
|
||||||
/// Given a slice of layouts, this will create a `LayoutCycler` with two methods;
|
/// This will add tags with the given names to `output` and return [`TagHandle`]s to all of
|
||||||
/// one will cycle forward the layout for the active tag, and one will cycle backward.
|
/// them.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// # Example
|
|
||||||
/// ```
|
/// ```
|
||||||
/// todo!()
|
/// // 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 layout_cycler(layouts: &[Layout]) -> LayoutCycler {
|
pub fn add(
|
||||||
let indices = std::rc::Rc::new(std::cell::RefCell::new(HashMap::<TagId, usize>::new()));
|
&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 indices_clone = indices.clone();
|
||||||
let layouts = layouts.to_vec();
|
|
||||||
|
let layouts = layouts.into_iter().collect::<Vec<_>>();
|
||||||
let layouts_clone = layouts.clone();
|
let layouts_clone = layouts.clone();
|
||||||
let len = layouts.len();
|
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 next = move |output: Option<&OutputHandle>| {
|
||||||
let Some(output) = output.cloned().or_else(crate::output::get_focused) else {
|
let Some(output) = output.cloned().or_else(|| output_module.get_focused()) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(tag) = output
|
let Some(first_tag) = output
|
||||||
.properties()
|
.props()
|
||||||
.tags
|
.tags
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|tag| tag.properties().active == Some(true))
|
.find(|tag| tag.active() == Some(true))
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut indices = indices.borrow_mut();
|
let mut indices = indices.lock().expect("layout next mutex lock failed");
|
||||||
let index = indices.entry(tag.0).or_insert(0);
|
let index = indices.entry(first_tag.id).or_insert(0);
|
||||||
|
|
||||||
if *index + 1 >= len {
|
if *index + 1 >= len {
|
||||||
*index = 0;
|
*index = 0;
|
||||||
|
@ -81,128 +259,270 @@ pub fn layout_cycler(layouts: &[Layout]) -> LayoutCycler {
|
||||||
*index += 1;
|
*index += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
tag.set_layout(layouts[*index]);
|
first_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
|
let prev = move |output: Option<&OutputHandle>| {
|
||||||
.properties()
|
let Some(output) = output
|
||||||
.tags
|
.cloned()
|
||||||
.into_iter()
|
.or_else(|| output_module_clone.get_focused())
|
||||||
.find(|tag| tag.properties().active == Some(true))
|
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut indices = indices_clone.borrow_mut();
|
let Some(first_tag) = output
|
||||||
let index = indices.entry(tag.0).or_insert(0);
|
.props()
|
||||||
|
.tags
|
||||||
|
.into_iter()
|
||||||
|
.find(|tag| tag.active() == Some(true))
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
if index.wrapping_sub(1) == usize::MAX {
|
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;
|
*index = len - 1;
|
||||||
} else {
|
} else {
|
||||||
*index -= 1;
|
*index -= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
tag.set_layout(layouts_clone[*index]);
|
first_tag.set_layout(layouts_clone[*index]);
|
||||||
};
|
};
|
||||||
|
|
||||||
LayoutCycler {
|
LayoutCycler {
|
||||||
next: Box::new(next),
|
|
||||||
prev: Box::new(prev),
|
prev: Box::new(prev),
|
||||||
|
next: Box::new(next),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A layout cycler that keeps track of tags and their layouts and provides methods to cycle
|
/// A layout cycler that keeps track of tags and their layouts and provides functions to cycle
|
||||||
/// layouts on them.
|
/// layouts on them.
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
pub struct LayoutCycler {
|
pub struct LayoutCycler {
|
||||||
/// Cycle to the next layout on the given output, or the focused output if `None`.
|
/// Cycle to the next layout on the given output, or the focused output if `None`.
|
||||||
pub next: Box<dyn FnMut(Option<&OutputHandle>)>,
|
pub prev: Box<dyn Fn(Option<&OutputHandle>) + Send + Sync + 'static>,
|
||||||
/// Cycle to the previous layout on the given output, or the focused output if `None`.
|
/// Cycle to the previous layout on the given output, or the focused output if `None`.
|
||||||
pub prev: Box<dyn FnMut(Option<&OutputHandle>)>,
|
pub next: Box<dyn Fn(Option<&OutputHandle>) + Send + Sync + 'static>,
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub(crate) enum TagId {
|
|
||||||
None,
|
|
||||||
#[serde(untagged)]
|
|
||||||
Some(u32),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A handle to a tag.
|
/// A handle to a tag.
|
||||||
pub struct TagHandle(pub(crate) TagId);
|
///
|
||||||
|
/// This handle allows you to do things like switch to tags and get their properties.
|
||||||
/// Properties of a tag, retrieved through [`TagHandle::properties`].
|
#[derive(Debug, Clone)]
|
||||||
#[derive(Debug)]
|
pub struct TagHandle {
|
||||||
pub struct TagProperties {
|
pub(crate) client: TagServiceClient<Channel>,
|
||||||
/// Whether or not the tag is active.
|
pub(crate) output_client: OutputServiceClient<Channel>,
|
||||||
pub active: Option<bool>,
|
pub(crate) id: u32,
|
||||||
/// The tag's name.
|
|
||||||
pub name: Option<String>,
|
|
||||||
/// The output the tag is on.
|
|
||||||
pub output: Option<OutputHandle>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TagHandle {
|
/// Various static layouts.
|
||||||
/// Get this tag's [`TagProperties`].
|
#[repr(i32)]
|
||||||
pub fn properties(&self) -> TagProperties {
|
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, TryFromPrimitive)]
|
||||||
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)]
|
|
||||||
pub enum Layout {
|
pub enum Layout {
|
||||||
/// One master window on the left with all other windows stacked to the right.
|
/// One master window on the left with all other windows stacked to the right
|
||||||
MasterStack,
|
MasterStack = 1,
|
||||||
/// Windows split in half towards the bottom right corner.
|
/// Windows split in half towards the bottom right corner
|
||||||
Dwindle,
|
Dwindle,
|
||||||
/// Windows split in half in a spiral
|
/// Windows split in half in a spiral
|
||||||
Spiral,
|
Spiral,
|
||||||
/// One main corner window in the top left with a column of windows on the right and a row on the bottom.
|
/// One main corner window in the top left with a column of windows on the right and a row on the bottom
|
||||||
CornerTopLeft,
|
CornerTopLeft,
|
||||||
/// One main corner window in the top right with a column of windows on the left and a row on the bottom.
|
/// One main corner window in the top right with a column of windows on the left and a row on the bottom
|
||||||
CornerTopRight,
|
CornerTopRight,
|
||||||
/// One main corner window in the bottom left with a column of windows on the right and a row on the top.
|
/// One main corner window in the bottom left with a column of windows on the right and a row on the top.
|
||||||
CornerBottomLeft,
|
CornerBottomLeft,
|
||||||
/// One main corner window in the bottom right with a column of windows on the left and a row on the top.
|
/// One main corner window in the bottom right with a column of windows on the left and a row on the top.
|
||||||
CornerBottomRight,
|
CornerBottomRight,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TagHandle {
|
||||||
|
/// Activate this tag and deactivate all other ones on the same output.
|
||||||
|
///
|
||||||
|
/// This essentially emulates what a traditional workspace is.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// // Assume the focused output has the following inactive tags and windows:
|
||||||
|
/// // "1": Alacritty
|
||||||
|
/// // "2": Firefox, Discord
|
||||||
|
/// // "3": Steam
|
||||||
|
/// tag.get("2")?.switch_to(); // Displays Firefox and Discord
|
||||||
|
/// tag.get("3")?.switch_to(); // Displays Steam
|
||||||
|
/// ```
|
||||||
|
pub fn switch_to(&self) {
|
||||||
|
let mut client = self.client.clone();
|
||||||
|
block_on(client.switch_to(SwitchToRequest {
|
||||||
|
tag_id: Some(self.id),
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set this tag to active or not.
|
||||||
|
///
|
||||||
|
/// While active, windows with this tag will be displayed.
|
||||||
|
///
|
||||||
|
/// While inactive, windows with this tag will not be displayed unless they have other active
|
||||||
|
/// tags.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// // Assume the focused output has the following inactive tags and windows:
|
||||||
|
/// // "1": Alacritty
|
||||||
|
/// // "2": Firefox, Discord
|
||||||
|
/// // "3": Steam
|
||||||
|
/// tag.get("2")?.set_active(true); // Displays Firefox and Discord
|
||||||
|
/// tag.get("3")?.set_active(true); // Displays Firefox, Discord, and Steam
|
||||||
|
/// tag.get("2")?.set_active(false); // Displays Steam
|
||||||
|
/// ```
|
||||||
|
pub fn set_active(&self, set: bool) {
|
||||||
|
let mut client = self.client.clone();
|
||||||
|
block_on(client.set_active(SetActiveRequest {
|
||||||
|
tag_id: Some(self.id),
|
||||||
|
set_or_toggle: Some(tag::v0alpha1::set_active_request::SetOrToggle::Set(set)),
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toggle this tag between active and inactive.
|
||||||
|
///
|
||||||
|
/// While active, windows with this tag will be displayed.
|
||||||
|
///
|
||||||
|
/// While inactive, windows with this tag will not be displayed unless they have other active
|
||||||
|
/// tags.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// // Assume the focused output has the following inactive tags and windows:
|
||||||
|
/// // "1": Alacritty
|
||||||
|
/// // "2": Firefox, Discord
|
||||||
|
/// // "3": Steam
|
||||||
|
/// tag.get("2")?.toggle(); // Displays Firefox and Discord
|
||||||
|
/// tag.get("3")?.toggle(); // Displays Firefox, Discord, and Steam
|
||||||
|
/// tag.get("3")?.toggle(); // Displays Firefox, Discord
|
||||||
|
/// tag.get("2")?.toggle(); // Displays nothing
|
||||||
|
/// ```
|
||||||
|
pub fn toggle_active(&self) {
|
||||||
|
let mut client = self.client.clone();
|
||||||
|
block_on(client.set_active(SetActiveRequest {
|
||||||
|
tag_id: Some(self.id),
|
||||||
|
set_or_toggle: Some(tag::v0alpha1::set_active_request::SetOrToggle::Toggle(())),
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove this tag from its output.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let tags = tag
|
||||||
|
/// .add(output.get_by_name("DP-1")?, ["1", "2", "Buckle", "Shoe"])
|
||||||
|
/// .collect::<Vec<_>>;
|
||||||
|
///
|
||||||
|
/// tags[1].remove();
|
||||||
|
/// tags[3].remove();
|
||||||
|
/// // "DP-1" now only has tags "1" and "Buckle"
|
||||||
|
/// ```
|
||||||
|
pub fn remove(mut self) {
|
||||||
|
block_on(self.client.remove(RemoveRequest {
|
||||||
|
tag_ids: vec![self.id],
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set this tag's layout.
|
||||||
|
///
|
||||||
|
/// Layouting only applies to tiled windows (windows that are not floating, maximized, or
|
||||||
|
/// fullscreen). If multiple tags are active on an output, the first active tag's layout will
|
||||||
|
/// determine the layout strategy.
|
||||||
|
///
|
||||||
|
/// See [`Layout`] for the different static layouts Pinnacle currently has to offer.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use pinnacle_api::tag::Layout;
|
||||||
|
///
|
||||||
|
/// // Set the layout of tag "1" on the focused output to "corner top left".
|
||||||
|
/// tag.get("1", None)?.set_layout(Layout::CornerTopLeft);
|
||||||
|
/// ```
|
||||||
|
pub fn set_layout(&self, layout: Layout) {
|
||||||
|
let mut client = self.client.clone();
|
||||||
|
block_on(client.set_layout(SetLayoutRequest {
|
||||||
|
tag_id: Some(self.id),
|
||||||
|
layout: Some(layout as i32),
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all properties of this tag.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use pinnacle_api::tag::TagProperties;
|
||||||
|
///
|
||||||
|
/// let TagProperties {
|
||||||
|
/// active,
|
||||||
|
/// name,
|
||||||
|
/// output,
|
||||||
|
/// } = tag.get("1", None)?.props();
|
||||||
|
/// ```
|
||||||
|
pub fn props(&self) -> TagProperties {
|
||||||
|
let mut client = self.client.clone();
|
||||||
|
let output_client = self.output_client.clone();
|
||||||
|
|
||||||
|
let response = block_on(client.get_properties(tag::v0alpha1::GetPropertiesRequest {
|
||||||
|
tag_id: Some(self.id),
|
||||||
|
}))
|
||||||
|
.unwrap()
|
||||||
|
.into_inner();
|
||||||
|
|
||||||
|
TagProperties {
|
||||||
|
active: response.active,
|
||||||
|
name: response.name,
|
||||||
|
output: response.output_name.map(|name| OutputHandle {
|
||||||
|
client: output_client,
|
||||||
|
tag_client: client,
|
||||||
|
name,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get this tag's active status.
|
||||||
|
///
|
||||||
|
/// Shorthand for `self.props().active`.
|
||||||
|
pub fn active(&self) -> Option<bool> {
|
||||||
|
self.props().active
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get this tag's name.
|
||||||
|
///
|
||||||
|
/// Shorthand for `self.props().name`.
|
||||||
|
pub fn name(&self) -> Option<String> {
|
||||||
|
self.props().name
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a handle to the output this tag is on.
|
||||||
|
///
|
||||||
|
/// Shorthand for `self.props().output`.
|
||||||
|
pub fn output(&self) -> Option<OutputHandle> {
|
||||||
|
self.props().output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Properties of a tag.
|
||||||
|
pub struct TagProperties {
|
||||||
|
/// Whether the tag is active or not
|
||||||
|
pub active: Option<bool>,
|
||||||
|
/// The name of the tag
|
||||||
|
pub name: Option<String>,
|
||||||
|
/// The output the tag is on
|
||||||
|
pub output: Option<OutputHandle>,
|
||||||
|
}
|
||||||
|
|
|
@ -1,206 +1,530 @@
|
||||||
//! Window management.
|
//! Window management.
|
||||||
|
//!
|
||||||
|
//! This module provides [`Window`], which allows you to get [`WindowHandle`]s and move and resize
|
||||||
|
//! windows using the mouse.
|
||||||
|
//!
|
||||||
|
//! [`WindowHandle`]s allow you to do things like resize and move windows, toggle them between
|
||||||
|
//! floating and tiled, close them, and more.
|
||||||
|
//!
|
||||||
|
//! This module also allows you to set window rules; see the [rules] module for more information.
|
||||||
|
|
||||||
|
use futures::executor::block_on;
|
||||||
|
use num_enum::TryFromPrimitive;
|
||||||
|
use pinnacle_api_defs::pinnacle::{
|
||||||
|
output::v0alpha1::output_service_client::OutputServiceClient,
|
||||||
|
tag::v0alpha1::tag_service_client::TagServiceClient,
|
||||||
|
window::v0alpha1::{
|
||||||
|
window_service_client::WindowServiceClient, AddWindowRuleRequest, CloseRequest,
|
||||||
|
MoveToTagRequest, SetTagRequest,
|
||||||
|
},
|
||||||
|
window::{
|
||||||
|
self,
|
||||||
|
v0alpha1::{
|
||||||
|
GetRequest, MoveGrabRequest, ResizeGrabRequest, SetFloatingRequest,
|
||||||
|
SetFullscreenRequest, SetMaximizedRequest,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use tonic::transport::Channel;
|
||||||
|
|
||||||
|
use crate::{input::MouseButton, tag::TagHandle, util::Geometry};
|
||||||
|
|
||||||
|
use self::rules::{WindowRule, WindowRuleCondition};
|
||||||
|
|
||||||
pub mod rules;
|
pub mod rules;
|
||||||
|
|
||||||
use crate::{
|
/// A struct containing methods that get [`WindowHandle`]s and move windows with the mouse.
|
||||||
input::MouseButton,
|
|
||||||
msg::{Msg, Request, RequestResponse},
|
|
||||||
request, send_msg,
|
|
||||||
tag::TagHandle,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A unique identifier for each window.
|
|
||||||
#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub(crate) enum WindowId {
|
|
||||||
/// A config API returned an invalid window. It should be using this variant.
|
|
||||||
None,
|
|
||||||
/// A valid window id.
|
|
||||||
#[serde(untagged)]
|
|
||||||
Some(u32),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get all windows with the class `class`.
|
|
||||||
pub fn get_by_class(class: &str) -> impl Iterator<Item = WindowHandle> + '_ {
|
|
||||||
get_all().filter(|win| win.properties().class.as_deref() == Some(class))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the currently focused window, or `None` if there isn't one.
|
|
||||||
pub fn get_focused() -> Option<WindowHandle> {
|
|
||||||
get_all().find(|win| win.properties().focused.is_some_and(|focused| focused))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get all windows.
|
|
||||||
pub fn get_all() -> impl Iterator<Item = WindowHandle> {
|
|
||||||
let RequestResponse::Windows { window_ids } = request(Request::GetWindows) else {
|
|
||||||
unreachable!()
|
|
||||||
};
|
|
||||||
|
|
||||||
window_ids.into_iter().map(WindowHandle)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Begin a window move.
|
|
||||||
///
|
///
|
||||||
/// This will start a window move grab with the provided button on the window the pointer
|
/// See [`WindowHandle`] for more information.
|
||||||
/// is currently hovering over. Once `button` is let go, the move will end.
|
#[derive(Debug, Clone)]
|
||||||
pub fn begin_move(button: MouseButton) {
|
pub struct Window {
|
||||||
let msg = Msg::WindowMoveGrab {
|
channel: Channel,
|
||||||
button: button as u32,
|
|
||||||
};
|
|
||||||
|
|
||||||
send_msg(msg).unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Begin a window resize.
|
impl Window {
|
||||||
|
pub(crate) fn new(channel: Channel) -> Self {
|
||||||
|
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 start a window resize grab with the provided button on the window the
|
/// This will begin moving the window under the pointer using the specified [`MouseButton`].
|
||||||
/// pointer is currently hovering over. Once `button` is let go, the resize will end.
|
/// The button must be held down at the time this method is called for the move to start.
|
||||||
pub fn begin_resize(button: MouseButton) {
|
|
||||||
let msg = Msg::WindowResizeGrab {
|
|
||||||
button: button as u32,
|
|
||||||
};
|
|
||||||
|
|
||||||
send_msg(msg).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A handle to a window.
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
||||||
pub struct WindowHandle(WindowId);
|
|
||||||
|
|
||||||
/// Properties of a window, retrieved through [`WindowHandle::properties`].
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct WindowProperties {
|
|
||||||
/// The size of the window, in pixels.
|
|
||||||
pub size: Option<(i32, i32)>,
|
|
||||||
/// The location of the window in the global space.
|
|
||||||
pub loc: Option<(i32, i32)>,
|
|
||||||
/// The window's class.
|
|
||||||
pub class: Option<String>,
|
|
||||||
/// The window's title.
|
|
||||||
pub title: Option<String>,
|
|
||||||
/// Whether or not the window is focused.
|
|
||||||
pub focused: Option<bool>,
|
|
||||||
/// Whether or not the window is floating.
|
|
||||||
pub floating: Option<bool>,
|
|
||||||
/// Whether the window is fullscreen, maximized, or neither.
|
|
||||||
pub fullscreen_or_maximized: Option<FullscreenOrMaximized>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WindowHandle {
|
|
||||||
/// Toggle this window between floating and tiled.
|
|
||||||
pub fn toggle_floating(&self) {
|
|
||||||
send_msg(Msg::ToggleFloating { window_id: self.0 }).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Toggle this window's fullscreen status.
|
|
||||||
///
|
///
|
||||||
/// If used while not fullscreen, it becomes fullscreen.
|
/// This is intended to be used with [`Input::keybind`][crate::input::Input::keybind].
|
||||||
/// 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.
|
|
||||||
///
|
///
|
||||||
/// If used while not maximized, it becomes maximized.
|
/// # Examples
|
||||||
/// If used while maximized, it becomes unmaximized.
|
///
|
||||||
/// If used while fullscreen, it becomes maximized.
|
/// ```
|
||||||
pub fn toggle_maximized(&self) {
|
/// use pinnacle_api::input::{Mod, MouseButton, MouseEdge};
|
||||||
send_msg(Msg::ToggleMaximized { window_id: self.0 }).unwrap();
|
///
|
||||||
}
|
/// // Set `Super + left click` to begin moving a window
|
||||||
|
/// input.mousebind([Mod::Super], MouseButton::Left, MouseEdge::Press, || {
|
||||||
/// Set this window's size. None parameters will be ignored.
|
/// window.begin_move(MouseButton::Left);
|
||||||
pub fn set_size(&self, width: Option<i32>, height: Option<i32>) {
|
/// });
|
||||||
send_msg(Msg::SetWindowSize {
|
/// ```
|
||||||
window_id: self.0,
|
pub fn begin_move(&self, button: MouseButton) {
|
||||||
width,
|
let mut client = self.create_window_client();
|
||||||
height,
|
block_on(client.move_grab(MoveGrabRequest {
|
||||||
})
|
button: Some(button as u32),
|
||||||
|
}))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a close event to this window.
|
/// Start resizing the window with the mouse.
|
||||||
pub fn close(&self) {
|
|
||||||
send_msg(Msg::CloseWindow { window_id: self.0 }).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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!()
|
|
||||||
};
|
|
||||||
|
|
||||||
WindowProperties {
|
|
||||||
size,
|
|
||||||
loc,
|
|
||||||
class,
|
|
||||||
title,
|
|
||||||
focused,
|
|
||||||
floating,
|
|
||||||
fullscreen_or_maximized,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Move this window to `tag`.
|
|
||||||
///
|
///
|
||||||
/// This will remove all other tags on this window.
|
/// This will begin resizing the window under the pointer using the specified [`MouseButton`].
|
||||||
pub fn move_to_tag(&self, tag: &TagHandle) {
|
/// The button must be held down at the time this method is called for the resize to start.
|
||||||
let msg = Msg::MoveWindowToTag {
|
///
|
||||||
window_id: self.0,
|
/// This is intended to be used with [`Input::keybind`][crate::input::Input::keybind].
|
||||||
tag_id: tag.0,
|
///
|
||||||
};
|
/// # 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();
|
||||||
|
}
|
||||||
|
|
||||||
send_msg(msg).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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether or not a window is floating or tiled.
|
/// A handle to a window.
|
||||||
#[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.
|
/// This allows you to manipulate the window and get its properties.
|
||||||
Floating,
|
#[derive(Debug, Clone)]
|
||||||
/// The window is tiled.
|
pub struct WindowHandle {
|
||||||
///
|
pub(crate) client: WindowServiceClient<Channel>,
|
||||||
/// It cannot be resized and can only move by swapping places with other tiled windows.
|
pub(crate) id: u32,
|
||||||
Tiled,
|
pub(crate) tag_client: TagServiceClient<Channel>,
|
||||||
|
pub(crate) output_client: OutputServiceClient<Channel>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the window is fullscreen, maximized, or neither.
|
/// Whether a window is fullscreen, maximized, or neither.
|
||||||
///
|
#[repr(i32)]
|
||||||
/// These three states are mutually exclusive. Setting a window to maximized while it is fullscreen
|
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, TryFromPrimitive)]
|
||||||
/// will make it stop being fullscreen and vice versa.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub enum FullscreenOrMaximized {
|
pub enum FullscreenOrMaximized {
|
||||||
/// The window is not fullscreen or maximized.
|
/// The window is neither fullscreen nor maximized
|
||||||
Neither,
|
Neither = 1,
|
||||||
/// The window is fullscreen.
|
/// 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,
|
Fullscreen,
|
||||||
/// The window is maximized.
|
/// The window is maximized
|
||||||
///
|
|
||||||
/// It will fill up as much space on its output as it can, respecting any layer surfaces.
|
|
||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,83 +1,222 @@
|
||||||
//! Window rules.
|
//! Types for window rules.
|
||||||
|
//!
|
||||||
|
//! A window rule is a way to set the properties of a window on open.
|
||||||
|
//!
|
||||||
|
//! They are comprised of two parts: the [condition][WindowRuleCondition] and the actual [rule][WindowRule].
|
||||||
|
//!
|
||||||
|
//! # [`WindowRuleCondition`]s
|
||||||
|
//! `WindowRuleCondition`s are conditions that the window needs to open with in order to apply a
|
||||||
|
//! rule. For example, you may want to set a window to maximized if it has the class "steam", or
|
||||||
|
//! you might want to open all Firefox instances on tag "3".
|
||||||
|
//!
|
||||||
|
//! To do this, you must build a `WindowRuleCondition` to tell the compositor when to apply any
|
||||||
|
//! rules.
|
||||||
|
//!
|
||||||
|
//! ## Building `WindowRuleCondition`s
|
||||||
|
//! A condition is created through [`WindowRuleCondition::new`]:
|
||||||
|
//! ```
|
||||||
|
//! let cond = WindowRuleCondition::new();
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! In order to understand conditions, you must understand the concept of "any" and "all".
|
||||||
|
//!
|
||||||
|
//! **"Any"**
|
||||||
|
//!
|
||||||
|
//! "Any" conditions only need one of their constituent items to be true for the whole condition to
|
||||||
|
//! evaluate to true. Think of it as one big `if a || b || c || d || ... {}` block.
|
||||||
|
//!
|
||||||
|
//! **"All"**
|
||||||
|
//!
|
||||||
|
//! "All" conditions need *all* of their constituent items to be true for the condition to evaluate
|
||||||
|
//! to true. This is like a big `if a && b && c && d && ... {}` block.
|
||||||
|
//!
|
||||||
|
//! Note that any items in a top level `WindowRuleCondition` fall under "all", so all those items
|
||||||
|
//! must be true.
|
||||||
|
//!
|
||||||
|
//! With that out of the way, we can get started building conditions.
|
||||||
|
//!
|
||||||
|
//! ### `WindowRuleCondition::classes`
|
||||||
|
//! With [`WindowRuleCondition::classes`], you can specify what classes a window needs to have for
|
||||||
|
//! a rule to apply.
|
||||||
|
//!
|
||||||
|
//! The following will apply to windows with the class "firefox":
|
||||||
|
//! ```
|
||||||
|
//! let cond = WindowRuleCondition::new().classes(["firefox"]);
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Note that you pass in some `impl IntoIterator<Item = impl Into<String>>`. This means you can
|
||||||
|
//! pass in more than one class here:
|
||||||
|
//! ```
|
||||||
|
//! let failing_cond = WindowRuleCondition::new().classes(["firefox", "steam"]);
|
||||||
|
//! ```
|
||||||
|
//! *HOWEVER*: this will not work. Recall that top level conditions are implicitly "all". This
|
||||||
|
//! means the above would require windows to have *both classes*, which is impossible. Thus, the
|
||||||
|
//! condition above will never be true.
|
||||||
|
//!
|
||||||
|
//! ### `WindowRuleCondition::titles`
|
||||||
|
//! Like `classes`, you can use `titles` to specify that the window needs to open with a specific
|
||||||
|
//! title for the condition to apply.
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! let cond = WindowRuleCondition::new().titles(["Steam"]);
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Like `classes`, passing in multiple titles at the top level will cause the condition to always
|
||||||
|
//! fail.
|
||||||
|
//!
|
||||||
|
//! ### `WindowRuleCondition::tags`
|
||||||
|
//! You can specify that the window needs to open on the given tags in order to apply a rule.
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! let cond = WindowRuleCondition::new().tags([&tag.get("3", output.get_by_name("HDMI-1")?)?]);
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Here, if you have tag "3" active on "HDMI-1" and spawn a window on that output, this condition
|
||||||
|
//! will apply.
|
||||||
|
//!
|
||||||
|
//! Unlike `classes` and `titles`, you can specify multiple tags at the top level:
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! let op = output.get_by_name("HDMI-1")?;
|
||||||
|
//! let tag1 = tag.get("1", &op)?;
|
||||||
|
//! let tag2 = tag.get("2", &op)?;
|
||||||
|
//!
|
||||||
|
//! let cond = WindowRuleCondition::new().tags([&tag1, &tag2]);
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Now, you must have both tags "1" and "2" active and spawn a window for the condition to apply.
|
||||||
|
//!
|
||||||
|
//! ### `WindowRuleCondition::any`
|
||||||
|
//! Now we can get to ways to compose more complex conditions.
|
||||||
|
//!
|
||||||
|
//! `WindowRuleCondition::any` takes in conditions and will evaluate to true if *anything* in those
|
||||||
|
//! conditions are true.
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! let cond = WindowRuleCondition::new()
|
||||||
|
//! .any([
|
||||||
|
//! WindowRuleCondition::new().classes(["Alacritty"]),
|
||||||
|
//! WindowRuleCondition::new().tags([&tag.get("2", None)?]),
|
||||||
|
//! ]);
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! This condition will apply if the window is *either* "Alacritty" *or* opens on tag "2".
|
||||||
|
//!
|
||||||
|
//! ### `WindowRuleCondition::all`
|
||||||
|
//! With `WindowRuleCondition::all`, *all* specified conditions must be true for the condition to
|
||||||
|
//! be true.
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! let cond = WindowRuleCondition::new()
|
||||||
|
//! .all([
|
||||||
|
//! WindowRuleCondition::new().classes(["Alacritty"]),
|
||||||
|
//! WindowRuleCondition::new().tags([&tag.get("2", None)?]),
|
||||||
|
//! ]);
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! This condition applies if the window has the class "Alacritty" *and* opens on tag "2".
|
||||||
|
//!
|
||||||
|
//! You can write the above a bit shorter, as top level conditions are already "all":
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! let cond = WindowRuleCondition::new()
|
||||||
|
//! .classes(["Alacritty"])
|
||||||
|
//! .tags([&tag.get("2", None)?]);
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! ## Complex condition composition
|
||||||
|
//! You can arbitrarily nest `any` and `all` to achieve desired logic.
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! let op = output.get_by_name("HDMI-1")?;
|
||||||
|
//! let tag1 = tag.get("1", &op)?;
|
||||||
|
//! let tag2 = tag.get("2", &op)?;
|
||||||
|
//!
|
||||||
|
//! let complex_cond = WindowRuleCondition::new()
|
||||||
|
//! .any([
|
||||||
|
//! WindowRuleCondition::new().all([
|
||||||
|
//! WindowRuleCondition::new()
|
||||||
|
//! .classes("Alacritty")
|
||||||
|
//! .tags([&tag1, &tag2])
|
||||||
|
//! ]),
|
||||||
|
//! WindowRuleCondition::new().all([
|
||||||
|
//! WindowRuleCondition::new().any([
|
||||||
|
//! WindowRuleCondition::new().titles(["nvim", "emacs", "nano"]),
|
||||||
|
//! ]),
|
||||||
|
//! WindowRuleCondition::new().any([
|
||||||
|
//! WindowRuleCondition::new().tags([&tag1, &tag2]),
|
||||||
|
//! ]),
|
||||||
|
//! ])
|
||||||
|
//! ])
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! The above is true if either of the following are true:
|
||||||
|
//! - The window has class "Alacritty" and opens on both tags "1" and "2", or
|
||||||
|
//! - The window's class is either "nvim", "emacs", or "nano" *and* it opens on either tag "1" or
|
||||||
|
//! "2".
|
||||||
|
//!
|
||||||
|
//! # [`WindowRule`]s
|
||||||
|
//! `WindowRuleCondition`s are half of a window rule. The other half is the [`WindowRule`] itself.
|
||||||
|
//!
|
||||||
|
//! A `WindowRule` is what will apply to a window if a condition is true.
|
||||||
|
//!
|
||||||
|
//! ## Building `WindowRule`s
|
||||||
|
//!
|
||||||
|
//! Create a new window rule with [`WindowRule::new`]:
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! let rule = WindowRule::new();
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! There are several rules you can set currently.
|
||||||
|
//!
|
||||||
|
//! ### [`WindowRule::output`]
|
||||||
|
//! This will cause the window to open on the specified output.
|
||||||
|
//!
|
||||||
|
//! ### [`WindowRule::tags`]
|
||||||
|
//! This will cause the window to open with the given tags.
|
||||||
|
//!
|
||||||
|
//! ### [`WindowRule::floating`]
|
||||||
|
//! This will cause the window to open either floating or tiled.
|
||||||
|
//!
|
||||||
|
//! ### [`WindowRule::fullscreen_or_maximized`]
|
||||||
|
//! This will cause the window to open either fullscreen, maximized, or neither.
|
||||||
|
//!
|
||||||
|
//! ### [`WindowRule::x`]
|
||||||
|
//! This will cause the window to open at the given x-coordinate.
|
||||||
|
//!
|
||||||
|
//! Note: this only applies to floating windows; tiled windows' geometry will be overridden by
|
||||||
|
//! layouting.
|
||||||
|
//!
|
||||||
|
//! ### [`WindowRule::y`]
|
||||||
|
//! This will cause the window to open at the given y-coordinate.
|
||||||
|
//!
|
||||||
|
//! Note: this only applies to floating windows; tiled windows' geometry will be overridden by
|
||||||
|
//! layouting.
|
||||||
|
//!
|
||||||
|
//! ### [`WindowRule::width`]
|
||||||
|
//! This will cause the window to open with the given width in pixels.
|
||||||
|
//!
|
||||||
|
//! Note: this only applies to floating windows; tiled windows' geometry will be overridden by
|
||||||
|
//! layouting.
|
||||||
|
//!
|
||||||
|
//! ### [`WindowRule::height`]
|
||||||
|
//! This will cause the window to open with the given height in pixels.
|
||||||
|
//!
|
||||||
|
//! Note: this only applies to floating windows; tiled windows' geometry will be overridden by
|
||||||
|
//! layouting.
|
||||||
|
|
||||||
use std::num::NonZeroU32;
|
use pinnacle_api_defs::pinnacle::window;
|
||||||
|
|
||||||
use crate::{msg::Msg, output::OutputHandle, send_msg, tag::TagHandle};
|
use crate::{output::OutputHandle, tag::TagHandle};
|
||||||
|
|
||||||
use super::{FloatingOrTiled, FullscreenOrMaximized};
|
use super::FullscreenOrMaximized;
|
||||||
|
|
||||||
/// Add a window rule.
|
|
||||||
pub fn add(cond: WindowRuleCondition, rule: WindowRule) {
|
|
||||||
let msg = Msg::AddWindowRule {
|
|
||||||
cond: cond.0,
|
|
||||||
rule: rule.0,
|
|
||||||
};
|
|
||||||
|
|
||||||
send_msg(msg).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A window rule.
|
|
||||||
///
|
|
||||||
/// This is what will be applied to a window if it meets a [`WindowRuleCondition`].
|
|
||||||
///
|
|
||||||
/// `WindowRule`s are built using the builder pattern.
|
|
||||||
/// // TODO: show example
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct WindowRule(crate::msg::WindowRule);
|
|
||||||
|
|
||||||
impl WindowRule {
|
|
||||||
/// Create a new, empty window rule.
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Default::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This rule will force windows to open on the provided `output`.
|
|
||||||
pub fn output(mut self, output: &OutputHandle) -> Self {
|
|
||||||
self.0.output = Some(output.0.clone());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This rule will force windows to open with the provided `tags`.
|
|
||||||
pub fn tags(mut self, tags: &[TagHandle]) -> Self {
|
|
||||||
self.0.tags = Some(tags.iter().map(|tag| tag.0).collect());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This rule will force windows to open either floating or tiled.
|
|
||||||
pub fn floating_or_tiled(mut self, floating_or_tiled: FloatingOrTiled) -> Self {
|
|
||||||
self.0.floating_or_tiled = Some(floating_or_tiled);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This rule will force windows to open either fullscreen, maximized, or neither.
|
|
||||||
pub fn fullscreen_or_maximized(
|
|
||||||
mut self,
|
|
||||||
fullscreen_or_maximized: FullscreenOrMaximized,
|
|
||||||
) -> Self {
|
|
||||||
self.0.fullscreen_or_maximized = Some(fullscreen_or_maximized);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This rule will force windows to open with a specific size.
|
|
||||||
///
|
|
||||||
/// This will only actually be visible if the window is also floating.
|
|
||||||
pub fn size(mut self, width: NonZeroU32, height: NonZeroU32) -> Self {
|
|
||||||
self.0.size = Some((width, height));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This rule will force windows to open at a specific location.
|
|
||||||
///
|
|
||||||
/// This will only actually be visible if the window is also floating.
|
|
||||||
pub fn location(mut self, x: i32, y: i32) -> Self {
|
|
||||||
self.0.location = Some((x, y));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A condition for a [`WindowRule`] to apply to a window.
|
/// A condition for a [`WindowRule`] to apply to a window.
|
||||||
#[derive(Default, Debug)]
|
///
|
||||||
pub struct WindowRuleCondition(crate::msg::WindowRuleCondition);
|
/// `WindowRuleCondition`s are built using the builder pattern.
|
||||||
|
#[derive(Default, Debug, Clone)]
|
||||||
|
pub struct WindowRuleCondition(pub(super) window::v0alpha1::WindowRuleCondition);
|
||||||
|
|
||||||
impl WindowRuleCondition {
|
impl WindowRuleCondition {
|
||||||
/// Create a new, empty `WindowRuleCondition`.
|
/// Create a new, empty `WindowRuleCondition`.
|
||||||
|
@ -86,14 +225,41 @@ impl WindowRuleCondition {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This condition requires that at least one provided condition is true.
|
/// This condition requires that at least one provided condition is true.
|
||||||
pub fn any(mut self, conds: &[WindowRuleCondition]) -> Self {
|
///
|
||||||
self.0.cond_any = Some(conds.iter().map(|cond| cond.0.clone()).collect());
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use pinnacle_api::window::rules::WindowRuleCondition;
|
||||||
|
///
|
||||||
|
/// // `cond` will be true if the window opens with *either* class "Alacritty" or "firefox"
|
||||||
|
/// // *or* with title "Steam"
|
||||||
|
/// let cond = WindowRuleCondition::new()
|
||||||
|
/// .any([
|
||||||
|
/// WindowRuleCondition::new().classes(["Alacritty", "firefox"]),
|
||||||
|
/// WindowRuleCondition::new().titles(["Steam"]).
|
||||||
|
/// ]);
|
||||||
|
/// ```
|
||||||
|
pub fn any(mut self, conds: impl IntoIterator<Item = WindowRuleCondition>) -> Self {
|
||||||
|
self.0.any = conds.into_iter().map(|cond| cond.0).collect();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This condition requires that all provided conditions are true.
|
/// This condition requires that all provided conditions are true.
|
||||||
pub fn all(mut self, conds: &[WindowRuleCondition]) -> Self {
|
///
|
||||||
self.0.cond_all = Some(conds.iter().map(|cond| cond.0.clone()).collect());
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use pinnacle_api::window::rules::WindowRuleCondition;
|
||||||
|
///
|
||||||
|
/// // `cond` will be true if the window opens with class "Alacritty" *and* on tag "1"
|
||||||
|
/// let cond = WindowRuleCondition::new()
|
||||||
|
/// .any([
|
||||||
|
/// WindowRuleCondition::new().tags([tag.get("1", None)?]),
|
||||||
|
/// WindowRuleCondition::new().titles(["Alacritty"]).
|
||||||
|
/// ]);
|
||||||
|
/// ```
|
||||||
|
pub fn all(mut self, conds: impl IntoIterator<Item = WindowRuleCondition>) -> Self {
|
||||||
|
self.0.all = conds.into_iter().map(|cond| cond.0).collect();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,8 +270,26 @@ impl WindowRuleCondition {
|
||||||
///
|
///
|
||||||
/// When used in [`WindowRuleCondition::any`], at least one of the
|
/// When used in [`WindowRuleCondition::any`], at least one of the
|
||||||
/// provided classes must match.
|
/// provided classes must match.
|
||||||
pub fn class(mut self, classes: &[&str]) -> Self {
|
///
|
||||||
self.0.class = Some(classes.iter().map(|s| s.to_string()).collect());
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use pinnacle_api::window::rules::WindowRuleCondition;
|
||||||
|
///
|
||||||
|
/// // `cond` will be true if the window opens with class "Alacritty"
|
||||||
|
/// let cond = WindowRuleCondition::new().classes(["Alacritty"]);
|
||||||
|
///
|
||||||
|
/// // Top level conditions need all items to be true,
|
||||||
|
/// // so the following will never be true as windows can't have two classes at once
|
||||||
|
/// let always_false = WindowRuleCondition::new().classes(["Alacritty", "firefox"]);
|
||||||
|
///
|
||||||
|
/// // To make the above work, use [`WindowRuleCondition::any`].
|
||||||
|
/// // The following will be true if the window is "Alacritty" or "firefox"
|
||||||
|
/// let any_class = WindowRuleCondition::new()
|
||||||
|
/// .any([ WindowRuleCondition::new().classes(["Alacritty", "firefox"]) ]);
|
||||||
|
/// ```
|
||||||
|
pub fn classes(mut self, classes: impl IntoIterator<Item = impl Into<String>>) -> Self {
|
||||||
|
self.0.classes = classes.into_iter().map(Into::into).collect();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,8 +300,26 @@ impl WindowRuleCondition {
|
||||||
///
|
///
|
||||||
/// When used in [`WindowRuleCondition::any`], at least one of the
|
/// When used in [`WindowRuleCondition::any`], at least one of the
|
||||||
/// provided titles must match.
|
/// provided titles must match.
|
||||||
pub fn title(mut self, titles: &[&str]) -> Self {
|
///
|
||||||
self.0.title = Some(titles.iter().map(|s| s.to_string()).collect());
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use pinnacle_api::window::rules::WindowRuleCondition;
|
||||||
|
///
|
||||||
|
/// // `cond` will be true if the window opens with title "vim"
|
||||||
|
/// let cond = WindowRuleCondition::new().titles(["vim"]);
|
||||||
|
///
|
||||||
|
/// // Top level conditions need all items to be true,
|
||||||
|
/// // so the following will never be true as windows can't have two titles at once
|
||||||
|
/// let always_false = WindowRuleCondition::new().titles(["vim", "emacs"]);
|
||||||
|
///
|
||||||
|
/// // To make the above work, use [`WindowRuleCondition::any`].
|
||||||
|
/// // The following will be true if the window has the title "vim" or "emacs"
|
||||||
|
/// let any_title = WindowRuleCondition::new()
|
||||||
|
/// .any([WindowRuleCondition::new().titles(["vim", "emacs"])]);
|
||||||
|
/// ```
|
||||||
|
pub fn titles(mut self, titles: impl IntoIterator<Item = impl Into<String>>) -> Self {
|
||||||
|
self.0.titles = titles.into_iter().map(Into::into).collect();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,8 +330,192 @@ impl WindowRuleCondition {
|
||||||
///
|
///
|
||||||
/// When used in [`WindowRuleCondition::any`], the window must open on at least
|
/// When used in [`WindowRuleCondition::any`], the window must open on at least
|
||||||
/// one of the given tags.
|
/// one of the given tags.
|
||||||
pub fn tag(mut self, tags: &[TagHandle]) -> Self {
|
///
|
||||||
self.0.tag = Some(tags.iter().map(|tag| tag.0).collect());
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use pinnacle_api::window::rules::WindowRuleCondition;
|
||||||
|
///
|
||||||
|
/// let tag1 = tag.get("1", None)?;
|
||||||
|
/// let tag2 = tag.get("2", None)?;
|
||||||
|
///
|
||||||
|
/// // `cond` will be true if the window opens with tag "1"
|
||||||
|
/// let cond = WindowRuleCondition::new().tags([&tag1]);
|
||||||
|
///
|
||||||
|
/// // Top level conditions need all items to be true,
|
||||||
|
/// // so the following will be true if the window opens with both tags "1" and "2"
|
||||||
|
/// let all_tags = WindowRuleCondition::new().tags([&tag1, &tag2]);
|
||||||
|
///
|
||||||
|
/// // This does the same as the above
|
||||||
|
/// let all_tags = WindowRuleCondition::new()
|
||||||
|
/// .all([WindowRuleCondition::new().tags([&tag1, &tag2])]);
|
||||||
|
///
|
||||||
|
/// // The following will be true if the window opens with *either* tag "1" or "2"
|
||||||
|
/// let any_tag = WindowRuleCondition::new()
|
||||||
|
/// .any([WindowRuleCondition::new().tags([&tag1, &tag2])]);
|
||||||
|
/// ```
|
||||||
|
pub fn tags<'a>(mut self, tags: impl IntoIterator<Item = &'a TagHandle>) -> Self {
|
||||||
|
self.0.tags = tags.into_iter().map(|tag| tag.id).collect();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A window rule.
|
||||||
|
///
|
||||||
|
/// This is what will be applied to a window if it meets a [`WindowRuleCondition`].
|
||||||
|
///
|
||||||
|
/// `WindowRule`s are built using the builder pattern.
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct WindowRule(pub(super) window::v0alpha1::WindowRule);
|
||||||
|
|
||||||
|
impl WindowRule {
|
||||||
|
/// Create a new, empty window rule.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This rule will force windows to open on the provided `output`.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use pinnacle_api::window::rules::WindowRule;
|
||||||
|
///
|
||||||
|
/// // Force the window to open on "HDMI-1"
|
||||||
|
/// let rule = WindowRule::new().output(output.get_by_name("HDMI-1")?);
|
||||||
|
/// ```
|
||||||
|
pub fn output(mut self, output: &OutputHandle) -> Self {
|
||||||
|
self.0.output = Some(output.name.clone());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This rule will force windows to open with the provided `tags`.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use pinnacle_api::window::rules::WindowRule;
|
||||||
|
///
|
||||||
|
/// let op = output.get_by_name("HDMI-1")?;
|
||||||
|
/// let tag1 = tag.get("1", &op)?;
|
||||||
|
/// let tag2 = tag.get("2", &op)?;
|
||||||
|
///
|
||||||
|
/// // Force the window to open with tags "1" and "2"
|
||||||
|
/// let rule = WindowRule::new().tags([&tag1, &tag2]);
|
||||||
|
/// ```
|
||||||
|
pub fn tags<'a>(mut self, tags: impl IntoIterator<Item = &'a TagHandle>) -> Self {
|
||||||
|
self.0.tags = tags.into_iter().map(|tag| tag.id).collect();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This rule will force windows to open either floating or not.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use pinnacle_api::window::rules::WindowRule;
|
||||||
|
///
|
||||||
|
/// // Force the window to open floating
|
||||||
|
/// let rule = WindowRule::new().floating(true);
|
||||||
|
///
|
||||||
|
/// // Force the window to open tiled
|
||||||
|
/// let rule = WindowRule::new().floating(false);
|
||||||
|
/// ```
|
||||||
|
pub fn floating(mut self, floating: bool) -> Self {
|
||||||
|
self.0.floating = Some(floating);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This rule will force windows to open either fullscreen, maximized, or neither.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use pinnacle_api::window::rules::WindowRule;
|
||||||
|
/// use pinnacle_api::window::FullscreenOrMaximized;
|
||||||
|
///
|
||||||
|
/// // Force the window to open fullscreen
|
||||||
|
/// let rule = WindowRule::new().fullscreen_or_maximized(FullscreenOrMaximized::Fullscreen);
|
||||||
|
///
|
||||||
|
/// // Force the window to open maximized
|
||||||
|
/// let rule = WindowRule::new().fullscreen_or_maximized(FullscreenOrMaximized::Maximized);
|
||||||
|
///
|
||||||
|
/// // Force the window to open not fullscreen nor maximized
|
||||||
|
/// let rule = WindowRule::new().fullscreen_or_maximized(FullscreenOrMaximized::Neither);
|
||||||
|
/// ```
|
||||||
|
pub fn fullscreen_or_maximized(
|
||||||
|
mut self,
|
||||||
|
fullscreen_or_maximized: FullscreenOrMaximized,
|
||||||
|
) -> Self {
|
||||||
|
self.0.fullscreen_or_maximized = Some(fullscreen_or_maximized as i32);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This rule will force windows to open at a specific x-coordinate.
|
||||||
|
///
|
||||||
|
/// This will only actually be visible if the window is also floating.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use pinnacle_api::window::rules::WindowRule;
|
||||||
|
///
|
||||||
|
/// // Force the window to open at x = 480
|
||||||
|
/// let rule = WindowRule::new().x(480);
|
||||||
|
/// ```
|
||||||
|
pub fn x(mut self, x: i32) -> Self {
|
||||||
|
self.0.x = Some(x);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This rule will force windows to open at a specific y-coordinate.
|
||||||
|
///
|
||||||
|
/// This will only actually be visible if the window is also floating.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use pinnacle_api::window::rules::WindowRule;
|
||||||
|
///
|
||||||
|
/// // Force the window to open at y = 240
|
||||||
|
/// let rule = WindowRule::new().y(240);
|
||||||
|
/// ```
|
||||||
|
pub fn y(mut self, y: i32) -> Self {
|
||||||
|
self.0.y = Some(y);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This rule will force windows to open with a specific width.
|
||||||
|
///
|
||||||
|
/// This will only actually be visible if the window is also floating.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use pinnacle_api::window::rules::WindowRule;
|
||||||
|
///
|
||||||
|
/// // Force the window to open with a width of 500 pixels
|
||||||
|
/// let rule = WindowRule::new().width(500);
|
||||||
|
/// ```
|
||||||
|
pub fn width(mut self, width: u32) -> Self {
|
||||||
|
self.0.width = Some(width as i32);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This rule will force windows to open with a specific height.
|
||||||
|
///
|
||||||
|
/// This will only actually be visible if the window is also floating.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use pinnacle_api::window::rules::WindowRule;
|
||||||
|
///
|
||||||
|
/// // Force the window to open with a height of 250 pixels
|
||||||
|
/// let rule = WindowRule::new().height(250);
|
||||||
|
/// ```
|
||||||
|
pub fn height(mut self, height: u32) -> Self {
|
||||||
|
self.0.height = Some(height as i32);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"]
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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),
|
|
||||||
}
|
|
|
@ -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))
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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>,
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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>,
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue