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