Add client side window rules API

This commit is contained in:
Ottatop 2023-09-05 22:13:43 -05:00
parent 4f8d662dd3
commit ec651e24b3
11 changed files with 218 additions and 207 deletions

View file

@ -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()

View file

@ -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?

View file

@ -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()

View file

@ -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.
---

68
api/lua/window_rules.lua Normal file
View file

@ -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

View file

@ -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.

View file

@ -59,7 +59,7 @@ pub enum Msg {
},
AddWindowRule {
cond: WindowRuleCondition,
rule: Vec<WindowRule>,
rule: WindowRule,
},
// Tag management

View file

@ -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<WindowRuleCondition>),
@ -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<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)>,
}

View file

@ -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::<Vec<_>>();
if let Some(tag_ids) = tags {
let tags = tag_ids
.iter()
.filter_map(|tag_id| tag_id.tag(&data.state))
.collect::<Vec<_>>();
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))
});
}
}
}

View file

@ -126,7 +126,7 @@ pub struct State {
pub dnd_icon: Option<WlSurface>,
pub windows: Vec<WindowElement>,
pub window_rules: Vec<(WindowRuleCondition, Vec<WindowRule>)>,
pub window_rules: Vec<(WindowRuleCondition, WindowRule)>,
pub async_scheduler: Scheduler<()>,
pub config_process: async_process::Child,

View file

@ -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,
// );
// }
}
}
}