diff --git a/api/lua/example_config.lua b/api/lua/example_config.lua index 647bcb4..d58ea22 100644 --- a/api/lua/example_config.lua +++ b/api/lua/example_config.lua @@ -12,7 +12,7 @@ pcall(require, "luarocks.loader") -- Neovim users be like: require("pinnacle").setup(function(pinnacle) local input = pinnacle.input -- Key and mouse binds - local client = pinnacle.client -- Window management + local window = pinnacle.window -- Window management local process = pinnacle.process -- Process spawning local tag = pinnacle.tag -- Tag management @@ -20,62 +20,90 @@ require("pinnacle").setup(function(pinnacle) -- Support for just putting in a string of a key is intended. local keys = input.keys + ---@type Modifier + local mod_key = "Ctrl" -- This is set to `Ctrl` instead of `Super` to not conflict with your WM/DE keybinds + -- ^ Add type annotations for that sweet, sweet autocomplete + + local terminal = "alacritty" + -- Keybinds ---------------------------------------------------------------------- - input.keybind({ "Ctrl", "Alt" }, keys.q, pinnacle.quit) + input.keybind({ mod_key, "Alt" }, keys.q, pinnacle.quit) - input.keybind({ "Ctrl", "Alt" }, keys.c, client.close_window) + input.keybind({ mod_key, "Alt" }, keys.c, window.close_window) - input.keybind({ "Ctrl", "Alt" }, keys.space, client.toggle_floating) + input.keybind({ mod_key, "Alt" }, keys.space, window.toggle_floating) - input.keybind({ "Ctrl" }, keys.Return, function() - process.spawn("alacritty", function(stdout, stderr, exit_code, exit_msg) - -- do something with the output here; remember to check for nil! + 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({ "Ctrl" }, keys.KEY_1, function() - process.spawn("kitty") - end) - input.keybind({ "Ctrl" }, keys.KEY_2, function() - process.spawn("foot") - end) - input.keybind({ "Ctrl" }, keys.KEY_3, function() - process.spawn("nautilus") - end) - -- Tags --------------------------------------------------------------------------- tag.add("1", "2", "3", "4", "5") tag.toggle("1") - input.keybind({ "Ctrl", "Shift" }, keys.KEY_1, function() + input.keybind({ mod_key }, keys.KEY_1, function() + tag.switch_to("1") + end) + input.keybind({ mod_key }, keys.KEY_2, function() + tag.switch_to("2") + end) + input.keybind({ mod_key }, keys.KEY_3, function() + tag.switch_to("3") + end) + input.keybind({ mod_key }, keys.KEY_4, function() + tag.switch_to("4") + end) + input.keybind({ mod_key }, keys.KEY_5, function() + tag.switch_to("5") + end) + + input.keybind({ mod_key, "Shift" }, keys.KEY_1, function() tag.toggle("1") end) - input.keybind({ "Ctrl", "Shift" }, keys.KEY_2, function() + input.keybind({ mod_key, "Shift" }, keys.KEY_2, function() tag.toggle("2") end) - input.keybind({ "Ctrl", "Shift" }, keys.KEY_3, function() + input.keybind({ mod_key, "Shift" }, keys.KEY_3, function() tag.toggle("3") end) - input.keybind({ "Ctrl", "Shift" }, keys.KEY_4, function() + input.keybind({ mod_key, "Shift" }, keys.KEY_4, function() tag.toggle("4") end) - input.keybind({ "Ctrl", "Shift" }, keys.KEY_5, function() + input.keybind({ mod_key, "Shift" }, keys.KEY_5, function() tag.toggle("5") end) - input.keybind({ "Ctrl", "Alt", "Shift" }, keys.KEY_1, function() - tag.switch_to("1") + input.keybind({ mod_key, "Alt" }, keys.KEY_1, function() + window.get_focused():move_to_tag("1") end) - input.keybind({ "Ctrl", "Alt", "Shift" }, keys.KEY_2, function() - tag.switch_to("2") + input.keybind({ mod_key, "Alt" }, keys.KEY_2, function() + window.get_focused():move_to_tag("2") end) - input.keybind({ "Ctrl", "Alt", "Shift" }, keys.KEY_3, function() - tag.switch_to("3") + input.keybind({ mod_key, "Alt" }, keys.KEY_3, function() + window.get_focused():move_to_tag("3") end) - input.keybind({ "Ctrl", "Alt", "Shift" }, keys.KEY_4, function() - tag.switch_to("4") + input.keybind({ mod_key, "Alt" }, keys.KEY_4, function() + window.get_focused():move_to_tag("4") end) - input.keybind({ "Ctrl", "Alt", "Shift" }, keys.KEY_5, function() - tag.switch_to("5") + input.keybind({ mod_key, "Alt" }, keys.KEY_5, function() + window.get_focused():move_to_tag("5") + end) + + input.keybind({ mod_key, "Shift", "Alt" }, keys.KEY_1, function() + window.get_focused():toggle_tag("1") + end) + input.keybind({ mod_key, "Shift", "Alt" }, keys.KEY_2, function() + window.get_focused():toggle_tag("2") + end) + input.keybind({ mod_key, "Shift", "Alt" }, keys.KEY_3, function() + window.get_focused():toggle_tag("3") + end) + input.keybind({ mod_key, "Shift", "Alt" }, keys.KEY_4, function() + window.get_focused():toggle_tag("4") + end) + input.keybind({ mod_key, "Shift", "Alt" }, keys.KEY_5, function() + window.get_focused():toggle_tag("5") end) end) diff --git a/api/lua/input.lua b/api/lua/input.lua index 4cda139..a362ee0 100644 --- a/api/lua/input.lua +++ b/api/lua/input.lua @@ -8,9 +8,9 @@ local input = { keys = require("keys"), } ----Set a keybind. If called on an already existing keybind, it gets replaced. ----@param key Keys The key for the keybind. NOTE: uppercase and lowercase characters are considered different. ----@param modifiers Modifiers[] Which modifiers need to be pressed for the keybind to trigger. +---Set a keybind. If called with an already existing keybind, it gets replaced. +---@param key Keys The key for the keybind. +---@param modifiers (Modifier)[] Which modifiers need to be pressed for the keybind to trigger. ---@param action fun() What to run. function input.keybind(modifiers, key, action) table.insert(CallbackTable, action) diff --git a/api/lua/keys.lua b/api/lua/keys.lua index 2c0a8b5..88286e0 100644 --- a/api/lua/keys.lua +++ b/api/lua/keys.lua @@ -4,7 +4,7 @@ -- -- SPDX-License-Identifier: MPL-2.0 ----@alias Modifiers "Alt" | "Ctrl" | "Shift" | "Super" +---@alias Modifier "Alt" | "Ctrl" | "Shift" | "Super" ---@enum Keys local M = { diff --git a/api/lua/msg.lua b/api/lua/msg.lua index 2cfcf9a..c56ea11 100644 --- a/api/lua/msg.lua +++ b/api/lua/msg.lua @@ -9,9 +9,13 @@ ---@class _Msg ---@field SetKeybind { key: Keys, modifiers: Modifiers[], callback_id: integer } ---@field SetMousebind { button: integer } +--Windows ---@field CloseWindow { client_id: integer? } ---@field ToggleFloating { client_id: integer? } ---@field SetWindowSize { window_id: integer, size: { w: integer, h: integer } } +---@field MoveWindowToTag { window_id: integer, tag_id: string } +---@field ToggleTagOnWindow { window_id: integer, tag_id: string } +-- ---@field Spawn { command: string[], callback_id: integer? } ---@field Request Request --Tags diff --git a/api/lua/pinnacle.lua b/api/lua/pinnacle.lua index 51187ab..d2fbf0f 100644 --- a/api/lua/pinnacle.lua +++ b/api/lua/pinnacle.lua @@ -51,7 +51,7 @@ local pinnacle = { ---Key and mouse binds input = require("input"), ---Window management - client = require("client"), + window = require("window"), ---Process spawning process = require("process"), ---Tag management diff --git a/api/lua/tag.lua b/api/lua/tag.lua index 818b3c5..decce6f 100644 --- a/api/lua/tag.lua +++ b/api/lua/tag.lua @@ -8,6 +8,8 @@ local tag = {} ---Add tags. --- +---If you need to add the strings in a table, use `tag.add_table` instead. +--- ---# Example --- ---```lua @@ -25,7 +27,7 @@ function tag.add(...) }) end ----Like `tag.add(...)`, but with a table of strings instead. +---Like `tag.add`, but with a table of strings instead. ---@param tags string[] The names of the new tags you want to add, as a table. function tag.add_table(tags) SendMsg({ diff --git a/api/lua/client.lua b/api/lua/window.lua similarity index 55% rename from api/lua/client.lua rename to api/lua/window.lua index 146331b..810e2c7 100644 --- a/api/lua/client.lua +++ b/api/lua/window.lua @@ -11,13 +11,13 @@ ---@field private size { w: integer, h: integer } The size of the window ---@field private location { x: integer, y: integer } The location of the window ---@field private floating boolean Whether the window is floating or not (tiled) -local window = {} +local win = {} ---@param props { id: integer, app_id: string?, title: string?, size: { w: integer, h: integer }, location: { x: integer, y: integer }, floating: boolean } ---@return Window local function new_window(props) -- Copy functions over - for k, v in pairs(window) do + for k, v in pairs(win) do props[k] = v end @@ -26,7 +26,7 @@ end ---Set a window's size. ---@param size { w: integer?, h: integer? } -function window:set_size(size) +function win:set_size(size) self.size = { w = size.w or self.size.w, h = size.h or self.size.h, @@ -39,19 +39,41 @@ function window:set_size(size) }) end +---Move a window to a tag, removing all other ones. +---@param name string The name of the tag. +function win:move_to_tag(name) + SendMsg({ + MoveWindowToTag = { + window_id = self.id, + tag_id = name, + }, + }) +end + +---Toggle the specified tag for this window. +---@param name string The name of the tag. +function win:toggle_tag(name) + SendMsg({ + ToggleTagOnWindow = { + window_id = self.id, + tag_id = name, + }, + }) +end + ---Get a window's size. ---@return { w: integer, h: integer } -function window:get_size() +function win:get_size() return self.size end ------------------------------------------------------------------- -local client = {} +local window = {} ---Close a window. ---@param client_id integer? The id of the window you want closed, or nil to close the currently focused window, if any. -function client.close_window(client_id) +function window.close_window(client_id) SendMsg({ CloseWindow = { client_id = client_id, @@ -61,7 +83,7 @@ end ---Toggle a window's floating status. ---@param client_id integer? The id of the window you want to toggle, or nil to toggle the currently focused window, if any. -function client.toggle_floating(client_id) +function window.toggle_floating(client_id) SendMsg({ ToggleFloating = { client_id = client_id, @@ -69,39 +91,25 @@ function client.toggle_floating(client_id) }) end ----Get a window. ----@param identifier { app_id: string } | { title: string } | "focus" A table with either the key app_id or title, depending if you want to get the window via its app_id or title, OR the string "focus" to get the currently focused window. ----@return Window -function client.get_window(identifier) +---Get a window by its app id (aka its X11 class). +---@param app_id string The window's app id. For example, Alacritty's app id is "Alacritty". +---@return Window window -- TODO: nil +function window.get_by_app_id(app_id) local req_id = Requests:next() - if type(identifier) == "string" then - SendRequest({ - GetWindowByFocus = { - id = req_id, - }, - }) - elseif identifier.app_id then - SendRequest({ - GetWindowByAppId = { - id = req_id, - app_id = identifier.app_id, - }, - }) - else - SendRequest({ - GetWindowByTitle = { - id = req_id, - title = identifier.title, - }, - }) - end + + SendRequest({ + GetWindowByAppId = { + id = req_id, + app_id = app_id, + }, + }) local response = ReadMsg() local props = response.RequestResponse.response.Window.window ---@type Window - local win = { + local wind = { id = props.id, app_id = props.app_id or "", title = props.title or "", @@ -116,12 +124,82 @@ function client.get_window(identifier) floating = props.floating, } - return new_window(win) + return new_window(wind) +end + +---Get a window by its title. +---@param title string The window's title. +---@return Window +function window.get_by_title(title) + local req_id = Requests:next() + + SendRequest({ + GetWindowByTitle = { + id = req_id, + title = title, + }, + }) + + local response = ReadMsg() + + local props = response.RequestResponse.response.Window.window + + ---@type Window + local wind = { + id = props.id, + app_id = props.app_id or "", + title = props.title or "", + size = { + w = props.size[1], + h = props.size[2], + }, + location = { + x = props.location[1], + y = props.location[2], + }, + floating = props.floating, + } + + return new_window(wind) +end + +---Get the currently focused window. +---@return Window +function window.get_focused() + local req_id = Requests:next() + + SendRequest({ + GetWindowByFocus = { + id = req_id, + }, + }) + + local response = ReadMsg() + + local props = response.RequestResponse.response.Window.window + + ---@type Window + local wind = { + id = props.id, + app_id = props.app_id or "", + title = props.title or "", + size = { + w = props.size[1], + h = props.size[2], + }, + location = { + x = props.location[1], + y = props.location[2], + }, + floating = props.floating, + } + + return new_window(wind) end ---Get all windows. ---@return Window[] -function client.get_windows() +function window.get_windows() SendRequest({ GetAllWindows = { id = Requests:next(), @@ -152,6 +230,4 @@ function client.get_windows() return windows end --- local win = client.get_window("focus") - -return client +return window diff --git a/src/api/msg.rs b/src/api/msg.rs index 494e66e..97cb8bf 100644 --- a/src/api/msg.rs +++ b/src/api/msg.rs @@ -40,8 +40,12 @@ pub enum Msg { window_id: WindowId, size: (i32, i32), }, - MoveToTag { - // TODO: + MoveWindowToTag { + window_id: WindowId, + tag_id: TagId, + }, + ToggleTagOnWindow { + window_id: WindowId, tag_id: TagId, }, diff --git a/src/state.rs b/src/state.rs index d417e6f..d12a0bd 100644 --- a/src/state.rs +++ b/src/state.rs @@ -195,9 +195,34 @@ impl State { }); window.toplevel().send_pending_configure(); } - Msg::MoveToTag { tag_id } => todo!(), + Msg::MoveWindowToTag { window_id, tag_id } => { + if let Some(window) = data.state.windows.iter().find(|&win| { + WindowState::with_state(win, |state| state.id == window_id) + }) { + WindowState::with_state(window, |state| { + state.tags = vec![tag_id.clone()]; + }); + } + + data.state.re_layout(); + }, + Msg::ToggleTagOnWindow { window_id, tag_id } => { + if let Some(window) = data.state.windows.iter().find(|&win| { + WindowState::with_state(win, |state| state.id == window_id) + }) { + WindowState::with_state(window, |state| { + if state.tags.contains(&tag_id) { + state.tags.retain(|id| id != &tag_id); + } else { + state.tags.push(tag_id.clone()); + } + }); + + data.state.re_layout(); + } + }, Msg::ToggleTag { tag_id } => { - let windows = OutputState::with( + OutputState::with( data .state .focus_state @@ -215,40 +240,13 @@ impl State { tracing::debug!("toggled tag {tag_id:?} on"); } } - // re-layout - for window in data.state.space.elements().cloned().collect::>() { - let should_render = WindowState::with_state(&window, |win_state| { - for tag_id in win_state.tags.iter() { - if *state.focused_tags.get(tag_id).unwrap_or(&false) { - return true; - } - } - false - }); - if !should_render { - data.state.space.unmap_elem(&window); - } - } - - data.state.windows.iter().filter(|&win| { - WindowState::with_state(win, |win_state| { - for tag_id in win_state.tags.iter() { - if *state.focused_tags.get(tag_id).unwrap_or(&false) { - return true; - } - } - false - }) - }).cloned().collect::>() } ); - tracing::info!("Laying out {} windows", windows.len()); - - Layout::master_stack(&mut data.state, windows, crate::layout::Direction::Left); + data.state.re_layout(); }, Msg::SwitchToTag { tag_id } => { - let windows = OutputState::with(data + OutputState::with(data .state .focus_state .focused_output @@ -263,36 +261,10 @@ impl State { } else { state.focused_tags.insert(tag_id.clone(), true); } - - // TODO: extract into fn, same with the one up there - for window in data.state.space.elements().cloned().collect::>() { - let should_render = WindowState::with_state(&window, |win_state| { - for tag_id in win_state.tags.iter() { - if *state.focused_tags.get(tag_id).unwrap_or(&false) { - return true; - } - } - false - }); - if !should_render { - data.state.space.unmap_elem(&window); - } - } - - data.state.windows.iter().filter(|&win| { - WindowState::with_state(win, |win_state| { - for tag_id in win_state.tags.iter() { - if *state.focused_tags.get(tag_id).unwrap_or(&false) { - return true; - } - } - false - }) - }).cloned().collect::>() } ); - Layout::master_stack(&mut data.state, windows, crate::layout::Direction::Left); + data.state.re_layout(); } Msg::AddTags { tags } => { data @@ -640,6 +612,37 @@ impl State { } } } + + pub fn re_layout(&mut self) { + let windows = OutputState::with(self.focus_state.focused_output.as_ref().unwrap(), |state| { + for window in self.space.elements().cloned().collect::>() { + let should_render = WindowState::with_state(&window, |win_state| { + for tag_id in win_state.tags.iter() { + if *state.focused_tags.get(tag_id).unwrap_or(&false) { + return true; + } + } + false + }); + if !should_render { + self.space.unmap_elem(&window); + } + } + + self.windows.iter().filter(|&win| { + WindowState::with_state(win, |win_state| { + for tag_id in win_state.tags.iter() { + if *state.focused_tags.get(tag_id).unwrap_or(&false) { + return true; + } + } + false + }) + }).cloned().collect::>() + }); + + Layout::master_stack(self, windows, crate::layout::Direction::Left); + } } pub struct CalloopData {