feat: add keybind cheatsheet plugin (#64)

* feat: add keybind cheatsheet

* docs: refresh keybind cheatsheet preview

* docs: use official generated keybind thumbnail

* feat(keybind-cheatsheet): preload cached snapshots

Move parser and cache ownership into an event-driven data service so the panel opens from a prepared last-known-good snapshot. Document the plugin's inspiration and independent v5 implementation.
This commit is contained in:
cheerfulScumbag
2026-07-21 19:50:47 -04:00
committed by GitHub
parent 17f7b819d3
commit 2a1efe0468
19 changed files with 2985 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
{
"mango": {
"count": 9,
"descriptions": ["Terminal", "Close window", "Workspace 1", "Move window", "Focus left", "Lock on close"]
},
"hypr_conf": {
"count": 5,
"descriptions": ["Terminal", "Move window", "Close window", "Workspace 1"]
},
"hypr_lua": {
"count": 4,
"categories": ["Applications", "Workspaces", "Media", "Without Description"]
},
"niri": {
"count": 5,
"descriptions": ["Terminal", "Close window", "Workspace 1"]
}
}
+6
View File
@@ -0,0 +1,6 @@
[
{"locked":false,"mouse":false,"release":false,"repeat":false,"has_description":true,"modmask":64,"submap":"","key":"T","keycode":0,"description":"Terminal","dispatcher":"exec","arg":"foot"},
{"locked":false,"mouse":false,"release":false,"repeat":false,"has_description":true,"modmask":64,"submap":"","key":"1","keycode":0,"description":"Workspace 1","dispatcher":"workspace","arg":"1"},
{"locked":false,"mouse":false,"release":false,"repeat":false,"has_description":true,"modmask":0,"submap":"","key":"XF86AudioMute","keycode":0,"description":"Mute","dispatcher":"exec","arg":"wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"},
{"locked":false,"mouse":false,"release":false,"repeat":false,"has_description":false,"modmask":64,"submap":"","key":"Q","keycode":0,"description":"","dispatcher":"killactive","arg":""}
]
+9
View File
@@ -0,0 +1,9 @@
$mainMod = SUPER
source = ./parts/*.conf
# 1. Applications
bind = $mainMod, T, exec, foot #"Terminal"
bindm = $mainMod, mouse:272, movewindow #"Move window"
# 2. Window Management
bind = $mainMod SHIFT, Q, killactive, #"Close window"
+9
View File
@@ -0,0 +1,9 @@
-- 1. Applications
Hyprland.config.bind("SUPER, T", launch_terminal, { description = "Terminal" })
-- 2. Workspaces
for i = 1, 9 do
Hyprland.config.bind("SUPER, " .. i, workspace, { description = "Workspace " .. i })
end
require("parts.media")
@@ -0,0 +1,2 @@
-- 3. Media
Hyprland.config.bind(", XF86AudioMute", mute, { desc = "Mute" })
@@ -0,0 +1,3 @@
# 3. Workspaces
binde = $mainMod, 1, workspace, 1 #"Workspace 1"
bind = , XF86AudioMute, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle
+14
View File
@@ -0,0 +1,14 @@
# Applications
bind=SUPER,T,spawn,foot #"Terminal"
bind=SUPER,B,spawn,firefox #"Browser"
source=./nested.conf
source-optional=./missing.conf
source=./parts/*.conf
# Media
bind=NONE,XF86AudioRaiseVolume,spawn,wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+
axisbind=SUPER,UP,viewtoleft_have_client
mousebind=SUPER,btn_left,moveresize,curmove #"Move window"
gesturebind=NONE,left,3,focusdir,left #"Focus left"
switchbind=fold,spawn,swaylock #"Lock on close"
+3
View File
@@ -0,0 +1,3 @@
# Window Management
bind=SUPER,Q,killclient #"Close window"
source=./main.conf
@@ -0,0 +1,2 @@
# Workspaces
bind=SUPER,1,view,1 #"Workspace 1"
+12
View File
@@ -0,0 +1,12 @@
include "./parts/*.kdl"
binds {
// #"Applications"
Mod+T hotkey-overlay-title="Terminal" { spawn "foot"; }
Mod+B { spawn "firefox"; }
// #"Window Management"
Mod+Q hotkey-overlay-title="Close window" {
close-window;
}
}
@@ -0,0 +1,5 @@
binds {
// #"Workspaces"
Mod+1 hotkey-overlay-title="Workspace 1" { focus-workspace 1; }
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
}
+220
View File
@@ -0,0 +1,220 @@
--!nonstrict
-- Minimal Noctalia host used by the standalone Luau parser tests.
local files = {}
local stateValues = {}
local stateWatchers = {}
local fileExistsOverrides = {}
local configValues = {}
local availableCommands = {}
local asyncRequests = {}
local renderCount = 0
local renderedTree = nil
local readCounts = {}
local writeCounts = {}
local wantsSecondTicks = false
local encodedValues = {}
local encodedIndex = 0
local errorNotifications = 0
local updateInterval = nil
local function copy(value)
if type(value) ~= "table" then return value end
local result = {}
for key, item in pairs(value) do result[copy(key)] = copy(item) end
return result
end
local function normalize(path)
local absolute = path:sub(1, 1) == "/"
local parts = {}
for part in path:gmatch("[^/]+") do
if part == ".." then
if #parts > 0 then table.remove(parts) end
elseif part ~= "." and part ~= "" then
table.insert(parts, part)
end
end
return (absolute and "/" or "") .. table.concat(parts, "/")
end
local function directory(path)
local prefix = path:gsub("/+$", "") .. "/"
local names = {}
for file in pairs(files) do
if file:sub(1, #prefix) == prefix then
local rest = file:sub(#prefix + 1)
local name = rest:match("^([^/]+)")
if name ~= nil then names[name] = true end
end
end
local result = {}
for name in pairs(names) do table.insert(result, name) end
table.sort(result)
return result
end
local function translation(key, substitutions)
local value = key
for name, replacement in pairs(substitutions or {}) do
value = value:gsub("{" .. name .. "}", tostring(replacement))
end
return value
end
local noctalia = {
tr = translation,
getenv = function(name) return name == "HOME" and "/home/test" or nil end,
expandPath = function(path) return normalize(path:gsub("^~", "/home/test")) end,
fileExists = function(path)
path = normalize(path)
if fileExistsOverrides[path] ~= nil then return fileExistsOverrides[path] end
if files[path] ~= nil then return true end
return #directory(path) > 0
end,
fileInfo = function(path)
path = normalize(path)
if files[path] ~= nil then return { size = #files[path], mtime = 0, isDir = false } end
if #directory(path) > 0 then return { size = 0, mtime = 0, isDir = true } end
return nil, "missing"
end,
listDir = function(path)
local values = directory(normalize(path))
if #values > 0 then return values, nil end
return nil, "missing"
end,
readFile = function(path)
path = normalize(path)
readCounts[path] = (readCounts[path] or 0) + 1
local content = files[path]
if content ~= nil then return content, nil end
return nil, "missing"
end,
writeFile = function(path, content)
path = normalize(path)
files[path] = content
writeCounts[path] = (writeCounts[path] or 0) + 1
return true
end,
renameFile = function(from, to)
from = normalize(from)
to = normalize(to)
if files[from] == nil then return false, "missing" end
files[to] = files[from]
files[from] = nil
return true
end,
removeFile = function(path)
files[normalize(path)] = nil
return true
end,
pluginDir = function() return "/plugin" end,
pluginDataDir = function() return "/data" end,
commandExists = function(name) return availableCommands[name] == true end,
getConfig = function(key) return configValues[key] end,
outputs = function() return {} end,
focusedOutputName = function() return nil end,
clipboardText = function() return nil end,
openColorPicker = function() return false end,
notify = function() end,
notifyError = function() errorNotifications += 1 end,
log = function() end,
togglePanel = function() end,
setUpdateInterval = function(value) updateInterval = value end,
runAsync = function(command, callback, timeoutMs)
table.insert(asyncRequests, { command = command, callback = callback, timeoutMs = timeoutMs, completed = false })
return true
end,
string = { trim = function(value) return (value:gsub("^%s+", ""):gsub("%s+$", "")) end },
json = {
decode = function(raw)
local value = encodedValues[raw]
if value == nil then return nil, "invalid mock JSON" end
return copy(value), nil
end,
encode = function(value)
encodedIndex += 1
local token = "__mock_json_" .. tostring(encodedIndex)
encodedValues[token] = copy(value)
return token, nil
end,
},
state = {
get = function(key) return copy(stateValues[key]) end,
set = function(key, value)
stateValues[key] = copy(value)
for _, watcher in ipairs(stateWatchers[key] or {}) do watcher(copy(value)) end
end,
watch = function(key, callback)
stateWatchers[key] = stateWatchers[key] or {}
table.insert(stateWatchers[key], callback)
end,
},
}
local function node(kind)
return function(props, children) return { type = kind, props = props or {}, children = children or {} } end
end
local ui = {}
for _, kind in ipairs({ "column", "row", "box", "label", "glyph", "image", "separator", "spacer", "progress", "button", "graph", "input", "select", "slider", "toggle", "scroll" }) do
ui[kind] = node(kind)
end
return {
noctalia = noctalia,
ui = ui,
panel = {
render = function(_tree)
renderCount += 1
renderedTree = _tree
end,
close = function() end,
setWantsSecondTicks = function(value)
wantsSecondTicks = value == true
end,
},
setFiles = function(value)
files = {}
fileExistsOverrides = {}
for path, content in pairs(value) do files[normalize(path)] = content end
end,
setFileExists = function(path, value)
fileExistsOverrides[normalize(path)] = value
end,
resetRuntime = function()
stateValues = {}
configValues = {}
availableCommands = {}
asyncRequests = {}
renderCount = 0
renderedTree = nil
readCounts = {}
writeCounts = {}
wantsSecondTicks = false
errorNotifications = 0
updateInterval = nil
end,
setFile = function(path, content)
files[normalize(path)] = content
end,
setConfig = function(values) configValues = values or {} end,
setAvailableCommands = function(values) availableCommands = values or {} end,
asyncLaunchCount = function() return #asyncRequests end,
completeAsync = function(index, result)
local request = asyncRequests[index]
if request == nil or request.completed then return false end
request.completed = true
if request.callback ~= nil then request.callback(result) end
return true
end,
renderCount = function() return renderCount end,
renderedTree = function() return renderedTree end,
readCount = function(path) return readCounts[normalize(path)] or 0 end,
writeCount = function(path) return writeCounts[normalize(path)] or 0 end,
fileContent = function(path) return files[normalize(path)] end,
wantsSecondTicks = function() return wantsSecondTicks end,
errorNotificationCount = function() return errorNotifications end,
stateValue = function(key) return copy(stateValues[key]) end,
updateInterval = function() return updateInterval end,
}