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:
@@ -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"]
|
||||
}
|
||||
}
|
||||
@@ -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":""}
|
||||
]
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user