From 4fa8c962388bd4a27acfdeeef5647d981c11fd8a Mon Sep 17 00:00:00 2001 From: cheerfulScumbag <164391367+cheerfulScumbag@users.noreply.github.com> Date: Sat, 15 Aug 2026 15:51:41 +0100 Subject: [PATCH] feat(keybind-cheatsheet): update to v0.2.2 data service architecture and bug fixes --- keybind-cheatsheet/panel.luau | 79 +++++++++++------- keybind-cheatsheet/plugin.toml | 2 +- keybind-cheatsheet/service.luau | 70 ++++++++++++++-- keybind-cheatsheet/translations/en.json | 106 ++++++++++++------------ 4 files changed, 167 insertions(+), 90 deletions(-) diff --git a/keybind-cheatsheet/panel.luau b/keybind-cheatsheet/panel.luau index 6b621903..6a9440a6 100644 --- a/keybind-cheatsheet/panel.luau +++ b/keybind-cheatsheet/panel.luau @@ -39,6 +39,10 @@ local refreshing = false local render local refresh +local env = getfenv() +local dynamicCallbacks = {} +local callbackIndex = 0 + local function tr(key, values) return noctalia.tr(key, values) end @@ -56,6 +60,22 @@ local function startsWith(value, prefix) return value:sub(1, #prefix) == prefix end +local function resetCallbacks() + for _, name in ipairs(dynamicCallbacks) do + env[name] = nil + end + dynamicCallbacks = {} + callbackIndex = 0 +end + +local function callback(prefix, fn) + callbackIndex += 1 + local name = "keybindCheatsheet_" .. prefix .. "_" .. callbackIndex + env[name] = fn + table.insert(dynamicCallbacks, name) + return name +end + local KEY_LABELS = { XF86AudioRaiseVolume = "Vol Up", XF86AudioLowerVolume = "Vol Down", @@ -546,7 +566,7 @@ local function bindingRow(binding, occurrence) local description = effectiveDescription(binding) local identity = binding.id .. "#" .. occurrence if view == "edit" and editingId == identity then - local visibilityChanged = function() + local visibilityChanged = callback("visibility", function() if hidden then preferences.hidden[binding.id] = nil else @@ -554,15 +574,15 @@ local function bindingRow(binding, occurrence) end savePreferences() render() - end - local inputChanged = function(value) editDraft = value end - local submit = function(value) saveCustomDescription(binding, value) end - local save = function() saveCustomDescription(binding, editDraft) end - local cancel = function() + end) + local inputChanged = callback("editChange", function(value) editDraft = value end) + local submit = callback("editSubmit", function(value) saveCustomDescription(binding, value) end) + local save = callback("editSave", function() saveCustomDescription(binding, editDraft) end) + local cancel = callback("editCancel", function() editingId = nil editDraft = "" render() - end + end) return ui.row({ key = "edit-" .. identity, gap = 6, align = "center", paddingV = 0 }, { ui.row({ minWidth = 188, gap = 3, align = "center", opacity = contentOpacity }, pills), ui.input({ key = "description-" .. identity, value = editDraft, placeholder = tr("edit_description"), focus = true, controlSize = "sm", flexGrow = 1, onChange = inputChanged, onSubmit = submit }), @@ -589,12 +609,12 @@ local function bindingRow(binding, occurrence) ui.column({ flexGrow = 1, gap = 0, opacity = contentOpacity }, labels), } if view == "edit" then - local edit = function() + local edit = callback("edit", function() editingId = identity editDraft = description render() - end - local visibilityChanged = function() + end) + local visibilityChanged = callback("visibility", function() if hidden then preferences.hidden[binding.id] = nil else @@ -602,7 +622,7 @@ local function bindingRow(binding, occurrence) end savePreferences() render() - end + end) table.insert(row, ui.button({ glyph = "pencil", width = 22, height = 22, glyphSize = 12, variant = "ghost", controlSize = "sm", tooltip = tr("edit_description"), onClick = edit })) table.insert(row, ui.button({ glyph = hidden and "eye-off" or "eye", width = 22, height = 22, glyphSize = 13, variant = "ghost", controlSize = "sm", selected = hidden, tooltip = hidden and tr("show_binding") or tr("hide_binding"), onClick = visibilityChanged })) end @@ -647,16 +667,16 @@ local function hiddenCount() end local function colorControl(bucket, property) - local choose = function() chooseColor(bucket.id, property) end - local paste = function() pasteColor(bucket.id, property) end - local reset = function() + local choose = callback("chooseColor", function() chooseColor(bucket.id, property) end) + local paste = callback("pasteColor", function() pasteColor(bucket.id, property) end) + local reset = callback("resetColor", function() if preferences.colors[bucket.id] ~= nil then preferences.colors[bucket.id][property] = nil if next(preferences.colors[bucket.id]) == nil then preferences.colors[bucket.id] = nil end savePreferences() render() end - end + end) return ui.row({ gap = 5, align = "center", flexGrow = 1 }, { ui.box({ width = 24, height = 24, radius = 5, fill = colorValue(bucket.id, property), border = "outline", borderWidth = 1 }), ui.button({ text = property == "background" and tr("background") or tr("text"), variant = "ghost", controlSize = "sm", flexGrow = 1, onClick = choose }), @@ -678,11 +698,11 @@ local function appearanceBody() }), })) end - local resetAll = function() + local resetAll = callback("resetAllColors", function() preferences.colors = {} savePreferences() render() - end + end) table.insert(rows, ui.separator({ spacing = 6 })) table.insert(rows, ui.row({ gap = 8, align = "center" }, { ui.button({ glyph = "restore", text = tr("reset_colors"), onClick = resetAll }), @@ -691,19 +711,19 @@ local function appearanceBody() end local function renderHeader() - local refreshClick = function() refresh() end - local editMode = function() + local refreshClick = callback("refresh", function() refresh() end) + local editMode = callback("editMode", function() view = view == "edit" and "bindings" or "edit" editingId = nil editDraft = "" render() - end - local appearance = function() + end) + local appearance = callback("appearance", function() view = view == "appearance" and "bindings" or "appearance" editingId = nil editDraft = "" render() - end + end) local title = tr("title") if currentCompositor ~= "" then title = (currentCompositor == "mango" and "Mango" or (currentCompositor == "niri" and "Niri" or "Hyprland")) .. " Keymap" @@ -715,16 +735,16 @@ local function renderHeader() }), } if (view == "bindings" or view == "edit") and not loading and panelError == nil then - local searchChanged = function(value) + local searchChanged = callback("search", function(value) query = value editingId = nil render() - end - local clearSearch = function() + end) + local clearSearch = callback("clearSearch", function() query = "" searchRevision += 1 render() - end + end) table.insert(children, ui.input({ key = "search-" .. searchRevision, value = query, placeholder = tr("search_placeholder"), controlSize = "sm", width = 300, onChange = searchChanged })) table.insert(children, ui.button({ glyph = "x", width = 26, variant = "ghost", controlSize = "sm", enabled = query ~= "", tooltip = tr("clear"), onClick = clearSearch })) end @@ -733,11 +753,11 @@ local function renderHeader() end if view == "edit" then local count = hiddenCount() - local restoreHidden = function() + local restoreHidden = callback("restoreHidden", function() restoreHiddenBindings(bindings, preferences.hidden) savePreferences() render() - end + end) table.insert(children, ui.label({ text = tr("hidden_count", { count = count }), color = "on_surface_variant", fontSize = 12 })) table.insert(children, ui.button({ glyph = "eye", variant = "ghost", controlSize = "sm", enabled = count > 0, tooltip = tr("restore_hidden"), onClick = restoreHidden })) end @@ -749,6 +769,7 @@ end render = function() if not panelOpen then return end + resetCallbacks() local contentState = "bindings" if view == "appearance" then contentState = "appearance" @@ -795,6 +816,7 @@ local function releasePanelState(clearModel) query = "" editingId = nil editDraft = "" + resetCallbacks() if clearModel then bindings = {} parseWarnings = {} @@ -849,6 +871,7 @@ local function lifecycleState() refreshing = refreshing, bindingCount = #bindings, snapshotLoaded = snapshot ~= nil, + callbackCount = #dynamicCallbacks, editDraft = editDraft, } end diff --git a/keybind-cheatsheet/plugin.toml b/keybind-cheatsheet/plugin.toml index 6d7b69c8..c52759e3 100644 --- a/keybind-cheatsheet/plugin.toml +++ b/keybind-cheatsheet/plugin.toml @@ -1,6 +1,6 @@ id = "kenn/keybind-cheatsheet" name = "Keybind Cheatsheet" -version = "0.2.1" +version = "0.2.2" plugin_api = 9 author = "kenn" license = "MIT" diff --git a/keybind-cheatsheet/service.luau b/keybind-cheatsheet/service.luau index a490342b..7d1d5705 100644 --- a/keybind-cheatsheet/service.luau +++ b/keybind-cheatsheet/service.luau @@ -623,7 +623,7 @@ end local function parseNiriContent(content, sourceFile, context) local includes = {} for rawLine in (content .. "\n"):gmatch("(.-)\r?\n") do - local includePath = rawLine:match('^%s*include%s+"([^"]+)"') + local includePath = rawLine:match('^%s*include%s+["\']([^"\']+)["\']') if includePath ~= nil then table.insert(includes, { path = includePath, optional = false }) end @@ -777,9 +777,42 @@ local function readConfig(rootPath, parser) return context end +local function extractLuaStringLiterals(expr) + local literals = {} + local index = 1 + while index <= #expr do + local char = expr:sub(index, index) + if char == '"' or char == "'" then + local quote = char + index += 1 + local value = {} + local escaped = false + while index <= #expr do + local c = expr:sub(index, index) + if escaped then + table.insert(value, c) + escaped = false + elseif c == "\\" then + escaped = true + elseif c == quote then + break + else + table.insert(value, c) + end + index += 1 + end + table.insert(literals, table.concat(value)) + end + index += 1 + end + return literals +end + local function scanHyprLuaContent(content, sourceFile, context) local category = "" local includes = {} + local pendingDescription = false + for rawLine in (content .. "\n"):gmatch("(.-)\r?\n") do local heading = rawLine:match("^%s*%-%-%s*%d+%.%s*(.-)%s*$") if heading ~= nil and heading ~= "" then @@ -796,13 +829,34 @@ local function scanHyprLuaContent(content, sourceFile, context) table.insert(includes, { path = pathJoin(context.luaRoot, modulePath), optional = false }) end - local description = code:match('description%s*=%s*"([^"]*)"') - or code:match("description%s*=%s*'([^']*)'") - or code:match('desc%s*=%s*"([^"]*)"') - or code:match("desc%s*=%s*'([^']*)'") - if description ~= nil and description ~= "" then - local kind = code:find("%.%.") ~= nil and "prefix" or "exact" - table.insert(context.rules, { kind = kind, value = description, category = category ~= "" and category or "Other" }) + local rhs = code:match('description%s*=%s*(.*)$') or code:match('desc%s*=%s*(.*)$') + if rhs ~= nil or pendingDescription then + local expr = rhs or code + local literals = extractLuaStringLiterals(expr) + local nonEmpty = {} + for _, str in ipairs(literals) do + local cleaned = trim(str) + if cleaned ~= "" then + table.insert(nonEmpty, cleaned) + end + end + + if #nonEmpty > 0 then + pendingDescription = false + local combined = table.concat(nonEmpty) + local hasConcat = code:find("%.%.", 1, true) ~= nil or (rhs ~= nil and rhs:find("%.%.", 1, true) ~= nil) + local kind = hasConcat and "prefix" or "exact" + local cat = category ~= "" and category or "Other" + + table.insert(context.rules, { kind = kind, value = combined, category = cat }) + for _, piece in ipairs(nonEmpty) do + if piece ~= combined then + table.insert(context.rules, { kind = "exact", value = piece, category = cat }) + end + end + elseif rhs ~= nil and #literals == 0 then + pendingDescription = true + end end end return includes diff --git a/keybind-cheatsheet/translations/en.json b/keybind-cheatsheet/translations/en.json index 07002327..9cc80c0b 100644 --- a/keybind-cheatsheet/translations/en.json +++ b/keybind-cheatsheet/translations/en.json @@ -1,88 +1,88 @@ { + "title": "Keybind Cheatsheet", + "widget_tooltip": "Open keybind cheatsheet", + "loading": "Reading keybindings...", + "search_placeholder": "Search keys, descriptions, and actions", + "refresh": "Refresh", "appearance": "Appearance", "back": "Back", - "background": "Background", - "binding_count": "{count} bindings", - "cancel": "Cancel", - "choose_color": "Choose color", "clear": "Clear search", - "customize_colors": "Key colors", "edit_bindings": "Edit bindings", - "edit_description": "Edit description", "finish_editing": "Finish editing", - "hidden_count": "{count} hidden", + "edit_description": "Edit description", "hide_binding": "Hide binding", - "hyprctl_failed": "Hyprland did not return its active keybindings.", - "hyprctl_missing": "hyprctl is required for Hyprland Lua keybindings.", - "loading": "Reading keybindings...", - "missing_config": "The configured keybind file could not be read.", - "no_results": "No keybindings match this search.", - "other": "Other", - "paste": "Paste color", - "refresh": "Refresh", + "show_binding": "Show binding", + "save": "Save", + "cancel": "Cancel", "reset": "Reset", - "reset_colors": "Reset all colors", + "paste": "Paste color", + "choose_color": "Choose color", "restore_hidden": "Restore hidden bindings", - "save": "Save", - "search_placeholder": "Search keys, descriptions, and actions", + "reset_colors": "Reset all colors", + "no_results": "No keybindings match this search.", + "without_description": "Without Description", + "other": "Other", + "source_warning": "Some configuration files could not be read.", + "unsupported": "No supported compositor was detected.", + "missing_config": "The configured keybind file could not be read.", + "hyprctl_missing": "hyprctl is required for Hyprland Lua keybindings.", + "hyprctl_failed": "Hyprland did not return its active keybindings.", + "customize_colors": "Key colors", + "background": "Background", + "text": "Text", + "hidden_count": "{count} hidden", + "binding_count": "{count} bindings", "settings": { - "columns": { - "description": "Maximum number of balanced columns in the panel.", - "label": "Columns" - }, "compositor": { - "description": "Detect the active compositor or force a parser.", "label": "Compositor", + "description": "Detect the active compositor or force a parser.", "options": { "auto": "Automatic", - "hyprland": "Hyprland", "mango": "Mango", + "hyprland": "Hyprland", "niri": "Niri" } }, - "glyph": { - "description": "Icon shown in the bar.", - "label": "Glyph" + "mango_config": { + "label": "Mango config", + "description": "Main Mango configuration file. Source directives are followed." }, "hyprland_config": { - "description": "Main classic Hyprland configuration file.", - "label": "Hyprland config" + "label": "Hyprland config", + "description": "Main classic Hyprland configuration file." }, "hyprland_lua_config": { - "description": "Lua configuration used to recover categories for live binds.", - "label": "Hyprland Lua config" + "label": "Hyprland Lua config", + "description": "Lua configuration used to recover categories for live binds." }, "hyprland_parser": { - "description": "Use the live Lua bind list, classic config, or automatic detection.", "label": "Hyprland parser", + "description": "Use the live Lua bind list, classic config, or automatic detection.", "options": { "auto": "Automatic", - "conf": "Classic config", - "lua": "Lua / hyprctl" + "lua": "Lua / hyprctl", + "conf": "Classic config" } }, - "mango_config": { - "description": "Main Mango configuration file. Source directives are followed.", - "label": "Mango config" - }, "niri_config": { - "description": "Main Niri KDL configuration file. Include directives are followed.", - "label": "Niri config" + "label": "Niri config", + "description": "Main Niri KDL configuration file. Include directives are followed." }, - "show_actions": { - "description": "Show compositor commands below binding descriptions.", - "label": "Show actions" + "columns": { + "label": "Columns", + "description": "Maximum number of balanced columns in the panel." }, "show_undescribed": { - "description": "Put bindings without descriptions in a separate section.", - "label": "Show undescribed bindings" + "label": "Show undescribed bindings", + "description": "Put bindings without descriptions in a separate section." + }, + "show_actions": { + "label": "Show actions", + "description": "Show compositor commands below binding descriptions." + }, + "glyph": { + "label": "Glyph", + "description": "Icon shown in the bar." } - }, - "show_binding": "Show binding", - "source_warning": "Some configuration files could not be read.", - "text": "Text", - "title": "Keybind Cheatsheet", - "unsupported": "No supported compositor was detected.", - "widget_tooltip": "Open keybind cheatsheet", - "without_description": "Without Description" + } }