From 9df86c8e146e7701fc2a6b87477e2dbb24ed6d36 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Mon, 17 Jun 2024 16:27:02 -0500 Subject: [PATCH 1/5] snowcap: Add Lua API --- api/lua/.luarc.json | 2 +- api/lua/examples/default/.luarc.json | 7 +- api/lua/examples/default/default_config.lua | 10 +- api/lua/pinnacle-api-dev-1.rockspec | 1 + api/lua/pinnacle.lua | 7 + api/lua/pinnacle/input.lua | 15 +- api/lua/pinnacle/snowcap.lua | 349 ++++++++++++++++++++ api/rust/src/snowcap/integration.rs | 33 +- justfile | 16 +- snowcap | 2 +- 10 files changed, 419 insertions(+), 23 deletions(-) create mode 100644 api/lua/pinnacle/snowcap.lua diff --git a/api/lua/.luarc.json b/api/lua/.luarc.json index 83c59a1..5e70c52 100644 --- a/api/lua/.luarc.json +++ b/api/lua/.luarc.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json", - "workspace.library": ["./"], + "workspace.library": ["./", "../../snowcap/api/lua"], "runtime.version": "Lua 5.2", "--comment": "Format using Stylua instead", diff --git a/api/lua/examples/default/.luarc.json b/api/lua/examples/default/.luarc.json index bbcbb2c..a1dde71 100644 --- a/api/lua/examples/default/.luarc.json +++ b/api/lua/examples/default/.luarc.json @@ -1,5 +1,10 @@ { "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json", - "workspace.library": ["~/.luarocks/share/lua/5.4/pinnacle.lua", "~/.luarocks/share/lua/5.4/pinnacle"], + "workspace.library": [ + "~/.luarocks/share/lua/5.4/pinnacle.lua", + "~/.luarocks/share/lua/5.4/pinnacle", + "~/.luarocks/share/lua/5.4/snowcap.lua", + "~/.luarocks/share/lua/5.4/snowcap" + ], "runtime.version": "Lua 5.4", } diff --git a/api/lua/examples/default/default_config.lua b/api/lua/examples/default/default_config.lua index 07f6702..e5b3530 100644 --- a/api/lua/examples/default/default_config.lua +++ b/api/lua/examples/default/default_config.lua @@ -7,6 +7,7 @@ require("pinnacle").setup(function(Pinnacle) local Window = Pinnacle.window local Layout = Pinnacle.layout local Util = Pinnacle.util + local Snowcap = Pinnacle.snowcap local key = Input.key @@ -15,6 +16,13 @@ require("pinnacle").setup(function(Pinnacle) local terminal = "alacritty" + Input.keybind({ mod_key }, "s", function() + Snowcap.integration.keybind_overlay():show() + end, { + group = "Compositor", + description = "Show the keybind overlay", + }) + -------------------- -- Mousebinds -- -------------------- @@ -33,7 +41,7 @@ require("pinnacle").setup(function(Pinnacle) -- mod_key + alt + q = Quit Pinnacle Input.keybind({ mod_key, "alt" }, "q", function() - Pinnacle.quit() + Snowcap.integration.quit_prompt():show() end, { group = "Compositor", description = "Quit Pinnacle", diff --git a/api/lua/pinnacle-api-dev-1.rockspec b/api/lua/pinnacle-api-dev-1.rockspec index f582eda..387fce8 100644 --- a/api/lua/pinnacle-api-dev-1.rockspec +++ b/api/lua/pinnacle-api-dev-1.rockspec @@ -31,5 +31,6 @@ build = { ["pinnacle.signal"] = "pinnacle/signal.lua", ["pinnacle.layout"] = "pinnacle/layout.lua", ["pinnacle.render"] = "pinnacle/render.lua", + ["pinnacle.snowcap"] = "pinnacle/snowcap.lua", }, } diff --git a/api/lua/pinnacle.lua b/api/lua/pinnacle.lua index 4fdee73..00ca53f 100644 --- a/api/lua/pinnacle.lua +++ b/api/lua/pinnacle.lua @@ -26,6 +26,8 @@ local pinnacle = { layout = require("pinnacle.layout"), ---@type Render render = require("pinnacle.render"), + ---@type pinnacle.Snowcap + snowcap = require("pinnacle.snowcap"), } ---Quit Pinnacle. @@ -52,6 +54,10 @@ end ---@see Pinnacle.run function pinnacle.setup(config_fn) require("pinnacle.grpc.protobuf").build_protos() + require("snowcap").init() + + -- Make Snowcap use Pinnacle's cqueues loop + require("snowcap.grpc.client").loop = client.loop -- This function ensures a config won't run forever if Pinnacle is killed -- and doesn't kill configs on drop. @@ -90,6 +96,7 @@ end ---@param run_fn fun(pinnacle: Pinnacle) function pinnacle.run(run_fn) require("pinnacle.grpc.protobuf").build_protos() + require("snowcap").init() run_fn(pinnacle) end diff --git a/api/lua/pinnacle/input.lua b/api/lua/pinnacle/input.lua index e9e86d0..1f178a5 100644 --- a/api/lua/pinnacle/input.lua +++ b/api/lua/pinnacle/input.lua @@ -190,7 +190,20 @@ function input.keybind_descriptions() local ret = {} for _, desc in ipairs(descs) do - desc.modifiers = desc.modifiers or {} + local mods = {} + for _, mod in ipairs(desc.modifiers or {}) do + if mod == modifier_values.shift then + table.insert(mods, "shift") + elseif mod == modifier_values.ctrl then + table.insert(mods, "ctrl") + elseif mod == modifier_values.alt then + table.insert(mods, "alt") + elseif mod == modifier_values.super then + table.insert(mods, "super") + end + end + + desc.modifiers = mods table.insert(ret, desc) end diff --git a/api/lua/pinnacle/snowcap.lua b/api/lua/pinnacle/snowcap.lua new file mode 100644 index 0000000..9b62e53 --- /dev/null +++ b/api/lua/pinnacle/snowcap.lua @@ -0,0 +1,349 @@ +local integration = {} + +---The Snowcap widget system, integrated into Pinnacle. +---@class pinnacle.Snowcap +local snowcap = { + layer = require("snowcap.layer"), + widget = require("snowcap.widget"), + input = { + key = require("snowcap.input.keys"), + }, + integration = integration, +} + +---@class pinnacle.snowcap.integration.QuitPrompt +---@field border_radius number +---@field border_thickness number +---@field background_color snowcap.Color +---@field border_color snowcap.Color +---@field font snowcap.Font +---@field width integer +---@field height integer +local QuitPrompt = {} + +---@class pinnacle.snowcap.integration.KeybindOverlay +---@field border_radius number +---@field border_thickness number +---@field background_color snowcap.Color +---@field border_color snowcap.Color +---@field font snowcap.Font +---@field width integer +---@field height integer +local KeybindOverlay = {} + +---Show this quit prompt. +function QuitPrompt:show() + local Widget = require("snowcap.widget") + local Layer = require("snowcap.layer") + + local quit_font = require("pinnacle.util").deep_copy(self.font) + quit_font.weight = Widget.font.weight.BOLD + + local prompt = Widget.container({ + width = Widget.length.Fill, + height = Widget.length.Fill, + valign = Widget.alignment.CENTER, + halign = Widget.alignment.CENTER, + border_radius = self.border_radius, + border_thickness = self.border_thickness, + border_color = self.border_color, + background_color = self.background_color, + child = Widget.column({ + children = { + Widget.text({ + text = "Quit Pinnacle?", + font = quit_font, + size = 20.0, + }), + Widget.text({ text = "", size = 8.0 }), + Widget.text({ + text = "Press ENTER to confirm, or\nany other key to close this", + font = self.font, + size = 14.0, + }), + }, + }), + }) + + local prompt = Layer.new_widget({ + widget = prompt, + width = self.width, + height = self.height, + anchor = nil, + keyboard_interactivity = Layer.keyboard_interactivity.EXCLUSIVE, + exclusive_zone = "respect", + layer = Layer.zlayer.OVERLAY, + }) + + prompt:on_key_press(function(_, key) + if key == require("snowcap.input.keys").Return then + require("pinnacle").quit() + else + prompt:close() + end + end) +end + +---Show this keybind overlay. +function KeybindOverlay:show() + local descriptions = require("pinnacle.input").keybind_descriptions() + + ---@param mods Modifier[] + ---@param xkb_name string + ---@return string + local function keybind_to_string(mods, xkb_name) + local repr = {} + for _, mod in ipairs(mods) do + if mod == "super" then + table.insert(repr, "Super") + break + end + end + for _, mod in ipairs(mods) do + if mod == "ctrl" then + table.insert(repr, "Ctrl") + break + end + end + for _, mod in ipairs(mods) do + if mod == "alt" then + table.insert(repr, "Alt") + break + end + end + for _, mod in ipairs(mods) do + if mod == "shift" then + table.insert(repr, "Shift") + break + end + end + table.insert(repr, xkb_name) + + return table.concat(repr, " + ") + end + + ---@type { group: string?, data: { keybind: string, descs: string[] }[] }[] + local groups = {} + + for _, desc in ipairs(descriptions) do + local repr = keybind_to_string(desc.modifiers, desc.xkb_name) + + for _, group in ipairs(groups) do + if group.group == desc.group then + for _, keybind in ipairs(group.data) do + if keybind.keybind == repr then + if desc.description then + table.insert(keybind.descs, desc.description) + end + goto continue + end + end + + table.insert(group.data, { keybind = repr, descs = { desc.description } }) + goto continue + end + end + + table.insert( + groups, + { group = desc.group, data = { { keybind = repr, descs = { desc.description } } } } + ) + + ::continue:: + end + + -- List keybinds without a group last + + local pos = nil + for i, group in ipairs(groups) do + if not group.group then + pos = i + break + end + end + + if pos then + local other = table.remove(groups, pos) + table.insert(groups, other) + end + + -- + + ---@type snowcap.WidgetDef[] + local sections = {} + + local Widget = require("snowcap.widget") + + local bold_font = require("pinnacle.util").deep_copy(self.font) + bold_font.weight = Widget.font.weight.BOLD + + for _, group in ipairs(groups) do + local group_name = group.group or "Other" + + table.insert(sections, Widget.text({ text = group_name, font = bold_font, size = 19.0 })) + + for _, keybind in ipairs(group.data) do + local repr = keybind.keybind + local descs = keybind.descs + + if #descs == 0 then + table.insert(sections, Widget.text({ text = repr, font = self.font })) + elseif #descs == 1 then + table.insert( + sections, + Widget.row({ + children = { + Widget.text({ + text = repr, + width = Widget.length.FillPortion(1), + font = self.font, + }), + Widget.text({ + text = descs[1], + width = Widget.length.FillPortion(2), + font = self.font, + }), + }, + }) + ) + else + local children = {} + + table.insert( + children, + Widget.text({ + text = repr .. ":", + font = self.font, + }) + ) + + for _, desc in descs do + table.insert( + children, + Widget.text({ + text = "\t" .. desc, + font = self.font, + }) + ) + end + + table.insert( + sections, + Widget.column({ + children = children, + }) + ) + end + end + + table.insert(sections, Widget.text({ text = "", size = 8.0 })) + end + + local scrollable = Widget.scrollable({ + child = Widget.column({ + children = sections, + }), + width = Widget.length.Fill, + height = Widget.length.Fill, + }) + + local overlay = Widget.container({ + child = Widget.column({ + children = { + Widget.text({ + text = "Keybinds", + font = bold_font, + size = 24.0, + width = Widget.length.Fill, + }), + Widget.text({ + text = "", + size = 8.0, + }), + scrollable, + }, + }), + width = Widget.length.Fill, + height = Widget.length.Fill, + padding = { + top = 16.0, + left = 16.0, + bottom = 16.0, + right = 16.0, + }, + valign = Widget.alignment.CENTER, + halign = Widget.alignment.CENTER, + border_radius = self.border_radius, + border_color = self.border_color, + border_thickness = self.border_thickness, + background_color = self.background_color, + }) + + local Layer = require("snowcap.layer") + + local overlay = Layer.new_widget({ + widget = overlay, + width = self.width, + height = self.height, + anchor = nil, + keyboard_interactivity = Layer.keyboard_interactivity.EXCLUSIVE, + exclusive_zone = "respect", + layer = Layer.zlayer.OVERLAY, + }) + + overlay:on_key_press(function(_, _) + overlay:close() + end) +end + +---Creates the default quit prompt. +--- +---Some of its characteristics can be changed by altering its fields. +---@return pinnacle.snowcap.integration.QuitPrompt +function integration.quit_prompt() + local Widget = require("snowcap.widget") + + ---@type pinnacle.snowcap.integration.QuitPrompt + local prompt = { + border_radius = 12.0, + border_thickness = 6.0, + background_color = Widget.color.from_rgba(0.15, 0.03, 0.1, 0.65), + border_color = Widget.color.from_rgba(0.8, 0.2, 0.4), + font = { + family = Widget.font.family.Name("Ubuntu"), + }, + width = 220, + height = 120, + } + + setmetatable(prompt, { __index = QuitPrompt }) + + return prompt +end + +---Creates the default keybind overlay. +--- +---Some of its characteristics can be changed by altering its fields. +---@return pinnacle.snowcap.integration.KeybindOverlay +function integration.keybind_overlay() + local Widget = require("snowcap.widget") + + ---@type pinnacle.snowcap.integration.KeybindOverlay + local prompt = { + border_radius = 12.0, + border_thickness = 6.0, + background_color = Widget.color.from_rgba(0.15, 0.15, 0.225, 0.8), + border_color = Widget.color.from_rgba(0.4, 0.4, 0.7), + font = { + family = Widget.font.family.Name("Ubuntu"), + }, + width = 700, + height = 500, + } + + setmetatable(prompt, { __index = KeybindOverlay }) + + return prompt +end + +return snowcap diff --git a/api/rust/src/snowcap/integration.rs b/api/rust/src/snowcap/integration.rs index 5285076..2ac3cda 100644 --- a/api/rust/src/snowcap/integration.rs +++ b/api/rust/src/snowcap/integration.rs @@ -166,22 +166,23 @@ impl KeybindOverlay { impl std::fmt::Display for KeybindRepr { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let bind = self - .mods - .iter() - .map(|m| { - // TODO: strum name or something - match m { - Mod::Shift => "Shift", - Mod::Ctrl => "Ctrl", - Mod::Alt => "Alt", - Mod::Super => "Super", - } - .to_string() - }) - .chain([self.name.clone()]) - .collect::>() - .join(" + "); + let mut parts = Vec::new(); + if self.mods.contains(&Mod::Super) { + parts.push("Super"); + } + if self.mods.contains(&Mod::Ctrl) { + parts.push("Ctrl"); + } + if self.mods.contains(&Mod::Alt) { + parts.push("Alt"); + } + if self.mods.contains(&Mod::Shift) { + parts.push("Shift"); + } + + parts.push(self.name.as_str()); + + let bind = parts.join(" + "); write!(f, "{bind}") } } diff --git a/justfile b/justfile index 5aa093b..8cd4c6e 100644 --- a/justfile +++ b/justfile @@ -11,7 +11,7 @@ list: @just --list --unsorted # Install the configs, protobuf definitions, and the Lua library (requires Luarocks) -install: install-configs install-protos install-lua-lib +install: install-configs install-protos install-lua-lib install-snowcap # Install the default Lua and Rust configs install-configs: @@ -41,7 +41,7 @@ install-lua-lib: luarocks make --local --lua-version "{{lua_version}}" # Remove installed configs and the Lua API (requires Luarocks) -clean: +clean: clean-snowcap rm -rf "{{xdg_data_dir}}" -luarocks remove --local pinnacle-api @@ -119,3 +119,15 @@ wlcs *args: compile-wlcs set -euxo pipefail cargo build -p wlcs_pinnacle RUST_BACKTRACE=1 ./wlcs/wlcs target/debug/libwlcs_pinnacle.so {{args}} + +install-snowcap: + #!/usr/bin/env bash + set -euxo pipefail + cd "{{rootdir}}/snowcap" + just install + +clean-snowcap: + #!/usr/bin/env bash + set -euxo pipefail + cd "{{rootdir}}/snowcap" + just clean diff --git a/snowcap b/snowcap index 3e79a16..f82a31b 160000 --- a/snowcap +++ b/snowcap @@ -1 +1 @@ -Subproject commit 3e79a16e1065f07001b08fabdc763bb274cac394 +Subproject commit f82a31b8790bb67cfe4c5806aac8593469043e66 From 50a6e4bf78414637d17a572569f480b63d57cd28 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Mon, 17 Jun 2024 18:08:27 -0500 Subject: [PATCH 2/5] snowcap/lua: Don't immediately crash if conn fails Hacky way to get tests to work --- api/lua/pinnacle.lua | 10 ++++++++-- snowcap | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/api/lua/pinnacle.lua b/api/lua/pinnacle.lua index 00ca53f..d053d70 100644 --- a/api/lua/pinnacle.lua +++ b/api/lua/pinnacle.lua @@ -54,7 +54,10 @@ end ---@see Pinnacle.run function pinnacle.setup(config_fn) require("pinnacle.grpc.protobuf").build_protos() - require("snowcap").init() + local success, snowcap = pcall(require, "snowcap") + if success then + snowcap.init() + end -- Make Snowcap use Pinnacle's cqueues loop require("snowcap.grpc.client").loop = client.loop @@ -96,7 +99,10 @@ end ---@param run_fn fun(pinnacle: Pinnacle) function pinnacle.run(run_fn) require("pinnacle.grpc.protobuf").build_protos() - require("snowcap").init() + local success, snowcap = pcall(require, "snowcap") + if success then + snowcap.init() + end run_fn(pinnacle) end diff --git a/snowcap b/snowcap index f82a31b..ab9207e 160000 --- a/snowcap +++ b/snowcap @@ -1 +1 @@ -Subproject commit f82a31b8790bb67cfe4c5806aac8593469043e66 +Subproject commit ab9207e657d0bd15ca922992eedced4021b3e7ee From 3690db083d73cfb6d367be87353506fef771e4d3 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Mon, 17 Jun 2024 18:21:21 -0500 Subject: [PATCH 3/5] Update Snowcap --- snowcap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snowcap b/snowcap index ab9207e..e901659 160000 --- a/snowcap +++ b/snowcap @@ -1 +1 @@ -Subproject commit ab9207e657d0bd15ca922992eedced4021b3e7ee +Subproject commit e901659a86ec41c6903f8c913da711e1b8d95a56 From 853a2a23f741a0a11a4d67cb57f2048a7f020985 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Mon, 17 Jun 2024 18:32:47 -0500 Subject: [PATCH 4/5] Decrease sctk log level --- src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 26ba9a6..2a3e04f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -46,7 +46,7 @@ async fn main() -> anyhow::Result<()> { let env_filter = EnvFilter::try_from_default_env(); let file_log_env_filter = - EnvFilter::new("debug,h2=warn,hyper=warn,smithay::xwayland::xwm=warn,wgpu_hal=warn,naga=warn,wgpu_core=warn,cosmic_text=warn,iced_wgpu=warn,sctk=warn"); + EnvFilter::new("debug,h2=warn,hyper=warn,smithay::xwayland::xwm=warn,wgpu_hal=warn,naga=warn,wgpu_core=warn,cosmic_text=warn,iced_wgpu=warn,sctk=error"); let file_log_layer = tracing_subscriber::fmt::layer() .compact() @@ -55,7 +55,7 @@ async fn main() -> anyhow::Result<()> { .with_filter(file_log_env_filter); let stdout_env_filter = - env_filter.unwrap_or_else(|_| EnvFilter::new("warn,pinnacle=info,snowcap=info")); + env_filter.unwrap_or_else(|_| EnvFilter::new("warn,pinnacle=info,snowcap=info,sctk=error")); let stdout_layer = tracing_subscriber::fmt::layer() .compact() .with_writer(std::io::stdout) From 2c52839d3e209390a68ed7cb22db7b069fada230 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Mon, 17 Jun 2024 18:45:36 -0500 Subject: [PATCH 5/5] Update README --- README.md | 26 +- .../examples/default_no_snowcap/.luarc.json | 10 + .../default_no_snowcap/default_config.lua | 314 ++++++++++++++++++ .../default_no_snowcap/metaconfig.toml | 46 +++ 4 files changed, 385 insertions(+), 11 deletions(-) create mode 100644 api/lua/examples/default_no_snowcap/.luarc.json create mode 100644 api/lua/examples/default_no_snowcap/default_config.lua create mode 100644 api/lua/examples/default_no_snowcap/metaconfig.toml diff --git a/README.md b/README.md index 3088c3c..0fad382 100644 --- a/README.md +++ b/README.md @@ -35,14 +35,10 @@ Pinnacle is a Wayland compositor built in Rust using [Smithay](https://github.co It's my attempt at creating something like [AwesomeWM](https://github.com/awesomeWM/awesome) for Wayland. -### What is Snowcap? -You will see references to Snowcap throughout this README. [Snowcap](https://github.com/pinnacle-comp/snowcap) is the -very, *very* WIP widget system for Pinnacle. Currently it's only being used for the builtin quit prompt and keybind overlay. +Pinnacle comes integrated with [Snowcap](https://github.com/pinnacle-comp/snowcap), a +very, *very* WIP widget system. Currently it's only being used for the builtin quit prompt and keybind overlay. In the future, Snowcap will be used for everything Awesome uses its widget system for: a taskbar, system tray, etc. -> [!NOTE] -> Only the Rust API has implemented Snowcap integration currently. Lua support soon™️ - ### Features - Tag system - Customizable layouts, including most of the ones from Awesome @@ -170,16 +166,20 @@ the Lua or Rust default configs standalone, run one of the following in the crat ```sh # For a Lua configuration -just install run -- -c "./api/lua/examples/default" +just install run -- -c ./api/lua/examples/default # For a Rust configuration -cargo run -- -c "./api/rust/examples/default_config" +cargo run -- -c ./api/rust/examples/default_config ``` -When running the default Rust config standalone without compiled Snowcap integration, -run the one in the following directory: +When running without compiled Snowcap integration, +use the following directories instead: ```sh -cargo run -- -c "./api/rust/examples/default_config_no_snowcap" +# Lua +just install run -- -c ./api/lua/examples/default_no_snowcap + +# Rust +cargo run -- -c ./api/rust/examples/default_config_no_snowcap ``` ## Custom configuration @@ -200,6 +200,8 @@ It will then generate a config at that directory. If Lua is chosen and there are files in the directory, the generator will prompt to rename them to a backup before continuing. If Rust is chosen, the directory must be manually emptied to continue. +Note that this currently copies default configs *with* Snowcap integration. + Run `cargo run -- config gen --help` for information on the command. ## More on configuration @@ -252,6 +254,8 @@ Rust: https://pinnacle-comp.github.io/rust-reference/main. > Documentation for other branches can be reached by replacing `main` with the branch you want. # Controls +> Yes, ctrl is a bad mod key I know, this will be changed to Awesome keybinds soon + The following are the default controls in the [`default_config`](api/rust/examples/default_config/main.rs). | Binding | Action | |----------------------------------------------|------------------------------------| diff --git a/api/lua/examples/default_no_snowcap/.luarc.json b/api/lua/examples/default_no_snowcap/.luarc.json new file mode 100644 index 0000000..a1dde71 --- /dev/null +++ b/api/lua/examples/default_no_snowcap/.luarc.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json", + "workspace.library": [ + "~/.luarocks/share/lua/5.4/pinnacle.lua", + "~/.luarocks/share/lua/5.4/pinnacle", + "~/.luarocks/share/lua/5.4/snowcap.lua", + "~/.luarocks/share/lua/5.4/snowcap" + ], + "runtime.version": "Lua 5.4", +} diff --git a/api/lua/examples/default_no_snowcap/default_config.lua b/api/lua/examples/default_no_snowcap/default_config.lua new file mode 100644 index 0000000..07f6702 --- /dev/null +++ b/api/lua/examples/default_no_snowcap/default_config.lua @@ -0,0 +1,314 @@ +-- neovim users be like +require("pinnacle").setup(function(Pinnacle) + local Input = Pinnacle.input + local Process = Pinnacle.process + local Output = Pinnacle.output + local Tag = Pinnacle.tag + local Window = Pinnacle.window + local Layout = Pinnacle.layout + local Util = Pinnacle.util + + local key = Input.key + + ---@type Modifier + local mod_key = "ctrl" + + local terminal = "alacritty" + + -------------------- + -- Mousebinds -- + -------------------- + + Input.mousebind({ mod_key }, "btn_left", "press", function() + Window.begin_move("btn_left") + end) + + Input.mousebind({ mod_key }, "btn_right", "press", function() + Window.begin_resize("btn_right") + end) + + -------------------- + -- Keybinds -- + -------------------- + + -- mod_key + alt + q = Quit Pinnacle + Input.keybind({ mod_key, "alt" }, "q", function() + Pinnacle.quit() + end, { + group = "Compositor", + description = "Quit Pinnacle", + }) + + -- mod_key + alt + r = Reload config + Input.keybind({ mod_key, "alt" }, "r", function() + Pinnacle.reload_config() + end, { + group = "Compositor", + description = "Reload the config", + }) + + -- mod_key + alt + c = Close window + Input.keybind({ mod_key, "alt" }, "c", function() + local focused = Window.get_focused() + if focused then + focused:close() + end + end, { + group = "Window", + description = "Close the focused window", + }) + + -- mod_key + alt + Return = Spawn `terminal` + Input.keybind({ mod_key }, key.Return, function() + Process.spawn(terminal) + end, { + group = "Process", + description = "Spawn `alacritty`", + }) + + -- mod_key + alt + space = Toggle floating + Input.keybind({ mod_key, "alt" }, key.space, function() + local focused = Window.get_focused() + if focused then + focused:toggle_floating() + focused:raise() + end + end, { + group = "Window", + description = "Toggle floating on the focused window", + }) + + -- mod_key + f = Toggle fullscreen + Input.keybind({ mod_key }, "f", function() + local focused = Window.get_focused() + if focused then + focused:toggle_fullscreen() + focused:raise() + end + end, { + group = "Window", + description = "Toggle fullscreen on the focused window", + }) + + -- mod_key + m = Toggle maximized + Input.keybind({ mod_key }, "m", function() + local focused = Window.get_focused() + if focused then + focused:toggle_maximized() + focused:raise() + end + end, { + group = "Window", + description = "Toggle maximized on the focused window", + }) + + ---------------------- + -- Tags and Outputs -- + ---------------------- + + local tag_names = { "1", "2", "3", "4", "5" } + + -- Setup outputs. + -- + -- `Output.setup` allows you to declare things like mode, scale, and tags for outputs. + -- Here we give all outputs tags 1 through 5. + Output.setup({ + -- "*" matches all outputs + ["*"] = { tags = tag_names }, + }) + + -- If you want to declare output locations as well, you can use `Output.setup_locs`. + -- This will additionally allow you to recalculate output locations on signals like + -- output connect, disconnect, and resize. + -- + -- Read the admittedly scuffed docs for more. + + -- Tag keybinds + for _, tag_name in ipairs(tag_names) do + -- nil-safety: tags are guaranteed to be on the outputs due to connect_for_all above + + -- mod_key + 1-5 = Switch to tags 1-5 + Input.keybind({ mod_key }, tag_name, function() + Tag.get(tag_name):switch_to() + end, { + group = "Tag", + description = "Switch to tag " .. tag_name, + }) + + -- mod_key + shift + 1-5 = Toggle tags 1-5 + Input.keybind({ mod_key, "shift" }, tag_name, function() + Tag.get(tag_name):toggle_active() + end, { + group = "Tag", + description = "Toggle tag " .. tag_name, + }) + + -- mod_key + alt + 1-5 = Move window to tags 1-5 + Input.keybind({ mod_key, "alt" }, tag_name, function() + local focused = Window.get_focused() + if focused then + focused:move_to_tag(Tag.get(tag_name) --[[@as TagHandle]]) + end + end, { + group = "Tag", + description = "Move the focused window to tag " .. tag_name, + }) + + -- mod_key + shift + alt + 1-5 = Toggle tags 1-5 on window + Input.keybind({ mod_key, "shift", "alt" }, tag_name, function() + local focused = Window.get_focused() + if focused then + focused:toggle_tag(Tag.get(tag_name) --[[@as TagHandle]]) + end + end, { + group = "Tag", + description = "Toggle tag " .. tag_name .. " on the focused window", + }) + end + + -------------------- + -- Layouts -- + -------------------- + + -- Pinnacle does not manage layouts compositor-side. + -- Instead, it delegates computation of layouts to your config, + -- which provides an interface to calculate the size and location of + -- windows that the compositor will use to position windows. + -- + -- If you're familiar with River's layout generators, you'll understand the system here + -- a bit better. + -- + -- The Lua API provides two layout system abstractions: + -- 1. Layout managers, and + -- 2. Layout generators. + -- + -- ### Layout Managers ### + -- A layout manager is a table that contains a `get_active` function + -- that returns some layout generator. + -- A manager is meant to keep track of and choose various layout generators + -- across your usage of the compositor. + -- + -- ### Layout generators ### + -- A layout generator is a table that holds some state as well as + -- the `layout` function, which takes in layout arguments and computes + -- an array of geometries that will determine the size and position + -- of windows being laid out. + -- + -- There is one built-in layout manager and five built-in layout generators, + -- as shown below. + -- + -- Additionally, this system is designed to be user-extensible; + -- you are free to create your own layout managers and generators for + -- maximum customizability! Docs for doing so are in the works, so sit tight. + + -- Create a cycling layout manager. This provides methods to cycle + -- between the given layout generators below. + local layout_manager = Layout.new_cycling_manager({ + -- `Layout.builtins` contains functions that create various layout generators. + -- Each of these has settings that can be overridden by passing in a table with + -- overriding options. + Layout.builtins.master_stack(), + Layout.builtins.master_stack({ master_side = "right" }), + Layout.builtins.master_stack({ master_side = "top" }), + Layout.builtins.master_stack({ master_side = "bottom" }), + Layout.builtins.dwindle(), + Layout.builtins.spiral(), + Layout.builtins.corner(), + Layout.builtins.corner({ corner_loc = "top_right" }), + Layout.builtins.corner({ corner_loc = "bottom_left" }), + Layout.builtins.corner({ corner_loc = "bottom_right" }), + Layout.builtins.fair(), + Layout.builtins.fair({ direction = "horizontal" }), + }) + + -- Set the cycling layout manager as the layout manager that will be used. + -- This then allows you to call `Layout.request_layout` to manually layout windows. + Layout.set_manager(layout_manager) + + -- mod_key + space = Cycle forward one layout on the focused output + -- + -- Yes, this is a bit verbose for my liking. + -- You need to cycle the layout on the first active tag + -- because that is the one that decides which layout is used. + Input.keybind({ mod_key }, key.space, function() + local focused_op = Output.get_focused() + if focused_op then + local tags = focused_op:tags() or {} + local tag = nil + + ---@type (fun(): (boolean|nil))[] + local tag_actives = {} + for i, t in ipairs(tags) do + tag_actives[i] = function() + return t:active() + end + end + + -- We are batching API calls here for better performance + tag_actives = Util.batch(tag_actives) + + for i, active in ipairs(tag_actives) do + if active then + tag = tags[i] + break + end + end + + if tag then + layout_manager:cycle_layout_forward(tag) + Layout.request_layout(focused_op) + end + end + end, { + group = "Layout", + description = "Cycle the layout forward on the first active tag", + }) + + -- mod_key + shift + space = Cycle backward one layout on the focused output + Input.keybind({ mod_key, "shift" }, key.space, function() + local focused_op = Output.get_focused() + if focused_op then + local tags = focused_op:tags() or {} + local tag = nil + + ---@type (fun(): (boolean|nil))[] + local tag_actives = {} + for i, t in ipairs(tags) do + tag_actives[i] = function() + return t:active() + end + end + + tag_actives = Util.batch(tag_actives) + + for i, active in ipairs(tag_actives) do + if active then + tag = tags[i] + break + end + end + + if tag then + layout_manager:cycle_layout_backward(tag) + Layout.request_layout(focused_op) + end + end + end, { + group = "Layout", + description = "Cycle the layout backward on the first active tag", + }) + + Input.set_libinput_settings({ + tap = true, + }) + + -- Enable sloppy focus + Window.connect_signal({ + pointer_enter = function(window) + window:set_focused(true) + end, + }) + + -- Spawning should happen after you add tags, as Pinnacle currently doesn't render windows without tags. + Process.spawn_once(terminal) +end) diff --git a/api/lua/examples/default_no_snowcap/metaconfig.toml b/api/lua/examples/default_no_snowcap/metaconfig.toml new file mode 100644 index 0000000..68239ce --- /dev/null +++ b/api/lua/examples/default_no_snowcap/metaconfig.toml @@ -0,0 +1,46 @@ +# 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`. +# To use a Rust config, this should be changed to something like ["cargo", "run"]. +# +# Because configuration is done using an external process, if it ever crashes, you lose all of your keybinds. +# The compositor will load the default config if that happens, but in the event that you don't have +# the necessary dependencies for it to run, you may get softlocked. +# 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 = ["lua", "default_config.lua"] + +### 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 ### +# If you need to spawn your config with any environment variables, list them here. +[envs] +# key = "value"