From ec651e24b38956f4c30ec86f7261a5cb327ac3b2 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Tue, 5 Sep 2023 22:13:43 -0500 Subject: [PATCH] Add client side window rules API --- api/lua/example_config.lua | 6 ++ api/lua/msg.lua | 1 + api/lua/test_config.lua | 147 +++++++------------------------ api/lua/window.lua | 5 +- api/lua/window_rules.lua | 68 +++++++++++++++ api/lua/window_rules_types.lua | 21 +++++ src/api/msg.rs | 2 +- src/api/msg/window_rules.rs | 7 ++ src/handlers/xdg_shell.rs | 154 ++++++++++++++++----------------- src/state.rs | 2 +- src/window.rs | 12 +-- 11 files changed, 218 insertions(+), 207 deletions(-) create mode 100644 api/lua/window_rules.lua create mode 100644 api/lua/window_rules_types.lua diff --git a/api/lua/example_config.lua b/api/lua/example_config.lua index c42d875..69e1f3c 100644 --- a/api/lua/example_config.lua +++ b/api/lua/example_config.lua @@ -105,6 +105,12 @@ require("pinnacle").setup(function(pinnacle) } local indices = {} + -- Window rules + window.rules.add({ + cond = { class = "kitty" }, + rule = { size = { 300, 300 }, location = { 50, 50 } }, + }) + -- Layout cycling -- Yes, this is overly complicated and yes, I'll cook up a way to make it less so. input.keybind({ mod_key }, keys.space, function() diff --git a/api/lua/msg.lua b/api/lua/msg.lua index 3a02400..e0a8409 100644 --- a/api/lua/msg.lua +++ b/api/lua/msg.lua @@ -13,6 +13,7 @@ ---@field ToggleFloating { window_id: WindowId }? ---@field ToggleFullscreen { window_id: WindowId }? ---@field ToggleMaximized { window_id: WindowId }? +---@field AddWindowRule { cond: WindowRuleCondition, rule: WindowRule }? -- ---@field Spawn { command: string[], callback_id: integer? }? ---@field Request Request? diff --git a/api/lua/test_config.lua b/api/lua/test_config.lua index 407dbeb..cd59682 100644 --- a/api/lua/test_config.lua +++ b/api/lua/test_config.lua @@ -27,10 +27,21 @@ require("pinnacle").setup(function(pinnacle) local terminal = "alacritty" + -- Outputs ----------------------------------------------------------------------- + + -- You can set your own monitor layout as I have done below for my monitors. + + -- local lg = output.get_by_name("DP-2") --[[@as Output]] + -- local dell = output.get_by_name("DP-3") --[[@as Output]] + -- + -- dell:set_loc_left_of(lg, "bottom") + -- Keybinds ---------------------------------------------------------------------- + -- mod_key + Alt + q quits the compositor input.keybind({ mod_key, "Alt" }, keys.q, pinnacle.quit) + -- mod_key + Alt + c closes the focused window input.keybind({ mod_key, "Alt" }, keys.c, function() -- The commented out line may crash the config process if you have no windows open. -- There is no nil warning here due to limitations in Lua LS type checking, so check for nil as shown below. @@ -41,6 +52,14 @@ require("pinnacle").setup(function(pinnacle) end end) + -- mod_key + return spawns a terminal + input.keybind({ mod_key }, keys.Return, function() + process.spawn(terminal, function(stdout, stderr, exit_code, exit_msg) + -- do something with the output here + end) + end) + + -- mod_key + Alt + Space toggle floating on the focused window input.keybind({ mod_key, "Alt" }, keys.space, function() local win = window.get_focused() if win ~= nil then @@ -48,138 +67,30 @@ require("pinnacle").setup(function(pinnacle) end end) - input.keybind({ mod_key }, keys.Return, function() - process.spawn(terminal, function(stdout, stderr, exit_code, exit_msg) - -- do something with the output here - end) - end) - - input.keybind({ mod_key }, keys.l, function() - process.spawn("kitty") - end) - input.keybind({ mod_key }, keys.k, function() - process.spawn("foot") - end) - input.keybind({ mod_key }, keys.j, function() - process.spawn("nautilus") - end) - + -- mod_key + f toggles fullscreen on the focused window input.keybind({ mod_key }, keys.f, function() local win = window.get_focused() if win ~= nil then - win:set_status("Fullscreen") + win:toggle_fullscreen() end end) + -- mod_key + m toggles maximized on the focused window input.keybind({ mod_key }, keys.m, function() local win = window.get_focused() if win ~= nil then - win:set_status("Maximized") + win:toggle_maximized() end end) - input.keybind({ mod_key }, keys.t, function() - local win = window.get_focused() - if win ~= nil then - win:set_status("Tiled") - end - end) - - -- Just testing stuff - input.keybind({ mod_key }, keys.h, function() - local dp2 = output.get_by_name("DP-2") - local dp3 = output.get_by_name("DP-3") - - dp2:set_loc_bottom_of(dp3, "right") - - -- local win = window.get_focused() - -- if win ~= nil then - -- win:set_size({ w = 500, h = 500 }) - -- end - - -- local wins = window.get_all() - -- for _, win in pairs(wins) do - -- print("loc: " .. (win:loc() and win:loc().x or "nil") .. ", " .. (win:loc() and win:loc().y or "nil")) - -- print("size: " .. (win:size() and win:size().w or "nil") .. ", " .. (win:size() and win:size().h or "nil")) - -- print("class: " .. (win:class() or "nil")) - -- print("title: " .. (win:title() or "nil")) - -- print("float: " .. tostring(win:floating())) - -- end - -- - -- print("----------------------") - -- - -- local op = output.get_focused() --[[@as Output]] - -- print("res: " .. (op:res() and (op:res().w .. ", " .. op:res().h) or "nil")) - -- print("loc: " .. (op:loc() and (op:loc().x .. ", " .. op:loc().y) or "nil")) - -- print("rr: " .. (op:refresh_rate() or "nil")) - -- print("make: " .. (op:make() or "nil")) - -- print("model: " .. (op:model() or "nil")) - -- print("focused: " .. (tostring(op:focused()))) - -- - -- print("----------------------") - -- - -- local wins = window.get_by_class("Alacritty") - -- for _, win in pairs(wins) do - -- print("loc: " .. (win:loc() and win:loc().x or "nil") .. ", " .. (win:loc() and win:loc().y or "nil")) - -- print("size: " .. (win:size() and win:size().w or "nil") .. ", " .. (win:size() and win:size().h or "nil")) - -- print("class: " .. (win:class() or "nil")) - -- print("title: " .. (win:title() or "nil")) - -- print("float: " .. tostring(win:floating())) - -- end - -- - -- print("----------------------") - -- - -- local wins = window.get_by_title("~/p/pinnacle") - -- for _, win in pairs(wins) do - -- print("loc: " .. (win:loc() and win:loc().x or "nil") .. ", " .. (win:loc() and win:loc().y or "nil")) - -- print("size: " .. (win:size() and win:size().w or "nil") .. ", " .. (win:size() and win:size().h or "nil")) - -- print("class: " .. (win:class() or "nil")) - -- print("title: " .. (win:title() or "nil")) - -- print("float: " .. tostring(win:floating())) - -- end - -- - -- print("----------------------") - -- - -- local tags = tag.get_on_output(output.get_focused() --[[@as Output]]) - -- for _, tg in pairs(tags) do - -- print(tg:name()) - -- print((tg:output() and tg:output():name()) or "nil output") - -- print(tg:active()) - -- end - -- - -- print("----------------------") - -- - -- local tags = tag.get_by_name("2") - -- for _, tg in pairs(tags) do - -- print(tg:name()) - -- print((tg:output() and tg:output():name()) or "nil output") - -- print(tg:active()) - -- end - -- - -- print("----------------------") - -- - -- local tags = tag.get_all() - -- for _, tg in pairs(tags) do - -- print(tg:name()) - -- print((tg:output() and tg:output():name()) or "nil output") - -- print(tg:active()) - -- end - end) - -- Tags --------------------------------------------------------------------------- output.connect_for_all(function(op) + -- Add tags 1, 2, 3, 4 and 5 on all monitors, and toggle tag 1 active by default + op:add_tags("1", "2", "3", "4", "5") -- Same as tag.add(op, "1", "2", "3", "4", "5") - - -- local tags_table = { "Terminal", "Browser", "Code", "Email", "Potato" } - -- op:add_tags(tags_table) - - -- for _, t in pairs(tag.get_by_name("1")) do - -- t:toggle() - -- end - - tag.toggle("1", op) + tag.toggle({ "1", op }) end) ---@type Layout[] @@ -194,6 +105,12 @@ require("pinnacle").setup(function(pinnacle) } local indices = {} + -- Window rules + window.rules.add({ + cond = { class = "kitty" }, + rule = { floating_or_tiled = "Floating" }, + }) + -- Layout cycling -- Yes, this is overly complicated and yes, I'll cook up a way to make it less so. input.keybind({ mod_key }, keys.space, function() diff --git a/api/lua/window.lua b/api/lua/window.lua index 7b7bd9d..50c1855 100644 --- a/api/lua/window.lua +++ b/api/lua/window.lua @@ -5,7 +5,10 @@ ---This module helps you deal with setting windows to fullscreen and maximized, setting their size, ---moving them between tags, and various other actions. ---@class WindowModule -local window_module = {} +local window_module = { + ---Window rules. + rules = require("window_rules"), +} ---A window object. --- diff --git a/api/lua/window_rules.lua b/api/lua/window_rules.lua new file mode 100644 index 0000000..82bcc49 --- /dev/null +++ b/api/lua/window_rules.lua @@ -0,0 +1,68 @@ +---Rules that apply to spawned windows when conditions are met. +---@class WindowRules +local window_rules = {} + +---Add one or more window rules. +--- +---A window rule defines what a window will spawn with given certain conditions. +---For example, if Firefox is spawned, you can set it to open on the second tag. +--- +---This function takes in a table with two keys: +--- - `cond`: The condition for `rule` to apply to a new window. +--- - `rule`: What gets applied to the new window if `cond` is true. +--- +---`cond` can be a bit confusing and *very* table heavy. Examples are shown below for guidance. +---An attempt at simplifying this API will happen in the future, but is a low priority. +--- +---### Examples +---```lua +----- A simple window rule. This one will cause Firefox to open on tag "Browser". +---window.rules.add({ +--- cond = { class = "firefox" }, +--- rule = { tags = { "Browser" } }, +---}) +--- +----- To apply rules when *all* provided conditions are true, use `cond_all`. +----- `cond_all` takes an array of conditions and checks if all are true. +----- Note that `cond_any` is not a keyed table; rather, it's a table of tables. +--- +----- The following will open Steam fullscreen only if it opens on tag "5". +---window.rules.add({ +--- cond = { +--- cond_any = { +--- { class = "steam" }, -- Note that each table must only have one key. +--- { tag = tag.get_by_name("5")[1] }, +--- } +--- }, +--- rule = { fullscreen_or_maximized = "Fullscreen" }, +---}) +--- +----- You can arbitrarily nest `cond_any` and `cond_all` to achieve desired logic. +----- The following will open Discord, Thunderbird, or Alacritty floating if they +----- open on either *all* of tags "A", "B", and "C" or both tags "1" and "2". +---window.rules.add({ +--- cond = { cond_all = { +--- { cond_any = { { class = "discord" }, { class = "firefox" }, { class = "thunderbird" } } }, +--- { cond_any = { +--- { cond_all = { { tag = "A" }, { tag = "B" }, { tag = "C" } } }, +--- { cond_all = { { tag = "1" }, { tag = "2" } } }, +--- } } +--- } }, +--- rule = { floating_or_tiled = "Floating" }, +---}) +---``` +---@param ... { cond: WindowRuleCondition, rule: WindowRule } +function window_rules.add(...) + local rules = { ... } + + for _, rule in pairs(rules) do + SendMsg({ + AddWindowRule = { + cond = rule.cond, + rule = rule.rule, + }, + }) + end +end + +return window_rules diff --git a/api/lua/window_rules_types.lua b/api/lua/window_rules_types.lua new file mode 100644 index 0000000..b720441 --- /dev/null +++ b/api/lua/window_rules_types.lua @@ -0,0 +1,21 @@ +-- SPDX-License-Identifier: GPL-3.0-or-later + +---@meta _ + +---Conditions for window rules. Only one condition can be in the table. +---If you have more than one you need to check for, use `cond_any` or `cond_all` +---to check for any or all conditions. +---@class WindowRuleCondition +---@field cond_any WindowRuleCondition[]? At least one provided condition must be true. +---@field cond_all WindowRuleCondition[]? All provided conditions must be true. +---@field class string? The window must have this class. +---@field title string? The window must have this title. +---@field tag TagId? The window must be on this tag. + +---@class WindowRule Attributes the window will be spawned with. +---@field output OutputName? The output this window will be spawned on. TODO: +---@field tags TagId[]? The tags this window will be spawned with. +---@field floating_or_tiled ("Floating"|"Tiled")? Whether or not this window will be spawned floating or tiled. +---@field fullscreen_or_maximized FullscreenOrMaximized? Whether or not this window will be spawned fullscreen, maximized, or forced to neither. +---@field size { [1]: integer, [2]: integer }? The size the window will spawn with, with [1] being width and [2] being height. This must be a strictly positive integer; putting 0 will crash the compositor. +---@field location { [1]: integer, [2]: integer }? The location the window will spawn at. If the window spawns tiled, it will instead snap to this location when set to floating. diff --git a/src/api/msg.rs b/src/api/msg.rs index 1e6cee9..98005c1 100644 --- a/src/api/msg.rs +++ b/src/api/msg.rs @@ -59,7 +59,7 @@ pub enum Msg { }, AddWindowRule { cond: WindowRuleCondition, - rule: Vec, + rule: WindowRule, }, // Tag management diff --git a/src/api/msg/window_rules.rs b/src/api/msg/window_rules.rs index 082e3d6..fca48ce 100644 --- a/src/api/msg/window_rules.rs +++ b/src/api/msg/window_rules.rs @@ -12,6 +12,7 @@ use crate::{ }; #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] pub enum WindowRuleCondition { /// This condition is met when any of the conditions provided is met. CondAny(Vec), @@ -85,17 +86,23 @@ impl WindowRuleCondition { #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct WindowRule { /// Set the output the window will open on. + #[serde(default)] pub output: Option, /// Set the tags the output will have on open. + #[serde(default)] pub tags: Option>, /// Set the window to floating or tiled on open. + #[serde(default)] pub floating_or_tiled: Option, /// Set the window to fullscreen, maximized, or force it to neither. + #[serde(default)] pub fullscreen_or_maximized: Option, /// 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)>, } diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs index a90e9ab..0580871 100644 --- a/src/handlers/xdg_shell.rs +++ b/src/handlers/xdg_shell.rs @@ -116,98 +116,96 @@ impl XdgShellHandler for State { } }, |data| { - for (cond, rules) in data.state.window_rules.iter() { + for (cond, rule) in data.state.window_rules.iter() { if cond.is_met(&data.state, &window) { - for rule in rules { - let WindowRule { - output, - tags, - floating_or_tiled, - fullscreen_or_maximized, - size, - location, - } = rule; + let WindowRule { + output, + tags, + floating_or_tiled, + fullscreen_or_maximized, + size, + location, + } = rule; - if let Some(_output_name) = output { - // TODO: - } + if let Some(_output_name) = output { + // TODO: + } - if let Some(tag_ids) = tags { - let tags = tag_ids - .iter() - .filter_map(|tag_id| tag_id.tag(&data.state)) - .collect::>(); + if let Some(tag_ids) = tags { + let tags = tag_ids + .iter() + .filter_map(|tag_id| tag_id.tag(&data.state)) + .collect::>(); - window.with_state(|state| state.tags = tags.clone()); - } + window.with_state(|state| state.tags = tags.clone()); + } - if let Some(floating_or_tiled) = floating_or_tiled { - match floating_or_tiled { - window_rules::FloatingOrTiled::Floating => { - if window - .with_state(|state| state.floating_or_tiled.is_tiled()) - { - window.toggle_floating(); - } + if let Some(floating_or_tiled) = floating_or_tiled { + match floating_or_tiled { + window_rules::FloatingOrTiled::Floating => { + if window.with_state(|state| state.floating_or_tiled.is_tiled()) + { + window.toggle_floating(); } - window_rules::FloatingOrTiled::Tiled => { - if window.with_state(|state| { - state.floating_or_tiled.is_floating() - }) { - window.toggle_floating(); - } + } + window_rules::FloatingOrTiled::Tiled => { + if window + .with_state(|state| state.floating_or_tiled.is_floating()) + { + window.toggle_floating(); } } } + } - if let Some(fs_or_max) = fullscreen_or_maximized { - window - .with_state(|state| state.fullscreen_or_maximized = *fs_or_max); - } + if let Some(fs_or_max) = fullscreen_or_maximized { + window.with_state(|state| state.fullscreen_or_maximized = *fs_or_max); + } - if let Some((w, h)) = size { - // TODO: tiled vs floating - // FIXME: this will map unmapped windows at 0,0 - let window_loc = data - .state - .space - .element_location(&window) - .unwrap_or((0, 0).into()); - let mut window_size = window.geometry().size; - window_size.w = u32::from(*w) as i32; - window_size.h = u32::from(*h) as i32; + if let Some((w, h)) = size { + let mut window_size = window.geometry().size; + window_size.w = u32::from(*w) as i32; + window_size.h = u32::from(*h) as i32; - // FIXME: this will resize tiled windows - window.request_size_change( - &mut data.state.space, - window_loc, - window_size, - ); - } - - if let Some(loc) = location { - match window.with_state(|state| state.floating_or_tiled) { - FloatingOrTiled::Floating(mut rect) => { - rect.loc = (*loc).into(); - window.with_state(|state| { - state.floating_or_tiled = - FloatingOrTiled::Floating(rect) - }); - data.state.space.map_element(window.clone(), *loc, false); + match window.with_state(|state| state.floating_or_tiled) { + FloatingOrTiled::Floating(mut rect) => { + rect.size = (u32::from(*w) as i32, u32::from(*h) as i32).into(); + window.with_state(|state| { + state.floating_or_tiled = FloatingOrTiled::Floating(rect) + }); + } + FloatingOrTiled::Tiled(mut rect) => { + if let Some(rect) = rect.as_mut() { + rect.size = + (u32::from(*w) as i32, u32::from(*h) as i32).into(); } - FloatingOrTiled::Tiled(rect) => { - // If the window is tiled, don't set the size. Instead, set - // what the size will be when it gets set to floating. - let rect = rect.unwrap_or_else(|| { - let size = window.geometry().size; - Rectangle::from_loc_and_size(Point::from(*loc), size) - }); + window.with_state(|state| { + state.floating_or_tiled = FloatingOrTiled::Tiled(rect) + }); + } + } + } - window.with_state(|state| { - state.floating_or_tiled = - FloatingOrTiled::Tiled(Some(rect)) - }); - } + if let Some(loc) = location { + match window.with_state(|state| state.floating_or_tiled) { + FloatingOrTiled::Floating(mut rect) => { + rect.loc = (*loc).into(); + window.with_state(|state| { + state.floating_or_tiled = FloatingOrTiled::Floating(rect) + }); + data.state.space.map_element(window.clone(), *loc, false); + } + FloatingOrTiled::Tiled(rect) => { + // If the window is tiled, don't set the size. Instead, set + // what the size will be when it gets set to floating. + let rect = rect.unwrap_or_else(|| { + let size = window.geometry().size; + Rectangle::from_loc_and_size(Point::from(*loc), size) + }); + + window.with_state(|state| { + state.floating_or_tiled = FloatingOrTiled::Tiled(Some(rect)) + }); } } } diff --git a/src/state.rs b/src/state.rs index 5df2750..5c5f752 100644 --- a/src/state.rs +++ b/src/state.rs @@ -126,7 +126,7 @@ pub struct State { pub dnd_icon: Option, pub windows: Vec, - pub window_rules: Vec<(WindowRuleCondition, Vec)>, + pub window_rules: Vec<(WindowRuleCondition, WindowRule)>, pub async_scheduler: Scheduler<()>, pub config_process: async_process::Child, diff --git a/src/window.rs b/src/window.rs index f05578b..a0ab173 100644 --- a/src/window.rs +++ b/src/window.rs @@ -221,23 +221,13 @@ impl WindowElement { surface .configure(Rectangle::from_loc_and_size(new_loc, new_size)) .expect("failed to configure x11 win"); - // self.with_state(|state| { - // state.resize_state = WindowResizeState::Acknowledged(new_loc); - // }); + if !surface.is_override_redirect() { surface .set_mapped(true) .expect("failed to set x11 win to mapped"); } space.map_element(self.clone(), new_loc, false); - // if let Some(focused_output) = state.focus_state.focused_output.clone() { - // self.send_frame( - // &focused_output, - // state.clock.now(), - // Some(Duration::ZERO), - // surface_primary_scanout_output, - // ); - // } } } }