mirror of
https://github.com/pinnacle-comp/pinnacle.git
synced 2024-12-25 09:59:21 +01:00
Merge pull request #256 from pinnacle-comp/snowcap-lua
Some checks failed
CI (Pinnacle) / Build (push) Waiting to run
CI (Pinnacle) / Run tests (push) Waiting to run
CI (Pinnacle) / Check formatting (push) Waiting to run
CI (Pinnacle) / Clippy check (push) Waiting to run
Build Lua Docs / Build Lua docs (push) Has been cancelled
Build Rust Docs / Build docs (push) Has been cancelled
Some checks failed
CI (Pinnacle) / Build (push) Waiting to run
CI (Pinnacle) / Run tests (push) Waiting to run
CI (Pinnacle) / Check formatting (push) Waiting to run
CI (Pinnacle) / Clippy check (push) Waiting to run
Build Lua Docs / Build Lua docs (push) Has been cancelled
Build Rust Docs / Build docs (push) Has been cancelled
Preliminary Snowcap integration (Lua)
This commit is contained in:
commit
aada66ce70
15 changed files with 812 additions and 36 deletions
26
README.md
26
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.</b>
|
|||
> 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 |
|
||||
|----------------------------------------------|------------------------------------|
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
10
api/lua/examples/default_no_snowcap/.luarc.json
Normal file
10
api/lua/examples/default_no_snowcap/.luarc.json
Normal file
|
@ -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",
|
||||
}
|
314
api/lua/examples/default_no_snowcap/default_config.lua
Normal file
314
api/lua/examples/default_no_snowcap/default_config.lua
Normal file
|
@ -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)
|
46
api/lua/examples/default_no_snowcap/metaconfig.toml
Normal file
46
api/lua/examples/default_no_snowcap/metaconfig.toml
Normal file
|
@ -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"
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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,13 @@ end
|
|||
---@see Pinnacle.run
|
||||
function pinnacle.setup(config_fn)
|
||||
require("pinnacle.grpc.protobuf").build_protos()
|
||||
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
|
||||
|
||||
-- This function ensures a config won't run forever if Pinnacle is killed
|
||||
-- and doesn't kill configs on drop.
|
||||
|
@ -90,6 +99,10 @@ end
|
|||
---@param run_fn fun(pinnacle: Pinnacle)
|
||||
function pinnacle.run(run_fn)
|
||||
require("pinnacle.grpc.protobuf").build_protos()
|
||||
local success, snowcap = pcall(require, "snowcap")
|
||||
if success then
|
||||
snowcap.init()
|
||||
end
|
||||
|
||||
run_fn(pinnacle)
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
349
api/lua/pinnacle/snowcap.lua
Normal file
349
api/lua/pinnacle/snowcap.lua
Normal file
|
@ -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
|
|
@ -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::<Vec<_>>()
|
||||
.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}")
|
||||
}
|
||||
}
|
||||
|
|
16
justfile
16
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
|
||||
|
|
2
snowcap
2
snowcap
|
@ -1 +1 @@
|
|||
Subproject commit 3e79a16e1065f07001b08fabdc763bb274cac394
|
||||
Subproject commit e901659a86ec41c6903f8c913da711e1b8d95a56
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue