diff options
| author | seraphim <18ycm9tx@anonaddy.me> | 2026-07-24 20:57:33 +0700 |
|---|---|---|
| committer | seraphim <18ycm9tx@anonaddy.me> | 2026-07-24 20:57:33 +0700 |
| commit | 395c08d0a334cdffbcc370384bee67f19f964399 (patch) | |
| tree | d57cdf13385785aaf068d4f8408fe6cc2f095999 /noctalia/.config/plugins | |
frst
Diffstat (limited to 'noctalia/.config/plugins')
56 files changed, 9797 insertions, 0 deletions
diff --git a/noctalia/.config/plugins/keybind-cheatsheet/BarWidget.qml b/noctalia/.config/plugins/keybind-cheatsheet/BarWidget.qml new file mode 100644 index 0000000..349d9cc --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/BarWidget.qml @@ -0,0 +1,75 @@ +import QtQuick +import Quickshell +import qs.Commons +import qs.Services.UI +import qs.Widgets + +NIconButton { + id: root + + property var pluginApi: null + property ShellScreen screen + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + baseSize: Style.getCapsuleHeightForScreen(screen?.name) + applyUiScale: false + icon: "keyboard" + tooltipText: pluginApi?.tr("barwidget.tooltip") + tooltipDirection: BarService.getTooltipDirection(screen?.name) + customRadius: Style.radiusL + + colorBg: Style.capsuleColor + colorFg: Color.mOnSurface + colorBgHover: Color.mHover + colorFgHover: Color.mOnHover + colorBorder: "transparent" + colorBorderHover: "transparent" + + border.color: Style.capsuleBorderColor + border.width: Style.capsuleBorderWidth + + NPopupContextMenu { + id: contextMenu + + model: [ + { + "label": pluginApi?.tr("barwidget.open"), + "action": "open-panel", + "icon": "keyboard" + }, + { + "label": pluginApi?.tr("barwidget.settings"), + "action": "plugin-settings", + "icon": "settings" + }, + ] + + onTriggered: action => { + contextMenu.close(); + PanelService.closeContextMenu(screen); + + if (action === "open-panel") { + if (pluginApi) { + pluginApi.openPanel(screen); + } + } else if (action === "plugin-settings") { + if (pluginApi) { + BarService.openPluginSettings(screen, pluginApi.manifest); + } + } + } + } + + onClicked: { + if (pluginApi) { + pluginApi.openPanel(screen); + } + } + + onRightClicked: { + PanelService.showContextMenu(contextMenu, root, screen); + } +} diff --git a/noctalia/.config/plugins/keybind-cheatsheet/CHANGELOG.md b/noctalia/.config/plugins/keybind-cheatsheet/CHANGELOG.md new file mode 100644 index 0000000..1d25f80 --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/CHANGELOG.md @@ -0,0 +1,284 @@ +# Changelog + +## [3.7.3] - 2026-05-25 + +### Fixes + +- **Niri: descriptions invisible on light/transparent backgrounds.** Since v3.5.0 the description text color defaulted to a hardcoded `#E0E0E0` (light gray), which is unreadable on light wallpapers shining through a transparent panel. Default is now empty → falls back to `Color.mOnSurface` (theme-aware, always readable). Existing users with a baked-in `#E0E0E0` in their `settings.json` should hit **Reset** on the Description color in Settings → Appearance to pick up the theme-aware default. Reported by @porcaror (#884) +- **MangoWC: `binds=` / `axisbinds=` / `mousebinds=` (plural) directives now parsed.** MangoWC has two directive families: singular (keycode, QWERTY-mapped) and plural (keysym, layout-aware — used by AZERTY and other non-QWERTY layouts). The parser previously only matched the singular form, so AZERTY configs showed no binds. Both forms are now recognised. Reported by @Pintjebier (#881) + +## [3.7.2] - 2026-05-21 + +### Fixes + +- **Settings panel was rendering empty**: `Settings.qml` and `ColorPairRow.qml` reference sibling files (`ColorPairRow`, `ColorPill`) but Quickshell's `NPluginSettingsPopup` loads Settings via `Loader.setSource(file://...)` without adding the plugin directory to the implicit import list. The QML engine threw `ColorPairRow is not a type`, aborting the load. Added `import "."` to both files so the sibling components resolve. Reported by @ajgringo619 (#862) +- **Hyprland Lua: split configs lost their category headers** (`require("modules.keybinds")` resolved as `modules.keybinds.lua` instead of `modules/keybinds.lua`). Lua's `require` treats dots as path separators; the parser now converts them before resolving, so configs that split binds into submodules — e.g. `hyprland.lua` -> `require("modules.keybinds")` -> `modules/keybinds.lua` — pick up their `-- N. CATEGORY` headers and binds land in the correct categories instead of all falling into "Other". Reported by @ajgringo619 (#862) + +## [3.7.1] - 2026-05-20 + +### Fixes + +- Settings: fix `ReferenceError: rootItem is not defined` in the bind-override management section — the override counter, **Restore hidden**, and **Clear all overrides** buttons were broken because of stale `rootItem` references after the `id` rename +- Settings: use the native `tr(key, interpolations)` API for the overrides summary line instead of chained `.replace()` calls (review feedback from @spiros132) + +## [3.7.0] - 2026-05-17 + +### Features + +**Hyprland Lua config support (`hyprland.lua`)** +- Hyprland 0.55+ replaces hyprlang `.conf` with a Lua config. Added a second Hyprland parsing tor that queries `hyprctl binds -j` for the authoritative, already-evaluated bind list — correctly handling `require()`, `for` loops, and multi-key chords that cannot be recovered by static text parsing +- Categories are recovered by lightly scanning `hyprland.lua` (and its `require()` modules) for `-- N. NAME` headers and `description = "..."` literals, with a prefix heuristic for loop-generated binds (e.g. `"Workspace " .. i`) +- Parser mode setting `auto` / `lua` / `conf`: `auto` uses the Lua tor when `hyprland.lua` exists, otherwise the existing `.conf` parser +- The legacy hyprlang `.conf` parser is **kept unchanged as a fallback** for users still on hyprlang configs + +**Add description / hide binds without a description** +- Binds with no description are surfaced in a dedicated "Without Description" section instead of being dropped +- Each undescribed bind can be given a custom description inline, or hidden, directly from the panel +- Overrides are keyed by a stable bind identity (`submap|modmask|key|flags|dispatcher`) that excludes the unstable `__lua` registry ref, so they survive Hyprland restarts +- New settings: `showUndescribedBinds`, an override summary, "Restore hidden" and "Clear all overrides" actions + +**`refresh` IPC** +- Added `refresh` to the plugin IPC handler so the cheatsheet can be re-parsed from a keybind, e.g. `qs ipc call plugin:keybind-cheatsheet refresh` + +### Bug Fixes + +- Fixed a `text` binding loop on the window-height settings input (now seeded once via `Component.onCompleted`) +- Replaced hardcoded `font.pointSize` in bind rows with `Style.fontSizeXS` + +### Notes + +- This release bundles the color-customization, MangoWC-support and Niri parser work previously tracked separately (see the 3.5.0–3.6.2 entries below) together with the Hyprland Lua support, shipped as a single 3.7.0 release. + +--- + +## [3.6.2] - 2026-04-29 + +### i18n + +- Translate "no description" fallback label for undescribed MangoWC binds — was hardcoded `"(no description)"`, now uses `panel.no-description` translation key across all 20 locale files (addresses review feedback from @spiros132 on PR #725). + +--- + +## [3.6.1] - 2026-04-26 + +### MangoWC Parser Regression Fixes + +Addresses review feedback on PR #725 from @countgitmick. + +- **XF86 media keys now formatted:** `formatMangoKeyCombo` calls `formatSpecialKey`, so `XF86AudioRaiseVolume` renders as "Vol Up", `XF86AudioMute` as "Mute", brightness keys as "Bright Up/Down", etc. +- **Stray `#` comments no longer become categories:** `extractMangoCategory` restored filters — length cap of 100 chars, horizontal-rule strip (`────`, `====`, `----`), numbered-list extraction (`1. Foo` → `Foo`), paren/bracket continuation reject, flow-arrow reject (`→`, `->`, `=>`), keyword-prefixed notes reject (TODO/FIXME/NOTE/HACK/XXX/BUG/WIP). +- **Per-bind `#"description"` suffix preserved:** `findMangoUnquotedComment` skips past `#"`, and `parseMangoConfig` extracts the trailing quoted description so `bind=SUPER,T,exec,foo #"Open terminal"` keeps "Open terminal" as the rendered description. +- **Modifier aliases:** `LOGO` (= SUPER) and `MOD1` (= ALT) are now recognized as valid Mango modifier tokens. +- **Default category localized:** the fallback category for `bind=`/`axisbind=`/`mousebind=` lines without a preceding `# Category` now uses `pluginApi.tr("default-category")` instead of hardcoded "General"/"Mouse". +- **Category detection actually runs now:** moved category extraction into the pure-comment branch so `# Title` lines actually update `currentCategory` (previously dead code path). + +--- + +## [3.5.0] - 2026-04-21 + +### New Features + +**Full Per-Category Color Customization** +- Added background and text color pickers for every key category: `Super`, `Ctrl`, `Shift`, `Alt`, `XF86`, `Print`, numeric, mouse, and default letter keys — plus the description text color +- 9 new `keyText*` settings (`keyTextSuper`, `keyTextCtrl`, …) let the label text of every category be themed independently of the background +- `keyColorSuper`, `keyColorCtrl`, `keyColorShift` use an empty-string sentinel to mean "use Material theme accent" (`mPrimary` / `mSecondary` / `mTertiary`), so themed setups remain untouched unless the user deliberately overrides + +**Redesigned Color Picker UI** +- Two-pill layout per category: left pill previews the background, right pill previews the label text on that background +- Single click anywhere on a pill opens `NColorPickerDialog` +- Pencil-icon edit affordance removed — the entire pill is the control +- Per-row reset button clears the override and restores the theme default +- New "Reset all colors" button zeroes out all 20+ color overrides in one action + +**Clipboard Quick-Paste** +- When a valid `#RRGGBB` or `#RRGGBBAA` hex is detected in the clipboard (via `wl-paste`, polled every 1500 ms), a paste icon appears inside each pill +- Clicking the icon applies the clipboard hex to that pill immediately, no picker dialog required +- A new hint row above the color pickers tells the user about this behavior + +**Live Preview + Revert on Cancel** +- Color changes are reflected in the preview row immediately via `_applyPreview(key, value)` instead of only after Save +- A snapshot is taken on `Component.onCompleted`; closing the Settings panel without clicking Save restores the snapshot in `Component.onDestruction`, so cancelled edits are truly reverted + +### Bug Fixes + +**Ctrl / Shift now actually customizable** +- Previously `keyColorCtrl` / `keyColorShift` could not be overridden; the panel always fell back to `Color.mPrimaryContainer` (which does not exist on this shell build). Fixed by consistently honoring the empty-string override sentinel and falling back to `mPrimary` / `mSecondary` / `mTertiary` +- Removed stray references to `Color.mPrimaryContainer` that produced undefined colors at runtime + +**Panel opacity now matches other plugins** +- Switched the Panel from painting its own opaque background to the standard plugin panel-in-panel pattern used by `tailscale` and `hello-world`, so the keybind panel now inherits the user's shell opacity/blur settings correctly + +### Code Quality + +**Component extraction** +- Extracted `ColorPill.qml` (single background/text pill with clipboard paste + picker dialog) and `ColorPairRow.qml` (label + bg pill + text pill + reset) from the Settings surface. Settings.qml is now noticeably smaller and each pill row is a single declarative `ColorPairRow { ... }` + +**Edit-copy discipline + i18n** +- Extended the edit-copy pattern to all 20+ color/text settings — `valueKeyColor*` / `valueKeyText*` properties are the source of truth during edit, `saveSettings()` writes them into `pluginSettings` and calls `pluginApi.saveSettings()` +- The hardcoded `qs … ipc call …` example now lives in `settings.keybind-ipc-command` so it is translation-system-backed like every other string +- Removed the last `|| "auto"` fallback strings in `ColorPairRow.qml`; the translation system handles missing keys +- New i18n keys `panel.search-placeholder`, `settings.color-auto`, `settings.color-paste-hint`, `settings.keybind-ipc-command` localized across all 20 supported languages + +**Cleanup** +- Timer and Process objects (clipboard poll + `wl-paste` Process) are stopped/terminated in `Component.onDestruction` +- Unified empty-string-or-hex property types for overrides (`string` for overrides, `color` for non-optional defaults), preventing QML's implicit `color` coercion from turning `""` into `#000000` + +### Manifest + +- Version bumped to `3.5.0` +- `metadata.defaultSettings` gains 20+ new color defaults covering every per-category override plus `keyLabelColor` and `descriptionTextColor` +- `windowHeight` default corrected from `0` to `850` so the manual-height branch has a sensible initial value +- Tags extended with `Hyprland` and `Niri` for plugin-catalog search filtering + +--- + +## [3.4.0] - 2026-04-07 + +### Bug Fixes + +**Settings unreachable from panel button** +- Fixed critical bug where clicking the settings button (top-right of the keybind panel) blocked all input in the settings window +- The panel now closes before opening settings, preventing the open panel from intercepting mouse events +- Reported by Discord users: editing width/height was impossible when settings were opened this way + +**Settings not saved when opened from panel** +- Fixed settings changes being silently discarded when the settings window was opened via the panel button +- `saveSettings()` was declared on an inner `ColumnLayout` instead of the root `Item`, making it invisible to the Noctalia shell +- With the previous direct-mutation approach this went unnoticed; after switching to the edit-copy pattern saves now work correctly from all entry points + +### Code Quality + +**Settings: edit-copy pattern** +- Replaced direct `pluginSettings` mutation in `onTextChanged` handlers with proper edit-copy properties (`editWindowWidth`, `editWindowHeight`, `editAutoHeight`, `editColumnCount`, `editModKeyVariable`, `editHyprlandConfigPath`, `editNiriConfigPath`) +- Changes are committed to `pluginSettings` only when the user clicks Save, matching Noctalia plugin conventions + +**i18n: corrected structure** +- Removed `"keybind-cheatsheet"` top-level wrapper from all 20 language JSON files +- Removed `"keybind-cheatsheet."` prefix from all 50 `tr()` calls across all QML files +- Structure now matches the Noctalia plugin i18n specification + +**Removed dead code** +- Deleted unused `parseNiriConfig()` function (118 lines) superseded by `parseNiriFileContent()` in v3.1.0 + +**Shell injection hardening** +- Glob expansion in config path resolution now passes user-provided patterns as positional shell arguments (`$1`) instead of string-concatenating them into the shell command, preventing potential injection via crafted config path values + +**resizeTimer: semantic popup detection** +- Replaced fragile `toString()` string matching (`"Popup_QMLTYPE"`, `"NPluginSettingsPopup"`) with a semantic check (`typeof obj.modal === "boolean"`) that works with any QML `Popup` subclass + +**Named key badge colors** +- Extracted hardcoded hex colors in `getKeyColor()` into named `readonly property color` constants (`keyColorAlt`, `keyColorXF86`, `keyColorPrint`, `keyColorNumeric`, `keyColorMouse`) + +### Manifest + +- Added missing `repository` field +- Added tags: `System`, `Indicator` (alongside existing `Bar`, `Panel`) + +--- + +## [3.2.2] - 2026-02-08 + +### Bug Fixes + +**Hyprland Parser - No Category Handling** +- Fixed parser crash when Hyprland config has no category headers (`# 1. Category Name`) +- Parser now creates default "Keybinds" category for configs without categories +- Default category name is fully translatable via i18n system +- Prevents keybind loss for users who don't organize their configs with categories + +### Translations + +**Complete i18n Coverage** +- Added `default-category` translations for all 19 supported languages +- Added missing `error` section translations to all language files (includes German from 3.1.2) +- All language files now have identical structure (61-62 lines each) +- Improved translation consistency across all locales + +**Supported Languages:** +- English (en), Polish (pl), German (de), French (fr), Spanish (es) +- Italian (it), Portuguese (pt), Dutch (nl), Russian (ru), Japanese (ja) +- Chinese Simplified (zh-CN), Chinese Traditional (zh-TW), Korean (ko-KR) +- Turkish (tr), Ukrainian (uk-UA), Swedish (sv), Hungarian (hu) +- Kurdish (ku), Norwegian Nynorsk (nn-NO), Hindi/Nepali (hn) + +### Code Quality + +**Memory Leak Prevention** +- Added recursion limit tracking for parser (`hasCategories` flag) +- Improved parser robustness and error handling +- Better fallback behavior for edge cases + +--- + +## [3.1.2] - 2026-02-08 + +### Translations + +- Added german translations for the `error` keys + +## [3.1.1] - 2026-02-03 + +### Smart Caching + +**Compositor Change Detection** +- Plugin now detects when compositor changes (e.g., switching from Hyprland to Niri) +- Automatically re-parses config only when compositor differs from cached data +- Instant panel opening when using same compositor (uses cache) +- Saves detected compositor in settings for comparison + +### Bug Fixes + +**Improved Niri Parser** +- Fixed multiline bind parsing - handles binds that span multiple lines +- Added `spawn-sh` action support for shell command spawning +- Added `move-column-to-workspace` and `move-window-to-workspace` action categories +- Better handling of complex Niri config structures + +**Better Error Messages** +- User-friendly messages for unsupported compositors (Sway, LabWC, MangoWC) +- Each compositor shows specific explanation why it's not supported +- All error messages are translatable via i18n + +### Documentation + +**README Updates** +- Fixed IPC command syntax in examples +- Updated keybind format examples to use `$mainMod` instead of `$mod` +- Clarified configuration file paths and formats + +--- + +## [3.1.0] - 2026-02-03 + +### New Features + +**Niri Support** +- Full Niri compositor support with KDL config parsing +- Recursive file parsing with `source` directive support +- Automatic action categorization +- Multiline bind support + +**Recursive Config Parsing** +- Both Hyprland and Niri now support recursive file includes +- Memory leak prevention with recursion limits +- Glob pattern support for bulk file imports + +### Improvements + +**Better Error Handling** +- Graceful fallback for unsupported compositors +- User-friendly error messages +- Translation support for all error states + +--- + +## [3.0.0] - 2026-01-30 + +### Initial Release + +**Core Features** +- Hyprland keybind parsing +- Category organization +- Multi-language support +- Bar widget integration +- Settings UI diff --git a/noctalia/.config/plugins/keybind-cheatsheet/ColorPairRow.qml b/noctalia/.config/plugins/keybind-cheatsheet/ColorPairRow.qml new file mode 100644 index 0000000..7094a4a --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/ColorPairRow.qml @@ -0,0 +1,93 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets +import "." as Local + +// Label + background pill + text pill + optional rename field + reset. Used by +// Settings.qml for per-category color customization. Empty bgValue/textValue +// means "use fallback". When showLabel is set, an extra text field lets the user +// rename the key's display label (used for the generic Mod2/Mod3/Mod5 keys). +RowLayout { + id: row + + property var pluginApi + property var screen + property string labelText: "" + property string bgValue: "" + property string textValue: "" + property color bgFallback: "#888888" + property color textFallback: "#FFFFFF" + property string letter: "A" + property string clipboardHex: "" + property bool showBg: true + property bool showText: true + // Optional display-label override (e.g. rename "Mod3" -> "Hyper") + property bool showLabel: false + property string labelValue: "" + property string labelPlaceholder: "" + + signal bgPicked(color value) + signal textPicked(color value) + signal bgPasted(string hex) + signal textPasted(string hex) + signal labelEdited(string value) + signal resetRequested() + + Layout.fillWidth: true + spacing: Style.marginM + + NText { + text: row.labelText + color: Color.mOnSurface + pointSize: Style.fontSizeM + Layout.preferredWidth: 180 * Style.uiScaleRatio + elide: Text.ElideRight + } + + Local.ColorPill { + Layout.preferredWidth: 130 * Style.uiScaleRatio + visible: row.showBg + screen: row.screen + hexValue: row.bgValue + displayColor: row.bgValue.length > 0 ? row.bgValue : row.bgFallback + textMode: false + clipboardHex: row.clipboardHex + placeholderText: row.bgValue.length === 0 ? row.pluginApi?.tr("settings.color-auto") : "" + onColorPicked: c => row.bgPicked(c) + onPasteRequested: hex => row.bgPasted(hex) + } + + Local.ColorPill { + Layout.preferredWidth: 130 * Style.uiScaleRatio + visible: row.showText + screen: row.screen + hexValue: row.textValue + displayColor: row.textValue.length > 0 ? row.textValue : row.textFallback + textMode: true + letter: row.letter + textModeBg: row.bgValue.length > 0 ? row.bgValue : row.bgFallback + clipboardHex: row.clipboardHex + placeholderText: row.textValue.length === 0 ? row.pluginApi?.tr("settings.color-auto") : "" + onColorPicked: c => row.textPicked(c) + onPasteRequested: hex => row.textPasted(hex) + } + + NTextInput { + Layout.preferredWidth: 160 * Style.uiScaleRatio + Layout.preferredHeight: Style.baseWidgetSize + visible: row.showLabel + text: row.labelValue + placeholderText: row.labelPlaceholder + onTextChanged: row.labelEdited(text) + } + + Item { Layout.fillWidth: true } + + NIconButton { + icon: "rotate" + tooltipText: row.pluginApi?.tr("settings.reset-color") + onClicked: row.resetRequested() + } +} diff --git a/noctalia/.config/plugins/keybind-cheatsheet/ColorPill.qml b/noctalia/.config/plugins/keybind-cheatsheet/ColorPill.qml new file mode 100644 index 0000000..98f21fa --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/ColorPill.qml @@ -0,0 +1,124 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +// Compact color pill: circle + hex code + optional paste icon. +// Opens NColorPickerDialog on click. Used in pairs (bg + text) per keybind category. +Rectangle { + id: pill + + property var screen + property string hexValue: "" + property color displayColor: "#888888" + property bool textMode: false + property string letter: "A" + property color textModeBg: "#444444" + property string clipboardHex: "" + property string placeholderText: "" + + signal colorPicked(color value) + signal pasteRequested(string hex) + + readonly property bool hasClipboardColor: clipboardHex.length > 0 && + clipboardHex.toLowerCase() !== hexValue.toLowerCase() + + implicitWidth: 150 + implicitHeight: Math.round(Style.baseWidgetSize * 1.1) + + radius: Style.iRadiusM + color: Color.mSurface + border.color: mouseArea.containsMouse ? Color.mPrimary : Color.mOutline + border.width: Style.borderS + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + var startColor = pill.hexValue.length > 0 ? pill.hexValue : pill.displayColor; + var dialog = dialogFactory.createObject(Overlay.overlay, { + "selectedColor": startColor, + "screen": pill.screen + }); + if (!dialog) return; + dialog.colorSelected.connect(function(c) { + pill.colorPicked(c); + }); + dialog.open(); + } + } + + Component { + id: dialogFactory + NColorPickerDialog {} + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Style.marginS + anchors.rightMargin: Style.marginS + spacing: Style.marginS + + Rectangle { + Layout.preferredWidth: pill.height * 0.62 + Layout.preferredHeight: pill.height * 0.62 + radius: width / 2 + color: pill.textMode ? pill.textModeBg : pill.displayColor + border.color: Color.mOutline + border.width: Style.borderS + + NText { + anchors.centerIn: parent + visible: pill.textMode + text: pill.letter + color: pill.displayColor + font.weight: Style.fontWeightBold + pointSize: Style.fontSizeS + } + } + + NText { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + text: pill.hexValue.length > 0 + ? pill.hexValue.toUpperCase() + : (pill.placeholderText.length > 0 ? pill.placeholderText : "auto") + family: Settings.data.ui.fontFixed + color: pill.hexValue.length > 0 ? Color.mOnSurface : Color.mOnSurfaceVariant + pointSize: Style.fontSizeXS + elide: Text.ElideRight + } + + Rectangle { + Layout.preferredWidth: pill.height * 0.62 + Layout.preferredHeight: pill.height * 0.62 + visible: pill.hasClipboardColor + radius: width / 2 + color: pasteArea.containsMouse ? Color.mPrimary : Color.mSurfaceVariant + border.color: Color.mOutline + border.width: Style.borderS + + NIcon { + anchors.centerIn: parent + icon: "clipboard" + color: pasteArea.containsMouse ? Color.mOnPrimary : Color.mOnSurfaceVariant + pointSize: Style.fontSizeS + } + + MouseArea { + id: pasteArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: pill.pasteRequested(pill.clipboardHex) + } + + ToolTip.visible: pasteArea.containsMouse && pill.clipboardHex.length > 0 + ToolTip.text: pill.clipboardHex.toUpperCase() + ToolTip.delay: 400 + } + } +} diff --git a/noctalia/.config/plugins/keybind-cheatsheet/Main.qml b/noctalia/.config/plugins/keybind-cheatsheet/Main.qml new file mode 100644 index 0000000..fbe79e0 --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/Main.qml @@ -0,0 +1,1740 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.UI +import qs.Services.Compositor + +Item { + id: root + property var pluginApi: null + + + Component.onCompleted: { + if (pluginApi && !parserStarted) { + checkAndParse(); + } + } + + onPluginApiChanged: { + if (pluginApi && !parserStarted) { + checkAndParse(); + } + } + + // Check if compositor changed since last parse, and re-parse if needed + function checkAndParse() { + var currentCompositor = getCurrentCompositor(); + var savedCompositor = pluginApi?.pluginSettings?.detectedCompositor || ""; + var hasData = (pluginApi?.pluginSettings?.cheatsheetData || []).length > 0; + + // Re-parse if: + // 1. No data cached yet, OR + // 2. Compositor changed since last parse + if (!hasData || currentCompositor !== savedCompositor) { + parserStarted = true; + runParser(); + } else { + parserStarted = true; // Mark as done, using cache + } + } + + // Get current compositor name + function getCurrentCompositor() { + if (CompositorService.isHyprland) return "hyprland"; + if (CompositorService.isNiri) return "niri"; + if (CompositorService.isSway) return "sway"; + if (CompositorService.isLabwc) return "labwc"; + if (CompositorService.isMango) return "mango"; + return "unknown"; + } + + // Get user-friendly message for unsupported compositors + function getUnsupportedCompositorMessage(compositor) { + var messages = { + "sway": { + short: pluginApi?.tr("error.sway-not-supported"), + detail: pluginApi?.tr("error.sway-detail") + }, + "labwc": { + short: pluginApi?.tr("error.labwc-not-supported"), + detail: pluginApi?.tr("error.labwc-detail") + }, + "unknown": { + short: pluginApi?.tr("error.unknown-compositor"), + detail: pluginApi?.tr("error.unknown-detail") + } + }; + return messages[compositor] || messages["unknown"]; + } + + property bool parserStarted: false + + // Bug 4 fix: version counter so Panel.qml can react to data updates + property int cheatsheetDataVersion: 0 + + // Memory leak prevention: cleanup on destruction + Component.onDestruction: { + clearParsingData(); + cleanupProcesses(); + } + + function cleanupProcesses() { + if (niriGlobProcess.running) niriGlobProcess.running = false; + if (niriReadProcess.running) niriReadProcess.running = false; + if (hyprGlobProcess.running) hyprGlobProcess.running = false; + if (hyprReadProcess.running) hyprReadProcess.running = false; + if (mangoGlobProcess.running) mangoGlobProcess.running = false; + if (mangoReadProcess.running) mangoReadProcess.running = false; + if (hyprDetectProcess.running) hyprDetectProcess.running = false; + if (hyprLuaReadProcess.running) hyprLuaReadProcess.running = false; + if (hyprctlBindsProcess.running) hyprctlBindsProcess.running = false; + + // Clear process buffers + niriGlobProcess.expandedFiles = []; + hyprGlobProcess.expandedFiles = []; + mangoGlobProcess.expandedFiles = []; + hyprctlChunks = []; + currentLines = []; + } + + function clearParsingData() { + filesToParse = []; + parsedFiles = {}; + accumulatedLines = []; + currentLines = []; + collectedBinds = {}; + parseDepthCounter = 0; + luaCategoryHeaders = []; + descToCategory = ({}); + prefixToCategory = []; + hyprctlChunks = []; + if (mangoGlobProcess.running) mangoGlobProcess.running = false; + if (mangoReadProcess.running) mangoReadProcess.running = false; + mangoGlobProcess.expandedFiles = []; + } + + // Refresh function - accessible from mainInstance + function refresh() { + if (!pluginApi) { + return; + } + + // Reset parserStarted to allow re-parsing + parserStarted = false; + isCurrentlyParsing = false; + + // Now run parser + parserStarted = true; + runParser(); + } + + // Recursive parsing support + property var filesToParse: [] + property var parsedFiles: ({}) + property var accumulatedLines: [] + property var currentLines: [] + property var collectedBinds: ({}) // Collect keybinds from all files + + // Memory leak prevention: recursion limits + property int maxParseDepth: 50 + property int parseDepthCounter: 0 + property bool isCurrentlyParsing: false + + function runParser() { + if (isCurrentlyParsing) { + return; + } + + isCurrentlyParsing = true; + parseDepthCounter = 0; + + // Detect compositor using CompositorService + var compositorName = getCurrentCompositor(); + if (!CompositorService.isHyprland && !CompositorService.isNiri && !CompositorService.isMango) { + isCurrentlyParsing = false; + + Logger.w("KeybindCheatsheet", "Unsupported compositor:", compositorName); + var unsupportedMsg = getUnsupportedCompositorMessage(compositorName); + saveToDb([{ + "title": pluginApi?.tr("error.unsupported-compositor"), + "binds": [ + { "keys": compositorName.toUpperCase(), "desc": unsupportedMsg.short }, + { "keys": "INFO", "desc": unsupportedMsg.detail } + ] + }]); + return; + } + + var homeDir = Quickshell.env("HOME"); + if (!homeDir) { + isCurrentlyParsing = false; + Logger.e("KeybindCheatsheet", "Cannot resolve $HOME environment variable"); + saveToDb([{ + "title": "ERROR", + "binds": [{ "keys": "ERROR", "desc": "Cannot get $HOME" }] + }]); + return; + } + + // Reset recursive state + filesToParse = []; + parsedFiles = {}; + accumulatedLines = []; + collectedBinds = {}; + + var filePath; + if (CompositorService.isHyprland) { + hyprConfPath = (pluginApi?.pluginSettings?.hyprlandConfigPath || (homeDir + "/.config/hypr/hyprland.conf")).replace(/^~/, homeDir); + hyprLuaPath = (pluginApi?.pluginSettings?.hyprlandLuaConfigPath || (homeDir + "/.config/hypr/hyprland.lua")).replace(/^~/, homeDir); + + var mode = pluginApi?.pluginSettings?.hyprlandParserMode || "auto"; + if (mode === "conf") { + startHyprlandConfTor(); + } else if (mode === "lua") { + startHyprlandLuaTor(); + } else { + // auto: prefer lua if hyprland.lua exists, else fall back to .conf + hyprDetectProcess.command = ["sh", "-c", '[ -f "$1" ] && echo lua || echo conf', "sh", hyprLuaPath]; + hyprDetectProcess.running = true; + } + return; + } + + if (CompositorService.isNiri) { + filePath = pluginApi?.pluginSettings?.niriConfigPath || (homeDir + "/.config/niri/config.kdl"); + } else if (CompositorService.isMango) { + filePath = pluginApi?.pluginSettings?.mangoConfigPath || (homeDir + "/.config/mango/config.conf"); + } + + filePath = filePath.replace(/^~/, homeDir); + filesToParse = [filePath]; + + if (CompositorService.isNiri) { + parseNextNiriFile(); + } else if (CompositorService.isMango) { + parseNextMangoFile(); + } + } + + function getDirectoryFromPath(filePath) { + var lastSlash = filePath.lastIndexOf('/'); + return lastSlash >= 0 ? filePath.substring(0, lastSlash) : "."; + } + + function resolveRelativePath(basePath, relativePath) { + var homeDir = Quickshell.env("HOME") || ""; + var resolved = relativePath.replace(/^~/, homeDir); + if (resolved.startsWith('/')) return resolved; + return getDirectoryFromPath(basePath) + "/" + resolved; + } + + function isGlobPattern(path) { + return path.indexOf('*') !== -1 || path.indexOf('?') !== -1; + } + + // ========== NIRI RECURSIVE PARSING ========== + function parseNextNiriFile() { + if (parseDepthCounter >= maxParseDepth) { + Logger.w("KeybindCheatsheet", "Niri parser hit max recursion depth (" + maxParseDepth + "), aborting"); + isCurrentlyParsing = false; + clearParsingData(); + return; + } + parseDepthCounter++; + + if (filesToParse.length === 0) { + finalizeNiriBinds(); + return; + } + + var nextFile = filesToParse.shift(); + + // Handle glob patterns + if (isGlobPattern(nextFile)) { + niriGlobProcess.expandedFiles = []; // Clear previous results + niriGlobProcess.command = ["sh", "-c", 'for f in $1; do [ -f "$f" ] && echo "$f"; done', "sh", nextFile]; + niriGlobProcess.running = true; + return; + } + + if (parsedFiles[nextFile]) { + parseNextNiriFile(); + return; + } + + parsedFiles[nextFile] = true; + currentLines = []; + niriReadProcess.currentFilePath = nextFile; + niriReadProcess.command = ["cat", nextFile]; + niriReadProcess.running = true; + } + + Process { + id: niriGlobProcess + property var expandedFiles: [] + running: false + + stdout: SplitParser { + onRead: data => { + var trimmed = data.trim(); + if (trimmed.length > 0) { + if (niriGlobProcess.expandedFiles.length < 100) { + niriGlobProcess.expandedFiles.push(trimmed); + } + } + } + } + + onExited: { + for (var i = 0; i < expandedFiles.length; i++) { + var path = expandedFiles[i]; + if (!root.parsedFiles[path] && root.filesToParse.indexOf(path) === -1) { + root.filesToParse.push(path); + } + } + expandedFiles = []; + root.parseNextNiriFile(); + } + } + + Process { + id: niriReadProcess + property string currentFilePath: "" + running: false + + stdout: SplitParser { + onRead: data => { + if (root.currentLines.length < 10000) { + root.currentLines.push(data); + } + } + } + + onExited: (exitCode, exitStatus) => { + if (exitCode === 0 && root.currentLines.length > 0) { + // First pass: find includes (Bug 1: skip commented-out lines) + for (var i = 0; i < root.currentLines.length; i++) { + var line = root.currentLines[i]; + // Bug 1 fix: skip lines starting with // (they are comments, not active includes) + if (line.trim().startsWith("//")) continue; + var includeMatch = line.match(/(?:include|source)\s+"([^"]+)"/i); + if (includeMatch) { + var includePath = includeMatch[1]; + var resolvedPath = root.resolveRelativePath(currentFilePath, includePath); + if (!root.parsedFiles[resolvedPath] && root.filesToParse.indexOf(resolvedPath) === -1) { + root.filesToParse.push(resolvedPath); + } + } + } + + // Bug 2 fix: auto-discover standard Noctalia/common keybind files from the config dir + var configDir = root.getDirectoryFromPath(currentFilePath); + var standardFiles = [ + configDir + "/keybindings.common.kdl", + configDir + "/keybindings.noctalia.kdl" + ]; + for (var s = 0; s < standardFiles.length; s++) { + var sf = standardFiles[s]; + if (!root.parsedFiles[sf] && root.filesToParse.indexOf(sf) === -1) { + // We optimistically add them; parseNextNiriFile will skip if the cat fails + root.filesToParse.push(sf); + } + } + + // Second pass: parse keybinds from this file + root.parseNiriFileContent(root.currentLines.join("\n")); + } + root.currentLines = []; + root.parseNextNiriFile(); + } + } + + function parseNiriFileContent(text) { + var lines = text.split('\n'); + var inBindsBlock = false; + var bindsBlockDepth = 0; + var currentCategory = null; + var bindsFoundInFile = 0; + + // State for multiline bind parsing + var currentBindKey = null; + var currentBindAttributes = ""; + var currentBindAction = ""; + var bindBraceDepth = 0; + + var actionCategories = { + "spawn": "Applications", + "spawn-sh": "Applications", + "focus-column": "Column Navigation", + "focus-window": "Window Focus", + "focus-workspace": "Workspace Navigation", + "move-column": "Move Columns", + "move-window": "Move Windows", + "move-column-to-workspace": "Workspace Management", + "move-window-to-workspace": "Workspace Management", + "consume-window": "Window Management", + "expel-window": "Window Management", + "close-window": "Window Management", + "fullscreen-window": "Window Management", + "maximize-column": "Column Management", + "set-column-width": "Column Width", + "switch-preset-column-width": "Column Width", + "reset-window-height": "Window Size", + "screenshot": "Screenshots", + "screenshot-window": "Screenshots", + "screenshot-screen": "Screenshots", + "power-off-monitors": "Power", + "quit": "System", + "toggle-animation": "Animations" + }; + + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim(); + + // Skip KDL block comments: /- + if (line.startsWith("/-")) continue; + + // Find binds block start + if (!inBindsBlock && line.startsWith("binds") && line.includes("{")) { + inBindsBlock = true; + bindsBlockDepth = 1; + continue; + } + + if (!inBindsBlock) continue; + + // Category markers - support multiple formats: + // //"Category Name" + // // "Category Name" + // // #"Category Name" + // //#"Category Name" + if (line.startsWith("//")) { + var categoryMatch = line.match(/\/\/\s*#?"([^"]+)"/); + if (categoryMatch) { + currentCategory = categoryMatch[1]; + } + continue; + } + + // Skip empty lines + if (line.length === 0) continue; + + // Track braces for binds block boundary + var openBraces = (line.match(/\{/g) || []).length; + var closeBraces = (line.match(/\}/g) || []).length; + + // If we're not currently parsing a multiline bind + if (currentBindKey === null) { + // Try to match a keybind start: Mod+Key or "Mod+Key" attributes { or Mod+Key { action; } + // Bug 3 fix: support optional surrounding quotes on the key combo (e.g. "Mod+Return") + var bindStartMatch = line.match(/^"?([A-Za-z0-9_+]+)"?\s*((?:[a-z\-]+=(?:"[^"]*"|[1-9][0-9]*|true|false)\s*)*)\{(.*)$/); + + if (bindStartMatch) { + currentBindKey = bindStartMatch[1]; + currentBindAttributes = bindStartMatch[2].trim(); + var restOfLine = bindStartMatch[3]; + + // Check if the bind is complete on this line (single-line bind) + if (restOfLine.includes("}")) { + // Single-line bind: Mod+H { focus-column-left; } + currentBindAction = restOfLine.replace(/\}\s*$/, "").trim(); + finalizeBind(); + } else { + // Multiline bind starts here + currentBindAction = restOfLine.trim(); + bindBraceDepth = 1; + } + } else { + // Not a bind start, track braces for binds block + bindsBlockDepth += openBraces - closeBraces; + if (bindsBlockDepth <= 0) { + inBindsBlock = false; + } + } + } else { + // We're in a multiline bind, accumulate action content + bindBraceDepth += openBraces - closeBraces; + + if (bindBraceDepth <= 0) { + // Bind is complete + currentBindAction += " " + line.replace(/\}\s*$/, "").trim(); + finalizeBind(); + } else { + // Still in multiline bind + currentBindAction += " " + line.trim(); + } + } + } + + function finalizeBind() { + if (!currentBindKey) return; + + bindsFoundInFile++; + var action = currentBindAction.trim().replace(/;$/, "").trim(); + + var hotkeyTitle = null; + var titleMatch = currentBindAttributes.match(/hotkey-overlay-title="([^"]+)"/); + if (titleMatch) hotkeyTitle = titleMatch[1]; + + var formattedKeys = formatNiriKeyCombo(currentBindKey); + var category = currentCategory || getNiriCategory(action, actionCategories); + var description = hotkeyTitle || formatNiriAction(action); + + // Extract verb (first whitespace-separated token of action) + var verbMatch = action.match(/^([A-Za-z0-9_\-]+)/); + var verb = verbMatch ? verbMatch[1] : ""; + + // Decompose key combo into modifier set + main key for merge/filter logic + var rawParts = currentBindKey.split("+"); + var mods = []; + var mainKey = ""; + for (var p = 0; p < rawParts.length; p++) { + var rp = rawParts[p].trim(); + if (rp === "Mod" || rp === "Super" || rp === "Win" || + rp === "Ctrl" || rp === "Control" || + rp === "Alt" || rp === "Shift") { + mods.push(rp === "Mod" || rp === "Win" ? "Super" : (rp === "Control" ? "Ctrl" : rp)); + } else if (rp.length > 0) { + mainKey = rp; + } + } + + if (!collectedBinds[category]) { + collectedBinds[category] = []; + } + collectedBinds[category].push({ + "keys": formattedKeys, + "desc": description, + "_verb": verb, + "_mods": mods.join("+"), + "_mainKey": mainKey + }); + + // Reset state + currentBindKey = null; + currentBindAttributes = ""; + currentBindAction = ""; + bindBraceDepth = 0; + } + } + + function finalizeNiriBinds() { + var categoryOrder = [ + "Noctalia", "Applications", "Window Management", "Column Navigation", + "Window Focus", "Workspace Navigation", "Workspace Management", + "Move Columns", "Move Windows", "Column Management", "Column Width", + "Window Size", "Screenshots", "Power", "System", "Animations" + ]; + + var categories = []; + for (var k = 0; k < categoryOrder.length; k++) { + var catName = categoryOrder[k]; + if (collectedBinds[catName] && collectedBinds[catName].length > 0) { + categories.push({ "title": catName, "binds": collectedBinds[catName] }); + } + } + + // Add remaining categories + for (var cat in collectedBinds) { + if (categoryOrder.indexOf(cat) === -1 && collectedBinds[cat].length > 0) { + categories.push({ "title": cat, "binds": collectedBinds[cat] }); + } + } + + if (categories.length === 0) { + Logger.w("KeybindCheatsheet", "Niri parser produced no binds; check config path and binds {} block"); + } + + if (pluginApi?.pluginSettings?.mergeSequentialBinds ?? true) { + categories = mergeSequentialBindsInCategories(categories); + } + + saveToDb(categories); + isCurrentlyParsing = false; + clearParsingData(); + } + + // ========== MERGE SEQUENTIAL KEYBINDS ========== + // Detects runs of consecutive numeric binds with identical modifiers, identical + // verb, and descriptions that differ only by the integer. + // Example: Super+1..5 → "Workspace 1", "Workspace 2", ... → "Workspace 1-5" + function mergeSequentialBindsInCategories(cats) { + if (!cats || cats.length === 0) return cats; + var out = []; + for (var c = 0; c < cats.length; c++) { + var cat = cats[c]; + out.push({ "title": cat.title, "binds": mergeBindsList(cat.binds || []) }); + } + return out; + } + + function mergeBindsList(binds) { + if (!binds || binds.length < 2) return binds; + + // Build a list of merge tokens for each bind: + // token = mods + "|" + verb + "|" + descriptionTemplate + // descriptionTemplate replaces standalone integers with "%N%" + var tokens = []; + var nums = []; + for (var i = 0; i < binds.length; i++) { + var b = binds[i]; + var mainKey = b._mainKey || ""; + var asInt = parseInt(mainKey, 10); + var keyIsInteger = !isNaN(asInt) && /^\d+$/.test(mainKey); + var descTemplate = (b.desc || "").replace(/\b\d+\b/g, "%N%"); + tokens.push({ + mods: b._mods || "", + verb: b._verb || "", + descTemplate: descTemplate, + keyIsInteger: keyIsInteger, + num: keyIsInteger ? asInt : NaN, + bind: b + }); + nums.push(keyIsInteger ? asInt : NaN); + } + + var result = []; + var i = 0; + while (i < tokens.length) { + var startTok = tokens[i]; + if (!startTok.keyIsInteger) { + result.push(startTok.bind); + i++; + continue; + } + + // Try to extend a run as long as next bind has same mods/verb/descTemplate + // and num is exactly previous + 1 + var runEnd = i; + while (runEnd + 1 < tokens.length) { + var nextTok = tokens[runEnd + 1]; + if (!nextTok.keyIsInteger) break; + if (nextTok.mods !== startTok.mods) break; + if (nextTok.verb !== startTok.verb) break; + if (nextTok.descTemplate !== startTok.descTemplate) break; + if (nextTok.num !== tokens[runEnd].num + 1) break; + runEnd++; + } + + var runLen = runEnd - i + 1; + if (runLen >= 3) { + // Collapse the run into a single ranged entry. + var firstBind = startTok.bind; + var lastBind = tokens[runEnd].bind; + var lo = startTok.num; + var hi = tokens[runEnd].num; + var rangeStr = lo + "-" + hi; + + // Reconstruct keys with the range token. Keep the modifier prefix from + // the first bind exactly as displayed (it already contains " + "). + var firstKeys = firstBind.keys || ""; + var lastPlus = firstKeys.lastIndexOf(" + "); + var prefix = lastPlus >= 0 ? firstKeys.substring(0, lastPlus + 3) : ""; + var mergedKeys = prefix + rangeStr; + + // Replace the integer in the description template with the range + var mergedDesc = (firstBind.desc || "").replace(/\b\d+\b/, rangeStr); + + result.push({ + "keys": mergedKeys, + "desc": mergedDesc, + "_verb": firstBind._verb, + "_mods": firstBind._mods, + "_mainKey": rangeStr, + "_merged": true + }); + i = runEnd + 1; + } else { + result.push(startTok.bind); + i++; + } + } + return result; + } + + // ========== HYPRLAND TOR SELECTION ========== + property string hyprConfPath: "" + property string hyprLuaPath: "" + + Process { + id: hyprDetectProcess + running: false + property string result: "" + stdout: SplitParser { + onRead: data => { hyprDetectProcess.result = data.trim(); } + } + onExited: { + if (result === "lua") { + root.startHyprlandLuaTor(); + } else { + root.startHyprlandConfTor(); + } + } + } + + function startHyprlandConfTor() { + filesToParse = [hyprConfPath]; + parseNextHyprlandFile(); + } + + function startHyprlandLuaTor() { + // Read hyprland.lua + required modules to recover category headers, + // then query hyprctl for the authoritative (loop/require-expanded) binds. + luaCategoryHeaders = []; + descToCategory = ({}); + prefixToCategory = []; + filesToParse = [hyprLuaPath]; + parseNextHyprLuaFile(); + } + + // ========== HYPRLAND LUA TOR (hyprctl binds -j) ========== + property var luaCategoryHeaders: [] // ordered category titles from `-- N. NAME` + property var descToCategory: ({}) // exact description -> category + property var prefixToCategory: [] // [{prefix, category}] for loop-built binds + property var hyprctlChunks: [] + + function parseNextHyprLuaFile() { + if (parseDepthCounter >= maxParseDepth) { + runHyprctlBinds(); + return; + } + parseDepthCounter++; + + if (filesToParse.length === 0) { + runHyprctlBinds(); + return; + } + + var nextFile = filesToParse.shift(); + if (parsedFiles[nextFile]) { + parseNextHyprLuaFile(); + return; + } + parsedFiles[nextFile] = true; + currentLines = []; + hyprLuaReadProcess.currentFilePath = nextFile; + hyprLuaReadProcess.command = ["cat", nextFile]; + hyprLuaReadProcess.running = true; + } + + Process { + id: hyprLuaReadProcess + property string currentFilePath: "" + running: false + + stdout: SplitParser { + onRead: data => { + if (root.currentLines.length < 10000) root.currentLines.push(data); + } + } + + onExited: (exitCode, exitStatus) => { + if (exitCode === 0 && root.currentLines.length > 0) { + var currentCategory = null; + for (var i = 0; i < root.currentLines.length; i++) { + var line = root.currentLines[i]; + + // require("a.b.c") / require 'a/b/c' -> a/b/c.lua relative to current file. + // Lua module names use dots as path separators; literal paths (with /) and + // .lua suffixes are passed through unchanged. + var reqMatch = line.match(/require\s*\(?\s*["']([^"']+)["']/); + if (reqMatch) { + var mod = reqMatch[1]; + if (!mod.endsWith(".lua")) { + if (mod.indexOf("/") === -1) mod = mod.replace(/\./g, "/"); + mod = mod + ".lua"; + } + var resolved = root.resolveRelativePath(currentFilePath, mod); + if (!root.parsedFiles[resolved] && root.filesToParse.indexOf(resolved) === -1) { + root.filesToParse.push(resolved); + } + } + + // Category header: -- 1. NAME + var headMatch = line.match(/^\s*--\s*\d+\.\s*(.+?)\s*$/); + if (headMatch) { + currentCategory = headMatch[1].trim(); + if (root.luaCategoryHeaders.indexOf(currentCategory) === -1) { + root.luaCategoryHeaders.push(currentCategory); + } + continue; + } + + // description = "..." / desc = '...' + var dMatch = line.match(/(?:description|desc)\s*=\s*["']([^"']*)["']/); + if (dMatch && currentCategory) { + var lit = dMatch[1]; + if (line.indexOf("..") !== -1) { + // Concatenated/dynamic description (e.g. "Workspace " .. i) + var pfx = lit.trim(); + if (pfx.length > 0) root.prefixToCategory.push({ prefix: pfx, category: currentCategory }); + } else if (lit.length > 0 && root.descToCategory[lit] === undefined) { + root.descToCategory[lit] = currentCategory; + } + } + } + } + root.currentLines = []; + root.parseNextHyprLuaFile(); + } + } + + function runHyprctlBinds() { + hyprctlChunks = []; + hyprctlBindsProcess.command = ["hyprctl", "binds", "-j"]; + hyprctlBindsProcess.running = true; + } + + Process { + id: hyprctlBindsProcess + running: false + + stdout: SplitParser { + onRead: data => { + if (root.hyprctlChunks.length < 20000) root.hyprctlChunks.push(data); + } + } + + onExited: (exitCode, exitStatus) => { + if (exitCode !== 0 || root.hyprctlChunks.length === 0) { + Logger.e("keybind-cheatsheet", "hyprctl binds -j failed (exit " + exitCode + ")"); + root.hyprctlChunks = []; + root.saveToDb([{ + "title": root.pluginApi?.tr("error.unsupported-compositor"), + "binds": [{ "keys": "hyprctl", "desc": root.pluginApi?.tr("error.hyprctl-failed") }] + }]); + root.isCurrentlyParsing = false; + root.clearParsingData(); + return; + } + + var binds = []; + try { + binds = JSON.parse(root.hyprctlChunks.join("\n")); + } catch (e) { + Logger.e("keybind-cheatsheet", "hyprctl JSON parse failed: " + e); + binds = []; + } + root.hyprctlChunks = []; + root.buildCategoriesFromHyprctl(binds); + } + } + + // Hyprland modifier bitmask (see HL_MODIFIER_*) + function decodeModmask(mask) { + var m = []; + if (mask & 64) m.push("Super"); // LOGO / SUPER / META + if (mask & 1) m.push("Shift"); + if (mask & 4) m.push("Ctrl"); + if (mask & 8) m.push("Alt"); + if (mask & 16) m.push("Mod2"); + if (mask & 32) m.push("Mod3"); + if (mask & 128) m.push("Mod5"); + return m; + } + + function computeBindId(b) { + // `arg` is an unstable lua registry ref for __lua binds — exclude it there. + var disp = (b.dispatcher === "__lua") ? "__lua" : (b.dispatcher + ":" + (b.arg || "")); + var flags = (b.release ? 1 : 0) + "" + (b.mouse ? 1 : 0) + (b.longPress ? 1 : 0); + return [b.submap || "", b.modmask, b.key, flags, disp].join("|"); + } + + function categoryForDesc(desc) { + if (descToCategory[desc] !== undefined) return descToCategory[desc]; + for (var i = 0; i < prefixToCategory.length; i++) { + if (desc.indexOf(prefixToCategory[i].prefix) === 0) return prefixToCategory[i].category; + } + return null; + } + + function buildCategoriesFromHyprctl(binds) { + var overrides = pluginApi?.pluginSettings?.bindOverrides || ({}); + var showUndescribed = pluginApi?.pluginSettings?.showUndescribedBinds ?? true; + + var byCat = ({}); + var otherTitle = pluginApi?.tr("panel.other"); + var undescTitle = pluginApi?.tr("panel.undescribed"); + + for (var i = 0; i < binds.length; i++) { + var b = binds[i]; + if (!b || b.key === undefined) continue; + + var bindId = computeBindId(b); + var ov = overrides[bindId]; + if (ov && ov.hidden === true) continue; + + var rawDesc = (b.has_description && b.description) ? b.description : ""; + if (ov && ov.desc) rawDesc = ov.desc; + var undescribed = (rawDesc === ""); + + if (undescribed && !showUndescribed) continue; + + var keyName = formatSpecialKey(b.key); + if (keyName === b.key) keyName = formatSpecialKey(String(b.key).toUpperCase()); + var mods = decodeModmask(b.modmask); + var fullKey = mods.length > 0 ? (mods.join(" + ") + " + " + keyName) : keyName; + + var cat; + if (undescribed) { + cat = undescTitle; + } else { + cat = categoryForDesc(rawDesc) || otherTitle; + } + + if (!byCat[cat]) byCat[cat] = []; + byCat[cat].push({ + "keys": fullKey, + "desc": undescribed ? "" : rawDesc, + "bindId": bindId, + "undescribed": undescribed, + // Decomposed fields so mergeSequentialBindsInCategories() can collapse + // runs of consecutive numeric binds (e.g. Super+1..9). __lua binds share + // an empty verb, which is fine — descTemplate still disambiguates them. + "_mainKey": String(b.key), + "_mods": mods.join("+"), + "_verb": "" + }); + } + + // Emit categories in lua header order, then Other, then Undescribed last. + var categories = []; + for (var h = 0; h < luaCategoryHeaders.length; h++) { + var t = luaCategoryHeaders[h]; + if (byCat[t] && byCat[t].length > 0) { + categories.push({ "title": t, "binds": byCat[t] }); + delete byCat[t]; + } + } + if (byCat[otherTitle] && byCat[otherTitle].length > 0) { + categories.push({ "title": otherTitle, "binds": byCat[otherTitle] }); + delete byCat[otherTitle]; + } + var undesc = byCat[undescTitle]; + if (undesc && undesc.length > 0) delete byCat[undescTitle]; + // Any leftover categories (shouldn't normally happen) + for (var k in byCat) { + if (byCat[k] && byCat[k].length > 0) categories.push({ "title": k, "binds": byCat[k] }); + } + if (undesc && undesc.length > 0) { + categories.push({ "title": undescTitle, "binds": undesc }); + } + + // Apply the "merge sequential" setting here too — the hyprctl path previously + // skipped it, so the toggle had no effect for Hyprland (hyprctl-sourced) binds. + if (pluginApi?.pluginSettings?.mergeSequentialBinds ?? true) + categories = mergeSequentialBindsInCategories(categories); + saveToDb(categories); + isCurrentlyParsing = false; + clearParsingData(); + } + + // ========== HYPRLAND RECURSIVE PARSING ========== + function parseNextHyprlandFile() { + if (parseDepthCounter >= maxParseDepth) { + Logger.w("KeybindCheatsheet", "Hyprland parser hit max recursion depth (" + maxParseDepth + "), aborting"); + isCurrentlyParsing = false; + clearParsingData(); + return; + } + parseDepthCounter++; + + if (filesToParse.length === 0) { + if (accumulatedLines.length > 0) { + parseHyprlandConfig(accumulatedLines.join("\n")); + } else { + isCurrentlyParsing = false; + } + return; + } + + var nextFile = filesToParse.shift(); + + // Handle glob patterns + if (isGlobPattern(nextFile)) { + hyprGlobProcess.expandedFiles = []; // Clear previous results + hyprGlobProcess.command = ["sh", "-c", 'for f in $1; do [ -f "$f" ] && echo "$f"; done', "sh", nextFile]; + hyprGlobProcess.running = true; + return; + } + + if (parsedFiles[nextFile]) { + parseNextHyprlandFile(); + return; + } + + parsedFiles[nextFile] = true; + currentLines = []; + hyprReadProcess.currentFilePath = nextFile; + hyprReadProcess.command = ["cat", nextFile]; + hyprReadProcess.running = true; + } + + Process { + id: hyprGlobProcess + property var expandedFiles: [] + running: false + + stdout: SplitParser { + onRead: data => { + var trimmed = data.trim(); + if (trimmed.length > 0) { + if (hyprGlobProcess.expandedFiles.length < 100) { + hyprGlobProcess.expandedFiles.push(trimmed); + } + } + } + } + + onExited: { + for (var i = 0; i < expandedFiles.length; i++) { + var path = expandedFiles[i]; + if (!root.parsedFiles[path] && root.filesToParse.indexOf(path) === -1) { + root.filesToParse.push(path); + } + } + expandedFiles = []; + root.parseNextHyprlandFile(); + } + } + + Process { + id: hyprReadProcess + property string currentFilePath: "" + running: false + + stdout: SplitParser { + onRead: data => { + if (root.currentLines.length < 10000) { + root.currentLines.push(data); + } + } + } + + onExited: (exitCode, exitStatus) => { + if (exitCode === 0 && root.currentLines.length > 0) { + for (var i = 0; i < root.currentLines.length; i++) { + var line = root.currentLines[i]; + root.accumulatedLines.push(line); + + // Check for source directive + var sourceMatch = line.trim().match(/^source\s*=\s*(.+)$/); + if (sourceMatch) { + var sourcePath = sourceMatch[1].trim(); + var commentIdx = sourcePath.indexOf('#'); + if (commentIdx > 0) sourcePath = sourcePath.substring(0, commentIdx).trim(); + var resolvedPath = root.resolveRelativePath(currentFilePath, sourcePath); + if (!root.parsedFiles[resolvedPath] && root.filesToParse.indexOf(resolvedPath) === -1) { + root.filesToParse.push(resolvedPath); + } + } + } + } + root.currentLines = []; + root.parseNextHyprlandFile(); + } + } + + // ========== HYPRLAND PARSER ========== + function parseHyprlandConfig(text) { + var lines = text.split('\n'); + var categories = []; + var currentCategory = null; + var hasCategories = false; // Track if we found any category headers + var skippedNoDesc = 0; + + var modVar = pluginApi?.pluginSettings?.modKeyVariable || "$mod"; + var modVarUpper = modVar.toUpperCase(); + var includeUndescribed = pluginApi?.pluginSettings?.showUndescribedBinds ?? false; + + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim(); + + // Category header: # 1. Category Name + if (line.startsWith("#") && line.match(/#\s*\d+\./)) { + hasCategories = true; // Found at least one category + if (currentCategory) { + categories.push(currentCategory); + } + var title = line.replace(/#\s*\d+\.\s*/, "").trim(); + currentCategory = { "title": title, "binds": [] }; + } + // Any bind directive (bind, bindm, binde, bindr, bindl, etc.) + else if (/^bind[a-z]*\s*=/.test(line)) { + var hasDesc = line.includes('#"'); + if (!hasDesc && !includeUndescribed) { + skippedNoDesc++; + continue; + } + + // If no categories found yet, create default category + if (!currentCategory && !hasCategories) { + var defaultCategoryName = pluginApi?.tr("default-category"); + currentCategory = { "title": defaultCategoryName, "binds": [] }; + } + + if (currentCategory) { + var description; + if (hasDesc) { + var descMatch = line.match(/#"(.*?)"$/); + description = descMatch ? descMatch[1] : "No description"; + } else { + description = ""; + } + + // Strip the trailing #"..." comment so it doesn't pollute parts splitting + var bindLine = hasDesc ? line.replace(/\s*#".*?"\s*$/, "") : line; + + var eqIdx = bindLine.indexOf("="); + if (eqIdx < 0) continue; + var rhs = bindLine.substring(eqIdx + 1); + var parts = rhs.split(','); + if (parts.length >= 2) { + var modPart = parts[0].trim().toUpperCase(); + var rawKey = parts[1].trim().toUpperCase(); + var key = formatSpecialKey(rawKey); + + var verb = parts.length >= 3 ? parts[2].trim().toLowerCase() : ""; + var param = parts.length >= 4 ? parts.slice(3).join(",").trim() : ""; + + // Build modifiers list properly + var mods = []; + if (modPart.includes(modVarUpper) || modPart.includes("SUPER")) mods.push("Super"); + if (modPart.includes("SHIFT")) mods.push("Shift"); + if (modPart.includes("CTRL") || modPart.includes("CONTROL")) mods.push("Ctrl"); + if (modPart.includes("ALT")) mods.push("Alt"); + + // Build full key string + var fullKey; + if (mods.length > 0) { + fullKey = mods.join(" + ") + " + " + key; + } else { + fullKey = key; + } + + // Auto-generate description for undescribed binds when included + if (!hasDesc && includeUndescribed) { + description = verb ? (verb + (param ? " " + param : "")) : pluginApi?.tr("panel.no-description"); + } + + currentCategory.binds.push({ + "keys": fullKey, + "desc": description, + "_verb": verb, + "_param": param, + "_mods": mods.join("+"), + "_mainKey": rawKey + }); + } + } + } + } + + if (currentCategory) { + categories.push(currentCategory); + } + + if (skippedNoDesc > 0) { + Logger.w("KeybindCheatsheet", "Skipped " + skippedNoDesc + " Hyprland binds without #\"description\" suffix (enable showUndescribedBinds to include)"); + } + + if (categories.length === 0 || categories.every(function(c) { return !c.binds || c.binds.length === 0; })) { + Logger.w("KeybindCheatsheet", "Hyprland parser produced no binds; check config path and #\"description\" annotations"); + } + + if (pluginApi?.pluginSettings?.mergeSequentialBinds ?? true) { + categories = mergeSequentialBindsInCategories(categories); + } + + saveToDb(categories); + isCurrentlyParsing = false; + clearParsingData(); + } + + function formatSpecialKey(key) { + var keyMap = { + // Audio keys (uppercase for Hyprland) + "XF86AUDIORAISEVOLUME": "Vol Up", + "XF86AUDIOLOWERVOLUME": "Vol Down", + "XF86AUDIOMUTE": "Mute", + "XF86AUDIOMICMUTE": "Mic Mute", + "XF86AUDIOPLAY": "Play", + "XF86AUDIOPAUSE": "Pause", + "XF86AUDIONEXT": "Next", + "XF86AUDIOPREV": "Prev", + "XF86AUDIOSTOP": "Stop", + "XF86AUDIOMEDIA": "Media", + // Audio keys (mixed case for Niri) + "XF86AudioRaiseVolume": "Vol Up", + "XF86AudioLowerVolume": "Vol Down", + "XF86AudioMute": "Mute", + "XF86AudioMicMute": "Mic Mute", + "XF86AudioPlay": "Play", + "XF86AudioPause": "Pause", + "XF86AudioNext": "Next", + "XF86AudioPrev": "Prev", + "XF86AudioStop": "Stop", + "XF86AudioMedia": "Media", + // Brightness keys + "XF86MONBRIGHTNESSUP": "Bright Up", + "XF86MONBRIGHTNESSDOWN": "Bright Down", + "XF86MonBrightnessUp": "Bright Up", + "XF86MonBrightnessDown": "Bright Down", + // Other common keys + "XF86CALCULATOR": "Calc", + "XF86MAIL": "Mail", + "XF86SEARCH": "Search", + "XF86EXPLORER": "Files", + "XF86WWW": "Browser", + "XF86HOMEPAGE": "Home", + "XF86FAVORITES": "Favorites", + "XF86POWEROFF": "Power", + "XF86SLEEP": "Sleep", + "XF86EJECT": "Eject", + // Print screen + "PRINT": "PrtSc", + "Print": "PrtSc", + // Navigation + "PRIOR": "PgUp", + "NEXT": "PgDn", + "Prior": "PgUp", + "Next": "PgDn", + // Mouse (for Hyprland) + "MOUSE_DOWN": "Scroll Down", + "MOUSE_UP": "Scroll Up", + "MOUSE:272": "Left Click", + "MOUSE:273": "Right Click", + "MOUSE:274": "Middle Click" + }; + return keyMap[key] || key; + } + + function formatNiriKeyCombo(combo) { + // Split by + and process each part + var parts = combo.split("+"); + var formattedParts = []; + + for (var i = 0; i < parts.length; i++) { + var part = parts[i].trim(); + if (part.length === 0) continue; + + // Map modifier names + if (part === "Mod" || part === "Super" || part === "Win") { + formattedParts.push("Super"); + } else if (part === "Ctrl" || part === "Control") { + formattedParts.push("Ctrl"); + } else if (part === "Alt") { + formattedParts.push("Alt"); + } else if (part === "Shift") { + formattedParts.push("Shift"); + } else { + // It's a key - format special keys + formattedParts.push(formatSpecialKey(part)); + } + } + + return formattedParts.join(" + "); + } + + // Map Noctalia IPC target+function to human-readable descriptions + property var noctaliaIpcLabels: ({ + "launcher toggle": "Launcher", + "launcher clipboard": "Clipboard History", + "launcher command": "Command Palette", + "launcher emoji": "Emoji Picker", + "launcher windows": "Window Switcher", + "launcher settings": "Launcher Settings", + "controlCenter toggle": "Control Center", + "settings toggle": "Settings", + "settings open": "Open Settings", + "sessionMenu toggle": "Session Menu", + "sessionMenu lock": "Lock & Session Menu", + "lockScreen lock": "Lock Screen", + "volume increase": "Volume Up", + "volume decrease": "Volume Down", + "volume muteOutput": "Mute Output", + "volume muteInput": "Mute Input", + "volume increaseInput": "Input Volume Up", + "volume decreaseInput": "Input Volume Down", + "brightness increase": "Brightness Up", + "brightness decrease": "Brightness Down", + "media playPause": "Play / Pause", + "media next": "Next Track", + "media previous": "Previous Track", + "media play": "Play", + "media pause": "Pause", + "media stop": "Stop", + "media toggle": "Media Panel", + "notifications toggleDND": "Do Not Disturb", + "notifications toggleHistory": "Notification History", + "notifications clear": "Clear Notifications", + "notifications dismissAll": "Dismiss All", + "wallpaper refresh": "Refresh Wallpaper", + "wallpaper toggle": "Wallpaper Panel", + "wallpaper toggleAutomation": "Toggle Wallpaper Automation", + "darkMode toggle": "Toggle Dark Mode", + "darkMode setDark": "Dark Mode", + "darkMode setLight": "Light Mode", + "nightLight toggle": "Toggle Night Light", + "dock toggle": "Toggle Dock", + "bar toggle": "Toggle Bar", + "desktopWidgets toggle": "Toggle Desktop Widgets", + "desktopWidgets edit": "Edit Desktop Widgets", + "calendar toggle": "Calendar", + "systemMonitor toggle": "System Monitor", + "idleInhibitor toggle": "Toggle Idle Inhibitor", + "monitors off": "Monitors Off", + "monitors on": "Monitors On", + "wifi toggle": "Toggle WiFi", + "bluetooth toggle": "Toggle Bluetooth", + "airplaneMode toggle": "Toggle Airplane Mode", + "powerProfile cycle": "Cycle Power Profile", + "battery togglePanel": "Battery Panel", + "network togglePanel": "Network Panel" + }) + + function formatNiriAction(action) { + // Detect Noctalia IPC commands in two formats: + // 1. spawn-sh "qs -c noctalia-shell ipc call target function" + // 2. spawn "qs" "-c" "noctalia-shell" "ipc" "call" "target" "function" + if (action.indexOf("noctalia-shell") !== -1 && action.indexOf("ipc") !== -1) { + // Extract target and function from either format: + // spawn-sh: ipc call target function (words separated by spaces) + // spawn multi-arg: "ipc" "call" "target" "function" (words wrapped in quotes) + var ipcMatch = action.match(/ipc\s+call\s+(\w+)\s+(\w+)/) || + action.match(/"ipc"\s+"call"\s+"(\w+)"\s+"(\w+)"/); + if (ipcMatch) { + var ipcKey = ipcMatch[1] + " " + ipcMatch[2]; + if (noctaliaIpcLabels[ipcKey]) { + return noctaliaIpcLabels[ipcKey]; + } + // Fallback: format target + function nicely + return ipcMatch[1].replace(/([A-Z])/g, ' $1').trim() + ": " + + ipcMatch[2].replace(/([A-Z])/g, ' $1').trim(); + } + } + + // Handle spawn and spawn-sh commands + if (action.startsWith("spawn")) { + var spawnMatch = action.match(/spawn(?:-sh)?\s+"([^"]+)"/); + if (spawnMatch) { + return "Run: " + spawnMatch[1]; + } + return action; + } + // Format action name: focus-column-left -> Focus Column Left + return action.replace(/-/g, ' ').replace(/\b\w/g, function(l) { return l.toUpperCase(); }); + } + + function getNiriCategory(action, actionCategories) { + // Noctalia IPC commands get their own category + if (action.indexOf("noctalia-shell") !== -1 && action.indexOf("ipc") !== -1) { + return "Noctalia"; + } + for (var prefix in actionCategories) { + if (action.startsWith(prefix)) { + return actionCategories[prefix]; + } + } + return "Other"; + } + + function saveToDb(data) { + if (pluginApi) { + var compositor = getCurrentCompositor(); + pluginApi.pluginSettings.cheatsheetData = data; + pluginApi.pluginSettings.detectedCompositor = compositor; + pluginApi.saveSettings(); + // Bug 4 fix: bump version counter so Panel.qml re-evaluates its data binding + cheatsheetDataVersion++; + } + } + + // ========== MANGO LOOKUP TABLES ========== + readonly property var mangoKeyNameMap: ({ + "Return": "Enter", "return": "Enter", + "equal": "=", "minus": "-", "plus": "+", + "space": "Space", "comma": ",", "period": ".", + "semicolon": ";", "apostrophe": "'", "grave": "`", + "slash": "/", "backslash": "\\", + "bracketleft": "[", "bracketright": "]", + "Escape": "Esc", "escape": "Esc" + }) + + readonly property var mangoAxisMap: ({ + "UP": "Scroll Up", "DOWN": "Scroll Down", + "LEFT": "Scroll Left", "RIGHT": "Scroll Right" + }) + + readonly property var mangoButtonMap: ({ + "BTN_LEFT": "Left Click", "BTN_RIGHT": "Right Click", + "BTN_MIDDLE": "Middle Click", "BTN_SIDE": "Mouse Side", + "BTN_EXTRA": "Mouse Extra" + }) + + readonly property var mangoNoArgActions: ({ + "killclient": "Close window", + "togglefullscreen": "Toggle fullscreen", + "togglemaximizescreen": "Maximize", + "togglefloating": "Toggle floating", + "reload_config": "Reload config", + "toggleoverview": "Toggle overview", + "quit": "Quit compositor", + "switch_proportion_preset": "Cycle column width", + "switch_keyboard_layout": "Switch keyboard layout", + "zoom": "Zoom", + "restart": "Restart", + "incnmaster": "Increase masters", + "switch_layout": "Switch layout" + }) + + readonly property var mangoDirActions: ({ + "focusdir": "Focus", + "exchange_client": "Swap", + "focusmon": "Focus Monitor", + "tagmon": "Send to Monitor" + }) + + // ========== MANGO RECURSIVE PARSING ========== + function parseNextMangoFile() { + if (parseDepthCounter >= maxParseDepth) { + Logger.w("KeybindCheatsheet", "Mango parser hit max recursion depth (" + maxParseDepth + "), aborting"); + isCurrentlyParsing = false; + clearParsingData(); + return; + } + parseDepthCounter++; + + if (filesToParse.length === 0) { + finalizeMangoBinds(); + return; + } + + var nextFile = filesToParse.shift(); + + // Handle glob patterns + if (isGlobPattern(nextFile)) { + mangoGlobProcess.expandedFiles = []; + mangoGlobProcess.command = ["sh", "-c", 'for f in $1; do [ -f "$f" ] && echo "$f"; done', "sh", nextFile]; + mangoGlobProcess.running = true; + return; + } + + if (parsedFiles[nextFile]) { + parseNextMangoFile(); + return; + } + + parsedFiles[nextFile] = true; + currentLines = []; + mangoReadProcess.currentFilePath = nextFile; + mangoReadProcess.command = ["cat", nextFile]; + mangoReadProcess.running = true; + } + + Process { + id: mangoGlobProcess + property var expandedFiles: [] + running: false + + stdout: SplitParser { + onRead: data => { + var trimmed = data.trim(); + if (trimmed.length > 0) { + if (mangoGlobProcess.expandedFiles.length < 100) { + mangoGlobProcess.expandedFiles.push(trimmed); + } + } + } + } + + onExited: { + for (var i = 0; i < expandedFiles.length; i++) { + var path = expandedFiles[i]; + if (!root.parsedFiles[path] && root.filesToParse.indexOf(path) === -1) { + root.filesToParse.push(path); + } + } + expandedFiles = []; + root.parseNextMangoFile(); + } + } + + Process { + id: mangoReadProcess + property string currentFilePath: "" + running: false + + stdout: SplitParser { + onRead: data => { + if (root.currentLines.length < 10000) { + root.currentLines.push(data); + } + } + } + + onExited: (exitCode, exitStatus) => { + if (exitCode === 0 && root.currentLines.length > 0) { + // First pass: find source/source-optional includes + for (var i = 0; i < root.currentLines.length; i++) { + var line = root.currentLines[i].trim(); + // Skip commented lines + var commentPos = root.findMangoUnquotedComment(line); + var effectiveLine = commentPos >= 0 ? line.substring(0, commentPos).trim() : line; + + var srcMatch = effectiveLine.match(/^source-optional\s*=\s*(.+)$/) || + effectiveLine.match(/^source\s*=\s*(.+)$/); + if (srcMatch) { + var srcPath = srcMatch[1].trim(); + var resolvedSrc = root.resolveRelativePath(currentFilePath, srcPath); + if (isGlobPattern(resolvedSrc)) { + if (root.filesToParse.indexOf(resolvedSrc) === -1) { + root.filesToParse.push(resolvedSrc); + } + } else if (!root.parsedFiles[resolvedSrc] && root.filesToParse.indexOf(resolvedSrc) === -1) { + root.filesToParse.push(resolvedSrc); + } + } + } + // Second pass: parse keybinds from this file + root.parseMangoConfig(root.currentLines.join("\n")); + } + root.currentLines = []; + root.parseNextMangoFile(); + } + } + + function findMangoUnquotedComment(str) { + var inSingle = false; + var inDouble = false; + for (var i = 0; i < str.length; i++) { + var ch = str[i]; + if (ch === "'" && !inDouble) { inSingle = !inSingle; continue; } + if (ch === '"' && !inSingle) { inDouble = !inDouble; continue; } + if (ch === '#' && !inSingle && !inDouble) { + // Per-bind description suffix: #"description" — not a comment + if (str.charAt(i + 1) === '"') continue; + return i; + } + } + return -1; + } + + function parseMangoConfig(text) { + var lines = text.split('\n'); + var currentCategory = null; + var defaultCat = pluginApi?.tr("default-category"); + + for (var i = 0; i < lines.length; i++) { + var rawLine = lines[i]; + var commentPos = findMangoUnquotedComment(rawLine); + var effectiveLine = commentPos >= 0 ? rawLine.substring(0, commentPos).trim() : rawLine.trim(); + + // Pure-comment lines: only candidate for category detection (# Title) + if (effectiveLine.length === 0) { + var categoryCandidate = extractMangoCategory(rawLine); + if (categoryCandidate !== null) { + currentCategory = categoryCandidate; + } + continue; + } + + // Extract optional per-bind description: trailing #"description" + var perBindDesc = null; + var descMatch = effectiveLine.match(/#"([^"]*)"\s*$/); + if (descMatch) { + perBindDesc = descMatch[1]; + effectiveLine = effectiveLine.substring(0, descMatch.index).trim(); + if (effectiveLine.length === 0) continue; + } + + // bind=MODS,KEY,ACTION,ARGS (also `binds=` — keysym-based variant + // used by layout-aware setups like AZERTY) + var bindMatch = effectiveLine.match(/^binds?\s*=\s*(.+)$/); + if (bindMatch) { + var parts = bindMatch[1].split(','); + if (parts.length >= 3) { + var modStr = parts[0].trim(); + var key = parts[1].trim(); + var action = parts[2].trim(); + var args = parts.length >= 4 ? parts.slice(3).join(',').trim() : ""; + var cat = currentCategory || defaultCat; + var formattedKeys = formatMangoKeyCombo(modStr, key, "bind"); + var description = perBindDesc || formatMangoAction(action, args); + + if (!collectedBinds[cat]) collectedBinds[cat] = []; + collectedBinds[cat].push({ "keys": formattedKeys, "desc": description }); + } + continue; + } + + // axisbind=MODS,AXIS,ACTION,ARGS (also `axisbinds=` keysym variant) + var axisMatch = effectiveLine.match(/^axisbinds?\s*=\s*(.+)$/); + if (axisMatch) { + var axisParts = axisMatch[1].split(','); + if (axisParts.length >= 3) { + var aModStr = axisParts[0].trim(); + var axis = axisParts[1].trim().toUpperCase(); + var aAction = axisParts[2].trim(); + var aArgs = axisParts.length >= 4 ? axisParts.slice(3).join(',').trim() : ""; + var aCat = currentCategory || defaultCat; + var aKey = mangoAxisMap[axis] || axis; + var aCombo = formatMangoKeyCombo(aModStr, aKey, "axis"); + var aDesc = perBindDesc || formatMangoAction(aAction, aArgs); + + if (!collectedBinds[aCat]) collectedBinds[aCat] = []; + collectedBinds[aCat].push({ "keys": aCombo, "desc": aDesc }); + } + continue; + } + + // mousebind=MODS,BTN,ACTION,ARGS (also `mousebinds=` keysym variant) + var mouseMatch = effectiveLine.match(/^mousebinds?\s*=\s*(.+)$/); + if (mouseMatch) { + var mouseParts = mouseMatch[1].split(','); + if (mouseParts.length >= 3) { + var mModStr = mouseParts[0].trim(); + var btn = mouseParts[1].trim().toUpperCase(); + var mAction = mouseParts[2].trim(); + var mArgs = mouseParts.length >= 4 ? mouseParts.slice(3).join(',').trim() : ""; + var mCat = currentCategory || defaultCat; + var mKey = mangoButtonMap[btn] || btn; + var mCombo = formatMangoKeyCombo(mModStr, mKey, "mouse"); + var mDesc = perBindDesc || formatMangoAction(mAction, mArgs); + + if (!collectedBinds[mCat]) collectedBinds[mCat] = []; + collectedBinds[mCat].push({ "keys": mCombo, "desc": mDesc }); + } + } + } + } + + function extractMangoCategory(line) { + var trimmed = line.trim(); + // A category line is a standalone comment: # Category Name + if (!trimmed.startsWith('#')) return null; + // Strip leading hashes (#, ##, ###, ...) + var rest = trimmed.replace(/^#+/, '').trim(); + if (rest.length === 0) return null; + + // Length cap — categories shouldn't be paragraphs ("# TODO fix later" etc.) + if (rest.length > 100) return null; + + // Pure horizontal rule (only decorative chars): ────, ====, ----, **** + if (/^[─━═=\-_*#/\\\s]+$/.test(rest)) return null; + + // Reject paren/bracket continuation: "(continued)", "(see above)", "[note]" + if (/^[\(\[]/.test(rest)) return null; + + // Reject flow arrows: "→ next step", "->", "=>" + if (/^[→←↑↓⇒⇐➜➔►▶]/.test(rest)) return null; + if (rest.indexOf('->') === 0 || rest.indexOf('=>') === 0) return null; + + // Reject keyword-prefixed notes: "# TODO ...", "# FIXME ...", "# NOTE ..." + if (/^(TODO|FIXME|NOTE|HACK|XXX|BUG|WIP)\b/i.test(rest)) return null; + + // Strip trailing horizontal-rule decoration: "Category ────" -> "Category" + rest = rest.replace(/[─━═=\-_*]{3,}\s*$/, '').trim(); + if (rest.length === 0) return null; + + // Numbered list extraction: "1. Foo" -> "Foo" + var numbered = rest.match(/^\d+\.\s*(.+)$/); + if (numbered) return numbered[1].trim(); + + return rest; + } + + function formatMangoKeyCombo(modStr, key, kind) { + var mods = []; + var modUpper = modStr.toUpperCase(); + // SUPER aliases: SUPER, LOGO + if (modUpper.indexOf("SUPER") !== -1 || modUpper.indexOf("LOGO") !== -1) mods.push("Super"); + if (modUpper.indexOf("SHIFT") !== -1) mods.push("Shift"); + if (modUpper.indexOf("CTRL") !== -1 || modUpper.indexOf("CONTROL") !== -1) mods.push("Ctrl"); + // ALT aliases: ALT, MOD1 + if (modUpper.indexOf("ALT") !== -1 || modUpper.indexOf("MOD1") !== -1) mods.push("Alt"); + + // Map the key name. For bind keys, prefer XF86 special-key formatting (Vol Up, Mute, ...). + var mappedKey; + if (kind === "bind") { + var specialMapped = formatSpecialKey(key); + mappedKey = (specialMapped !== key) ? specialMapped : (mangoKeyNameMap[key] || key); + } else { + mappedKey = key; + } + + if (mods.length > 0) { + return mods.join(" + ") + " + " + mappedKey; + } + return mappedKey; + } + + function formatMangoAction(action, args) { + // Check Noctalia IPC calls: spawn_shell with qs ipc call ... + if (args.indexOf("noctalia-shell") !== -1 && args.indexOf("ipc") !== -1) { + var ipcMatch = args.match(/ipc\s+call\s+(\w+)\s+(\w+)/); + if (ipcMatch) { + var ipcKey = ipcMatch[1] + " " + ipcMatch[2]; + if (noctaliaIpcLabels[ipcKey]) return noctaliaIpcLabels[ipcKey]; + return ipcMatch[1] + ": " + ipcMatch[2]; + } + } + + // No-arg actions + if (mangoNoArgActions[action]) return mangoNoArgActions[action]; + + // Direction actions + if (mangoDirActions[action]) { + var dir = args ? args.toUpperCase() : ""; + return mangoDirActions[action] + (dir ? " " + dir : ""); + } + + // spawn / spawn_shell + if (action === "spawn" || action === "spawn_shell") { + return args ? "Run: " + args : "Run command"; + } + + // workspace / move to workspace + if (action === "workspace") return args ? "Workspace " + args : "Switch workspace"; + if (action === "movetoworkspace") return args ? "Move to workspace " + args : "Move to workspace"; + if (action === "movetoworkspacesilent") return args ? "Move (silent) to workspace " + args : "Move to workspace"; + + // Generic fallback: convert snake_case / camelCase to readable + return action.replace(/_/g, ' ').replace(/([A-Z])/g, ' $1').trim() + .replace(/\b\w/g, function(l) { return l.toUpperCase(); }); + } + + function finalizeMangoBinds() { + var categories = []; + for (var cat in collectedBinds) { + if (collectedBinds[cat].length > 0) { + categories.push({ "title": cat, "binds": collectedBinds[cat] }); + } + } + + if (categories.length === 0) { + Logger.w("KeybindCheatsheet", "Mango parser produced no binds; check config path and bind= directives"); + } + + saveToDb(categories); + isCurrentlyParsing = false; + clearParsingData(); + } + + IpcHandler { + target: "plugin:keybind-cheatsheet" + + function toggle() { + if (root.pluginApi) { + root.pluginApi.withCurrentScreen(screen => { + root.pluginApi.togglePanel(screen); + }); + } + } + + function refresh() { + root.refresh(); + } + } +} diff --git a/noctalia/.config/plugins/keybind-cheatsheet/Panel.qml b/noctalia/.config/plugins/keybind-cheatsheet/Panel.qml new file mode 100644 index 0000000..946431e --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/Panel.qml @@ -0,0 +1,687 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.UI +import qs.Services.Compositor +import qs.Widgets + +Item { + id: root + property var pluginApi: null + + // Settings + property var cfg: pluginApi?.pluginSettings || ({}) + property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({}) + + // Settings values + property int settingsWidth: cfg.windowWidth ?? defaults.windowWidth ?? 1400 + property int settingsHeight: cfg.windowHeight ?? defaults.windowHeight ?? 0 + property bool autoHeight: cfg.autoHeight ?? defaults.autoHeight ?? true + property int columnCount: cfg.columnCount ?? defaults.columnCount ?? 3 + + // Bug 4 fix: re-evaluate rawCategories whenever Main.qml increments cheatsheetDataVersion + property int _dataVersion: pluginApi?.mainInstance?.cheatsheetDataVersion ?? 0 + property var rawCategories: { + var _v = _dataVersion; // force QML dependency on version counter + return pluginApi?.pluginSettings?.cheatsheetData || []; + } + property var categories: [] + + // Bug 5 fix: timeout if parsing never completes + property bool loadingTimedOut: false + + Timer { + id: loadingTimeoutTimer + interval: 4000 + repeat: false + running: false + onTriggered: { + if (root.isLoading) { + root.loadingTimedOut = true; + } + } + } + + Component.onCompleted: { + categories = processCategories(rawCategories); + // Start timeout timer if we have no data yet + if (root.isLoading) { + loadingTimeoutTimer.start(); + } + } + + + // Dynamic column items (up to 4 columns) + property var columnItems: [] + + // Memory leak prevention: debounce column updates + Timer { + id: columnUpdateDebounce + interval: 100 + repeat: false + onTriggered: updateColumnItemsNow() + } + + Component.onDestruction: { + // Stop timer to prevent firing after destruction + columnUpdateDebounce.stop(); + loadingTimeoutTimer.stop(); + + // Clear column items + columnItems = []; + } + + onRawCategoriesChanged: { + categories = processCategories(rawCategories); + updateColumnItems(); + } + + onCategoriesChanged: { + updateColumnItems(); + contentPreferredHeight = calculateDynamicHeight(); + } + + onColumnCountChanged: { + updateColumnItems(); + contentPreferredHeight = calculateDynamicHeight(); + } + + onPanelOpenScreenChanged: { + // Recalculate height when screen becomes available (important for bar widget opening) + contentPreferredHeight = calculateDynamicHeight(); + root.searchText = ""; + if (searchInput) searchInput.inputItem.forceActiveFocus(); + } + + onMaxScreenHeightChanged: { + contentPreferredHeight = calculateDynamicHeight(); + } + + function updateColumnItems() { + columnUpdateDebounce.restart(); + } + + function updateColumnItemsNow() { + columnItems = []; // Clear old items explicitly + var assignments = distributeCategories(); + var items = []; + for (var i = 0; i < columnCount; i++) { + items.push(buildColumnItems(assignments[i] || [])); + } + columnItems = items; + } + + // Screen height limit (90% of screen) + property var panelOpenScreen: pluginApi?.panelOpenScreen + property real maxScreenHeight: panelOpenScreen ? panelOpenScreen.height * 0.9 : 800 + + property string searchText: "" + + property real contentPreferredWidth: settingsWidth + property real contentPreferredHeight: calculateDynamicHeight() + readonly property var geometryPlaceholder: panelContainer + readonly property bool allowAttach: false + readonly property bool panelAnchorHorizontalCenter: true + readonly property bool panelAnchorVerticalCenter: true + anchors.fill: parent + + // Key badge colors — read from settings with manifest defaults as fallback + readonly property color keyColorAlt: cfg.keyColorAlt || defaults.keyColorAlt || "#FF6B6B" + readonly property color keyColorXF86: cfg.keyColorXF86 || defaults.keyColorXF86 || "#4ECDC4" + readonly property color keyColorPrint: cfg.keyColorPrint || defaults.keyColorPrint || "#95E1D3" + readonly property color keyColorNumeric: cfg.keyColorNumeric || defaults.keyColorNumeric || "#A8DADC" + readonly property color keyColorMouse: cfg.keyColorMouse || defaults.keyColorMouse || "#F38181" + // Empty string = use theme color (mPrimary/mSecondary/mTertiary) + readonly property string keyColorSuperOverride: cfg.keyColorSuper ?? defaults.keyColorSuper ?? "" + readonly property string keyColorCtrlOverride: cfg.keyColorCtrl ?? defaults.keyColorCtrl ?? "" + readonly property string keyColorShiftOverride: cfg.keyColorShift ?? defaults.keyColorShift ?? "" + readonly property color keyColorDefault: cfg.keyColorDefault || defaults.keyColorDefault || "#6C757D" + readonly property color keyLabelColor: cfg.keyLabelColor || defaults.keyLabelColor || "#FFFFFF" + // Empty string = theme-aware fallback (Color.mOnSurface). Any non-empty + // value is treated as an explicit user override. + readonly property string descriptionColorOverride: cfg.descriptionTextColor || defaults.descriptionTextColor || "" + readonly property color descriptionTextColor: descriptionColorOverride !== "" ? descriptionColorOverride : Color.mOnSurface + + // Per-category text color overrides (empty = fall back to keyLabelColor) + readonly property string keyTextSuperOverride: cfg.keyTextSuper ?? defaults.keyTextSuper ?? "" + readonly property string keyTextCtrlOverride: cfg.keyTextCtrl ?? defaults.keyTextCtrl ?? "" + readonly property string keyTextShiftOverride: cfg.keyTextShift ?? defaults.keyTextShift ?? "" + readonly property string keyTextAltOverride: cfg.keyTextAlt ?? defaults.keyTextAlt ?? "" + readonly property string keyTextXF86Override: cfg.keyTextXF86 ?? defaults.keyTextXF86 ?? "" + readonly property string keyTextPrintOverride: cfg.keyTextPrint ?? defaults.keyTextPrint ?? "" + readonly property string keyTextNumericOverride: cfg.keyTextNumeric ?? defaults.keyTextNumeric ?? "" + readonly property string keyTextMouseOverride: cfg.keyTextMouse ?? defaults.keyTextMouse ?? "" + readonly property string keyTextDefaultOverride: cfg.keyTextDefault ?? defaults.keyTextDefault ?? "" + + // The generic X11 mods (Mod2/Mod3/Mod5) are first-class, customizable keys: + // background colour, text colour, and a display-label override that renames + // them to something meaningful (e.g. Mod3 -> "Hyper" for caps:hyper). Empty + // colour = fall back to the default key colour; empty label = raw mod name. + readonly property string keyColorMod2Override: cfg.keyColorMod2 ?? defaults.keyColorMod2 ?? "" + readonly property string keyColorMod3Override: cfg.keyColorMod3 ?? defaults.keyColorMod3 ?? "" + readonly property string keyColorMod5Override: cfg.keyColorMod5 ?? defaults.keyColorMod5 ?? "" + readonly property string keyTextMod2Override: cfg.keyTextMod2 ?? defaults.keyTextMod2 ?? "" + readonly property string keyTextMod3Override: cfg.keyTextMod3 ?? defaults.keyTextMod3 ?? "" + readonly property string keyTextMod5Override: cfg.keyTextMod5 ?? defaults.keyTextMod5 ?? "" + readonly property string keyLabelMod2Override: cfg.keyLabelMod2 ?? defaults.keyLabelMod2 ?? "" + readonly property string keyLabelMod3Override: cfg.keyLabelMod3 ?? defaults.keyLabelMod3 ?? "" + readonly property string keyLabelMod5Override: cfg.keyLabelMod5 ?? defaults.keyLabelMod5 ?? "" + + // Workspace category split tuning + readonly property bool splitWorkspaces: cfg.splitLargeWorkspaceCategory ?? defaults.splitLargeWorkspaceCategory ?? true + readonly property int workspaceSplitThreshold: cfg.workspaceSplitThreshold ?? defaults.workspaceSplitThreshold ?? 12 + + // Data is loaded by Main.qml, we just display it + property bool isLoading: rawCategories.length === 0 + + function calculateDynamicHeight() { + // If auto height is disabled, use manual height (but still respect screen limit) + if (!autoHeight && settingsHeight > 0) { + return Math.min(settingsHeight, maxScreenHeight); + } + + if (categories.length === 0) return Math.min(400, maxScreenHeight); + + var assignments = distributeCategories(); + var maxColumnHeight = 0; + + for (var col = 0; col < columnCount; col++) { + var colHeight = 0; + var catIndices = assignments[col] || []; + + for (var i = 0; i < catIndices.length; i++) { + var catIndex = catIndices[i]; + if (catIndex >= categories.length) continue; + + var cat = categories[catIndex]; + colHeight += 26; // Header + colHeight += cat.binds.length * 20; // Binds + if (i < catIndices.length - 1) { + colHeight += 6; // Spacer + } + } + + if (colHeight > maxColumnHeight) { + maxColumnHeight = colHeight; + } + } + + // header (45) + content + margins (16) + var totalHeight = 45 + maxColumnHeight + 16 + 15 + 15; + // Limit to 80% of screen height + return Math.max(300, Math.min(totalHeight, maxScreenHeight)); + } + + // ========== UI ========== + Rectangle { + id: panelContainer + anchors.fill: parent + color: "transparent" + radius: Style.radiusL + clip: true + + Rectangle { + id: header + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + height: 45 + color: Color.mSurfaceVariant + radius: Style.radiusL + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Style.marginM + anchors.rightMargin: Style.marginM + spacing: Style.marginS + + // Title section (centered) + Item { Layout.fillWidth: true } + + NIcon { + icon: "keyboard" + pointSize: Style.fontSizeM + color: Color.mPrimary + } + NText { + text: CompositorService.isHyprland ? pluginApi?.tr("panel.title-hyprland") : + CompositorService.isNiri ? pluginApi?.tr("panel.title-niri") : + CompositorService.isMango ? pluginApi?.tr("panel.title-mango") : + pluginApi?.tr("panel.title") + font.pointSize: Style.fontSizeM + font.weight: Font.Bold + color: Color.mPrimary + } + + NTextInput { + id: searchInput + placeholderText: pluginApi?.tr("panel.search-placeholder") + text: root.searchText + + onTextChanged: { + root.searchText = text; + root.updateColumnItems(); + } + } + + Item { Layout.fillWidth: true } + + // Refresh button + NIconButton { + icon: "refresh" + onClicked: { + pluginApi?.mainInstance?.refresh(); + } + } + + // Settings button + NIconButton { + icon: "settings" + onClicked: { + var screen = pluginApi?.panelOpenScreen; + if (screen && pluginApi?.manifest) { + pluginApi.closePanel(screen); + BarService.openPluginSettings(screen, pluginApi.manifest); + } + } + } + } + } + + NText { + id: loadingText + anchors.centerIn: parent + text: root.loadingTimedOut ? pluginApi?.tr("panel.loading-timeout") : pluginApi?.tr("panel.loading") + visible: root.isLoading + font.pointSize: Style.fontSizeL + color: Color.mOnSurface + } + + NScrollView { + id: scrollView + visible: root.categories.length > 0 && !root.isLoading + anchors.top: header.bottom + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + clip: true + leftPadding: 35 + rightPadding: -10 + topPadding: 15 + bottomPadding: 15 + + RowLayout { + id: mainLayout + width: scrollView.availableWidth - Style.marginS + spacing: Style.marginS + + Repeater { + model: root.columnItems.length + + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + spacing: 2 + + property var colItems: root.columnItems[index] || [] + + Repeater { + model: colItems + Loader { + Layout.fillWidth: true + sourceComponent: modelData.type === "header" ? headerComponent : + (modelData.type === "spacer" ? spacerComponent : bindComponent) + property var itemData: modelData + + // Memory leak prevention: explicit cleanup + Component.onDestruction: { + active = false; + sourceComponent = undefined; + } + } + } + } + } + } + } + } + + Component { + id: headerComponent + ColumnLayout { + Layout.preferredWidth: 300 + Layout.topMargin: Style.marginM + Layout.bottomMargin: 4 + spacing: 0 + + Item { Layout.fillWidth: true; height: 1 } + + NText { + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + x: parent.width - implicitWidth + text: itemData.title + font.pointSize: Style.fontSizeM + font.weight: Font.Bold + color: Color.mPrimary + } + + Item { Layout.fillWidth: true; height: 1 } + } + } + + + Component { + id: spacerComponent + Item { + height: 10 + Layout.fillWidth: true + } + } + + Component { + id: bindComponent + RowLayout { + id: bindRow + spacing: Style.marginS + height: 22 + Layout.bottomMargin: 1 + + property bool editing: false + + Flow { + Layout.preferredWidth: 220 + Layout.alignment: Qt.AlignVCenter + spacing: 3 + Repeater { + model: itemData.keys.split(" + ") + Rectangle { + width: keyText.implicitWidth + 10 + height: 18 + color: getKeyColor(modelData) + radius: 3 + NText { + id: keyText + anchors.centerIn: parent + text: getKeyLabel(modelData) + font.pointSize: text.length > 12 ? 7 : 8 + font.weight: Font.Bold + color: getKeyTextColor(modelData) + } + } + } + } + + // Described bind: plain text (unchanged behaviour) + NText { + visible: !itemData.undescribed + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + text: itemData.desc + font.pointSize: Style.fontSizeXS + color: root.descriptionTextColor + elide: Text.ElideRight + } + + // Undescribed bind: placeholder + add-description / hide actions + NText { + visible: itemData.undescribed && !bindRow.editing + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + text: pluginApi?.tr("panel.no-description") + font.pointSize: Style.fontSizeXS + font.italic: true + color: Color.mOnSurfaceVariant + elide: Text.ElideRight + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { bindRow.editing = true; descInput.text = ""; descInput.inputItem.forceActiveFocus(); } + } + } + + NTextInput { + id: descInput + visible: itemData.undescribed && bindRow.editing + Layout.fillWidth: true + Layout.preferredHeight: Style.baseWidgetSize + placeholderText: pluginApi?.tr("panel.add-description-placeholder") + } + + NIconButton { + visible: itemData.undescribed && bindRow.editing + Layout.preferredHeight: 18 + icon: "check" + tooltipText: pluginApi?.tr("panel.save-description") + onClicked: { + root.saveBindDescription(itemData.bindId, descInput.text); + bindRow.editing = false; + } + } + + NIconButton { + visible: itemData.undescribed && bindRow.editing + Layout.preferredHeight: 18 + icon: "close" + tooltipText: pluginApi?.tr("panel.cancel") + onClicked: { bindRow.editing = false; } + } + + NIconButton { + visible: itemData.undescribed && !bindRow.editing + Layout.preferredHeight: 18 + icon: "edit" + tooltipText: pluginApi?.tr("panel.add-description") + onClicked: { bindRow.editing = true; descInput.text = ""; descInput.inputItem.forceActiveFocus(); } + } + + NIconButton { + visible: itemData.undescribed && !bindRow.editing + Layout.preferredHeight: 18 + icon: "eye-off" + tooltipText: pluginApi?.tr("panel.hide-bind") + onClicked: { root.hideBind(itemData.bindId); } + } + } + } + + function getKeyColor(keyName) { + if (keyName === "Super") return root.keyColorSuperOverride || Color.mPrimary; + if (keyName === "Ctrl") return root.keyColorCtrlOverride || Color.mSecondary; + if (keyName === "Shift") return root.keyColorShiftOverride || Color.mTertiary; + if (keyName === "Alt") return root.keyColorAlt; + if (keyName.startsWith("XF86")) return root.keyColorXF86; + if (keyName === "PRINT" || keyName === "Print") return root.keyColorPrint; + if (keyName.match(/^[0-9]$/)) return root.keyColorNumeric; + if (keyName.includes("MOUSE") || keyName.includes("Wheel")) return root.keyColorMouse; + if (keyName === "Mod2") return root.keyColorMod2Override || root.keyColorDefault; + if (keyName === "Mod3") return root.keyColorMod3Override || root.keyColorDefault; + if (keyName === "Mod5") return root.keyColorMod5Override || root.keyColorDefault; + return root.keyColorDefault; + } + + function getKeyTextColor(keyName) { + if (keyName === "Super") return root.keyTextSuperOverride || root.keyLabelColor; + if (keyName === "Ctrl") return root.keyTextCtrlOverride || root.keyLabelColor; + if (keyName === "Shift") return root.keyTextShiftOverride || root.keyLabelColor; + if (keyName === "Alt") return root.keyTextAltOverride || root.keyLabelColor; + if (keyName.startsWith("XF86")) return root.keyTextXF86Override || root.keyLabelColor; + if (keyName === "PRINT" || keyName === "Print") return root.keyTextPrintOverride || root.keyLabelColor; + if (keyName.match(/^[0-9]$/)) return root.keyTextNumericOverride || root.keyLabelColor; + if (keyName.includes("MOUSE") || keyName.includes("Wheel")) return root.keyTextMouseOverride || root.keyLabelColor; + if (keyName === "Mod2") return root.keyTextMod2Override || root.keyLabelColor; + if (keyName === "Mod3") return root.keyTextMod3Override || root.keyLabelColor; + if (keyName === "Mod5") return root.keyTextMod5Override || root.keyLabelColor; + return root.keyTextDefaultOverride || root.keyLabelColor; + } + + // Display label for a key token. Lets users rename the generic X11 modifier + // names (e.g. Mod3 -> "Hyper"); everything else renders as-is. Purely visual — + // colour lookup and the cached bind still use the original token. + function getKeyLabel(keyName) { + if (keyName === "Mod2" && root.keyLabelMod2Override) return root.keyLabelMod2Override; + if (keyName === "Mod3" && root.keyLabelMod3Override) return root.keyLabelMod3Override; + if (keyName === "Mod5" && root.keyLabelMod5Override) return root.keyLabelMod5Override; + return keyName; + } + + // ===== Bind override helpers (shared keyed map in plugin settings) ===== + function _cloneOverrides() { + if (!pluginApi || !pluginApi.pluginSettings) return ({}); + var src = pluginApi.pluginSettings.bindOverrides || ({}); + try { return JSON.parse(JSON.stringify(src)); } catch (e) { return ({}); } + } + + function saveBindDescription(bindId, desc) { + if (!pluginApi || !bindId) return; + var o = _cloneOverrides(); + if (!o[bindId]) o[bindId] = ({}); + var trimmed = (desc || "").trim(); + if (trimmed.length === 0) { + delete o[bindId].desc; + if (Object.keys(o[bindId]).length === 0) delete o[bindId]; + } else { + o[bindId].desc = trimmed; + } + pluginApi.pluginSettings.bindOverrides = o; + pluginApi.saveSettings(); + pluginApi.mainInstance?.refresh(); + } + + function hideBind(bindId) { + if (!pluginApi || !bindId) return; + var o = _cloneOverrides(); + if (!o[bindId]) o[bindId] = ({}); + o[bindId].hidden = true; + pluginApi.pluginSettings.bindOverrides = o; + pluginApi.saveSettings(); + pluginApi.mainInstance?.refresh(); + } + + function buildColumnItems(categoryIndices) { + var result = []; + if (!categoryIndices) return result; + + for (var i = 0; i < categoryIndices.length; i++) { + var catIndex = categoryIndices[i]; + if (catIndex >= categories.length) continue; + + var cat = categories[catIndex]; + result.push({ type: "header", title: cat.title }); + var term = root.searchText.toLowerCase(); + for (var j = 0; j < cat.binds.length; j++) { + var bnd = cat.binds[j]; + var isUndesc = bnd.undescribed === true; + if (!term || (bnd.desc && bnd.desc.toLowerCase().indexOf(term) !== -1) || (isUndesc && bnd.keys.toLowerCase().indexOf(term) !== -1)) { + result.push({ + type: "bind", + keys: bnd.keys, + desc: bnd.desc, + bindId: bnd.bindId || "", + undescribed: isUndesc + }); + } + } + if (i < categoryIndices.length - 1) { + result.push({ type: "spacer" }); + } + } + return result; + } + + function processCategories(cats) { + if (!cats || cats.length === 0) return []; + if (!root.splitWorkspaces) return cats; + + var result = []; + for (var i = 0; i < cats.length; i++) { + var cat = cats[i]; + if (!cat.binds || cat.binds.length <= root.workspaceSplitThreshold) { + result.push(cat); + continue; + } + + // Detect a category dominated by workspace verbs. + // Hyprland verbs: workspace, movetoworkspace, movetoworkspacesilent, movecurrentworkspacetomonitor + // Niri verbs: focus-workspace, move-window-to-workspace, move-column-to-workspace + var workspaceCount = 0; + for (var k = 0; k < cat.binds.length; k++) { + var v = cat.binds[k]._verb || ""; + if (v.indexOf("workspace") !== -1) workspaceCount++; + } + var workspaceDominated = workspaceCount >= Math.ceil(cat.binds.length * 0.6); + if (!workspaceDominated) { + result.push(cat); + continue; + } + + var switching = [], moving = [], mouse = []; + for (var j = 0; j < cat.binds.length; j++) { + var bind = cat.binds[j]; + var verb = bind._verb || ""; + var isMouse = (bind._mainKey || "").indexOf("MOUSE") !== -1 || + (bind.keys || "").indexOf("MOUSE") !== -1 || + (bind.keys || "").indexOf("Wheel") !== -1; + + if (isMouse) { + mouse.push(bind); + } else if (verb.indexOf("move") !== -1 || verb.indexOf("send") !== -1) { + // Hyprland: movetoworkspace*, Niri: move-window-to-workspace, move-column-to-workspace + moving.push(bind); + } else { + // Hyprland: workspace, Niri: focus-workspace + switching.push(bind); + } + } + + if (switching.length > 0) result.push({ title: pluginApi?.tr("panel.workspace-switching"), binds: switching }); + if (moving.length > 0) result.push({ title: pluginApi?.tr("panel.workspace-moving"), binds: moving }); + if (mouse.length > 0) result.push({ title: pluginApi?.tr("panel.workspace-mouse"), binds: mouse }); + } + return result; + } + + function distributeCategories() { + var numCols = root.columnCount; + + // Calculate weights for each category + var catData = []; + for (var i = 0; i < categories.length; i++) { + var weight = 1 + categories[i].binds.length + 1; // header + binds + spacer + catData.push({ index: i, weight: weight }); + } + + // Sort by weight descending (largest categories first for better distribution) + catData.sort(function(a, b) { return b.weight - a.weight; }); + + var columns = []; + var columnWeights = []; + for (var c = 0; c < numCols; c++) { + columns.push([]); + columnWeights.push(0); + } + + // Assign each category to the column with smallest current weight + for (var i = 0; i < catData.length; i++) { + var minCol = 0; + for (var c = 1; c < numCols; c++) { + if (columnWeights[c] < columnWeights[minCol]) { + minCol = c; + } + } + columns[minCol].push(catData[i].index); + columnWeights[minCol] += catData[i].weight; + } + + // Sort categories within each column by original order for consistent display + for (var c = 0; c < numCols; c++) { + columns[c].sort(function(a, b) { return a - b; }); + } + + return columns; + } + +} diff --git a/noctalia/.config/plugins/keybind-cheatsheet/README.md b/noctalia/.config/plugins/keybind-cheatsheet/README.md new file mode 100644 index 0000000..5c30b87 --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/README.md @@ -0,0 +1,272 @@ +# Keybind Cheatsheet for Noctalia + +Universal keyboard shortcuts cheatsheet plugin for Noctalia that **automatically detects** your compositor (Hyprland, Niri or MangoWC) and displays your keybindings with **recursive config parsing**. Supports the classic Hyprland `.conf` format, the new **Hyprland 0.55+ Lua config**, Niri KDL and MangoWC. + + + +## Features + +- **Automatic compositor detection** (Hyprland, Niri, MangoWC) +- **Hyprland Lua config support** (Hyprland 0.55+) via `hyprctl binds -j` +- **Recursive config parsing** - follows `source` (Hyprland `.conf`), `require()` (Hyprland Lua) and `include` (Niri) directives +- **Glob pattern support** - parses `~/.config/hypr/*.conf` style includes +- **Without Description section** - binds with no description are surfaced, not dropped; add a custom description or hide them inline +- **Full per-category color customization** - background + text color for every key category, with live preview and clipboard quick-paste +- **Search filter** - type to filter keybindings in the panel +- **Configurable paths & parser mode** - custom config locations and forced Lua/conf parser in settings +- **Smart key formatting** - XF86 keys display as readable names (Vol Up, Bright Down, etc.) +- **Flexible column layout** (1-4 columns) with auto-height +- **IPC support** - `toggle` and `refresh` for global hotkeys + +## Supported Compositors + +| Compositor | Default Config | How it's read | +|------------|----------------|----------------| +| **Hyprland 0.55+** | `~/.config/hypr/hyprland.lua` | `hyprctl binds -j` (live, already-evaluated binds) | +| **Hyprland (classic)** | `~/.config/hypr/hyprland.conf` | hyprlang `.conf` text parser (recursive) | +| **Niri** | `~/.config/niri/config.kdl` | KDL parser (recursive `include`) | +| **MangoWC** | `~/.config/mango/config.conf` | `bind=` / `axisbind=` / `mousebind=` parser | + +## Installation + +```bash +cp -r keybind-cheatsheet ~/.config/noctalia/plugins/ +``` + +## Usage + +### Bar Widget +Add the plugin to your bar configuration in Noctalia settings. Click the keyboard icon to open the cheatsheet. + +### Global Hotkey + +**Hyprland:** +```bash +bind = $mod, F1, exec, qs -c noctalia-shell ipc call plugin:keybind-cheatsheet toggle +``` +You can set your custom Super key variable (e.g. `$mainMod`) in the plugin settings. + +**Niri:** +```kdl +binds { + Mod+F1 { spawn-sh "qs -c noctalia-shell ipc call plugin:keybind-cheatsheet toggle"; } +} +``` + +**MangoWC:** +```bash +bind=SUPER,F1,spawn,qs -c noctalia-shell ipc call plugin:keybind-cheatsheet toggle +``` + +### IPC Commands + +| Command | Effect | +|---------|--------| +| `qs -c noctalia-shell ipc call plugin:keybind-cheatsheet toggle` | Open / close the cheatsheet panel | +| `qs -c noctalia-shell ipc call plugin:keybind-cheatsheet refresh` | Force a re-parse of your keybindings | + +`refresh` is useful after editing your config — bind it to a key to reload without restarting the shell. + +## Hyprland Lua Config (0.55+) + +Hyprland 0.55 replaced the static hyprlang `.conf` with a **Lua config** (`hyprland.lua`). Binds can now be generated with `require()`, `for` loops and runtime logic, so they **cannot be recovered by reading the file as text**. + +### How it works + +Instead of parsing the Lua file, the plugin asks Hyprland directly: + +1. **Authoritative bind list** — runs `hyprctl binds -j` and reads the already-evaluated JSON bind list. This correctly handles `require()` modules, `for` loops and multi-key chords — whatever Hyprland *actually* has bound at runtime. +2. **Category & description recovery** — the plugin then *lightly scans* `hyprland.lua` and every file it pulls in via `require()` to recover human-readable structure: + - **Category headers**: a comment in the form `-- N. NAME` (e.g. `-- 1. Applications`) + - **Static descriptions**: a `description = "..."` (or `desc = '...'`) literal maps that exact description to the current category + - **Loop-generated descriptions**: a concatenated literal such as `description = "Workspace " .. i` is treated as a *prefix* — any bind whose description starts with `Workspace ` is filed under that category +3. Binds whose description can't be matched fall into a generic **Other** category; binds with no description at all go to the **Without Description** section (see below). + +You don't need to change your Lua config — just keep using `-- N. NAME` comment headers and `description = "..."` fields where you want grouping. + +**Example Lua structure the scanner understands:** +```lua +-- 1. Applications +Hyprland.config.bind("$mod, T", function() ... end, { description = "Terminal" }) +Hyprland.config.bind("$mod, B", function() ... end, { description = "Browser" }) + +-- 2. Workspaces +for i = 1, 9 do + Hyprland.config.bind("$mod, " .. i, function() ... end, + { description = "Workspace " .. i }) -- prefix "Workspace " -> category "Workspaces" +end + +require("keybinds") -- followed and scanned recursively +``` + +### Parser mode + +A **Parser Mode** setting controls which Hyprland parser is used: + +| Mode | Behavior | +|------|----------| +| `auto` *(default)* | Use the Lua parser if `hyprland.lua` exists, otherwise fall back to the `.conf` parser | +| `lua` | Always use `hyprctl binds -j` (Lua parser) | +| `conf` | Always use the classic `.conf` text parser | + +The legacy hyprlang `.conf` parser is **kept unchanged** as a fallback for users still on hyprlang configs. + +## Without Description Section + +Binds with no description are no longer silently dropped. They appear in a dedicated **Without Description** section where you can: + +- **Add a custom description** inline — it is remembered and displayed like any other bind +- **Hide** a bind you don't care about + +Overrides are keyed by a **stable bind identity** (`submap | modmask | key | flags | dispatcher`) that deliberately excludes Hyprland's unstable internal Lua registry reference, so your custom descriptions and hidden binds **survive Hyprland restarts**. + +Settings provide a **"Show binds without a description"** toggle, plus **"Restore hidden"** and **"Clear all overrides"** actions. + +## Config Format + +### Hyprland (classic `.conf`) + +Recursively parses your main config and all `source` includes. + +```bash +# 1. APPLICATIONS +bind = $mainMod, T, exec, alacritty #"Terminal" +bind = $mainMod, B, exec, firefox #"Browser" + +# 2. WINDOW MANAGEMENT +bind = $mainMod, Q, killactive, #"Close window" +bind = $mainMod, F, fullscreen, #"Toggle fullscreen" +``` + +- Categories: `# N. CATEGORY NAME` (N is a number) +- Descriptions: `#"description"` at end of the bind line +- `source = ~/.config/hypr/keybinds.conf` and globs like `source = ~/.config/hypr/apps/*.conf` are followed automatically + +### Niri + +Parses the `binds { }` block and follows all `include` directives. + +```kdl +binds { + // #"Applications" + Mod+T hotkey-overlay-title="Terminal" { spawn "alacritty"; } + + // #"Window Management" + Mod+Q hotkey-overlay-title="Close window" { close-window; } + + // #"Workspaces" + Mod+1 { focus-workspace 1; } +} +``` + +- Categories: `// #"Category Name"` (exact format) +- Descriptions: `hotkey-overlay-title="description"` attribute +- Without descriptions, actions are auto-categorized by type (see table below) +- `include "~/.config/niri/binds.kdl"` is followed automatically + +### MangoWC + +Parses `bind=`, `axisbind=` and `mousebind=` directives from `~/.config/mango/config.conf`. + +```bash +# Applications +bind=SUPER,T,spawn,alacritty #"Terminal" +bind=SUPER,B,spawn,firefox #"Browser" + +# Window Management +bind=SUPER,Q,killclient, #"Close window" +``` + +- Categories: standalone `# Category Name` comment lines +- Descriptions: trailing `#"description"` on a bind line +- Modifier aliases recognized: `LOGO` = `SUPER`, `MOD1` = `ALT` +- XF86 media keys are formatted (Vol Up, Mute, Bright Up/Down, …) +- Lines without a preceding `# Category` use the localized default category + +## Auto-Categorization (Niri) + +When no category comment is provided, keybindings are grouped by action: + +| Action prefix | Category | +|---------------|----------| +| `spawn` | Applications | +| `focus-column-*` | Column Navigation | +| `focus-window-*` | Window Focus | +| `focus-workspace-*` | Workspace Navigation | +| `move-column-*` | Move Columns | +| `move-window-*` | Move Windows | +| `close-window`, `fullscreen-window` | Window Management | +| `maximize-column` | Column Management | +| `set-column-width` | Column Width | +| `screenshot*` | Screenshots | +| `power-off-monitors` | Power | +| `quit` | System | + +## Special Key Formatting + +| Raw Key | Display | +|---------|---------| +| `XF86AudioRaiseVolume` | Vol Up | +| `XF86AudioLowerVolume` | Vol Down | +| `XF86AudioMute` | Mute | +| `XF86MonBrightnessUp` | Bright Up | +| `XF86MonBrightnessDown` | Bright Down | +| `Print` | PrtSc | +| `Prior` / `Next` | PgUp / PgDn | + +## Color Customization + +Every key category has independently themeable **background** and **text** colors: +`Super`, `Ctrl`, `Shift`, `Alt`, `XF86`, `Print`, numeric, mouse and default letter keys — plus the description text color. + +- **Two-pill rows** per category: left pill = background, right pill = label text on that background. Click a pill to open the color picker. +- **Theme-aware defaults**: `Super` / `Ctrl` / `Shift` use an empty sentinel meaning "use the Material theme accent" (`mPrimary` / `mSecondary` / `mTertiary`), so themed setups stay untouched unless you deliberately override. +- **Clipboard quick-paste**: copy a `#RRGGBB` / `#RRGGBBAA` hex and a paste icon appears in each pill — one click applies it (clipboard polled via `wl-paste`). +- **Live preview + revert**: changes preview immediately; closing Settings without Save restores the snapshot taken when the panel opened. +- **Per-row reset** and a **"Reset all colors"** action restore theme defaults. + +## Settings + +Access settings via the gear icon in the panel header: + +- **Window width / height** (auto or manual) and **column count** (1-4) +- **Hyprland config path** — classic `.conf` location +- **Hyprland Lua path** — `hyprland.lua` location (0.55+) +- **Parser mode** — `auto` / `lua` / `conf` +- **Niri config path** / **MangoWC config path** +- **Show binds without a description** toggle + **Restore hidden** / **Clear all overrides** +- **Merge sequential binds** and **split large workspace category** (threshold configurable) +- **Per-category colors** — background + text for every category, with clipboard paste and reset +- **Refresh** — force reload keybindings + +## Troubleshooting + +### "Loading..." stays forever +1. Check the compositor is detected: look for logs tagged `[KeybindCheatsheet]` +2. Verify the config file exists at the configured path +3. On Hyprland 0.55+, confirm `hyprctl binds -j` works in your terminal + +### `hyprctl` error shown in the panel +The Lua parser depends on `hyprctl binds -j`. Ensure Hyprland is running and `hyprctl` is on your `PATH`, or force the classic parser by setting **Parser mode** to `conf`. + +### No categories found +- **Hyprland `.conf` / MangoWC:** categories must be `# 1.` / `# Category Name` comment lines +- **Hyprland Lua:** add `-- N. NAME` headers and `description = "..."` fields in `hyprland.lua` (and `require()`d modules) +- **Niri:** use `// #"Category Name"` format + +### Binds missing or under "Without Description" +On Lua configs, descriptions are matched from `description = "..."` literals against the live `hyprctl` binds. Loop-generated binds need a concatenated prefix literal (e.g. `"Workspace " .. i`). Anything unmatched lands in **Without Description**, where you can add a description or hide it. + +### Keybinds from included files not showing +The plugin follows `source` (Hyprland `.conf`), `require()` (Hyprland Lua) and `include` (Niri) directives automatically. Check logs to see which files are parsed. + +## Requirements + +- Noctalia Shell 3.6.0+ +- Hyprland (classic `.conf` or 0.55+ Lua), Niri, or MangoWC +- `hyprctl` on `PATH` for Hyprland Lua configs +- `wl-paste` (wl-clipboard) for the color clipboard quick-paste feature + +## License + +MIT diff --git a/noctalia/.config/plugins/keybind-cheatsheet/Settings.qml b/noctalia/.config/plugins/keybind-cheatsheet/Settings.qml new file mode 100644 index 0000000..3690ad1 --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/Settings.qml @@ -0,0 +1,1518 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Widgets +import qs.Services.UI +import "." as Local + +ColumnLayout { + id: root + spacing: 0 + + property var pluginApi: null + property var cfg: pluginApi?.pluginSettings || ({}) + property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({}) + + // Live preview + revert-on-cancel pattern (approved deviation from edit-copy). + // Color settings apply visually in real time via _applyPreview, but the snapshot + // is restored on close if saveSettings() (Apply) was never called. + property var _snapshot: null + property bool _applied: false + + function _applyPreview(key, value) { + if (!pluginApi) return; + var patch = {}; + patch[key] = value; + pluginApi.pluginSettings = Object.assign({}, pluginApi.pluginSettings, patch); + } + + // Edit-copy properties for non-preview settings (window, layout, parsing, paths) + property int editWindowWidth: cfg.windowWidth ?? defaults.windowWidth ?? 1400 + property int editWindowHeight: cfg.windowHeight ?? defaults.windowHeight ?? 850 + property bool editAutoHeight: cfg.autoHeight ?? defaults.autoHeight ?? true + property int editColumnCount: cfg.columnCount ?? defaults.columnCount ?? 3 + property string editModKeyVariable: cfg.modKeyVariable || defaults.modKeyVariable || "$mod" + property string editHyprlandConfigPath: cfg.hyprlandConfigPath || defaults.hyprlandConfigPath || "~/.config/hypr/hyprland.conf" + property string editHyprlandLuaConfigPath: cfg.hyprlandLuaConfigPath || defaults.hyprlandLuaConfigPath || "~/.config/hypr/hyprland.lua" + property string editHyprlandParserMode: cfg.hyprlandParserMode || defaults.hyprlandParserMode || "auto" + property string editNiriConfigPath: cfg.niriConfigPath || defaults.niriConfigPath || "~/.config/niri/config.kdl" + property string editMangoConfigPath: cfg.mangoConfigPath || defaults.mangoConfigPath || "~/.config/mango/config.conf" + property bool editMergeSequentialBinds: cfg.mergeSequentialBinds ?? defaults.mergeSequentialBinds ?? true + property bool editShowUndescribedBinds: cfg.showUndescribedBinds ?? defaults.showUndescribedBinds ?? false + property bool editSplitLargeWorkspaceCategory: cfg.splitLargeWorkspaceCategory ?? defaults.splitLargeWorkspaceCategory ?? true + property int editWorkspaceSplitThreshold: cfg.workspaceSplitThreshold ?? defaults.workspaceSplitThreshold ?? 12 + + // Live-preview properties for colors (mirror current pluginSettings; written via _applyPreview). + // All background color fields are stored as string (hex) for empty-string support. + property string valueKeyColorSuper: cfg.keyColorSuper ?? defaults.keyColorSuper ?? "" + property string valueKeyColorCtrl: cfg.keyColorCtrl ?? defaults.keyColorCtrl ?? "" + property string valueKeyColorShift: cfg.keyColorShift ?? defaults.keyColorShift ?? "" + property string valueKeyColorAlt: cfg.keyColorAlt ?? defaults.keyColorAlt ?? "#FF6B6B" + property string valueKeyColorXF86: cfg.keyColorXF86 ?? defaults.keyColorXF86 ?? "#4ECDC4" + property string valueKeyColorPrint: cfg.keyColorPrint ?? defaults.keyColorPrint ?? "#95E1D3" + property string valueKeyColorNumeric: cfg.keyColorNumeric ?? defaults.keyColorNumeric ?? "#A8DADC" + property string valueKeyColorMouse: cfg.keyColorMouse ?? defaults.keyColorMouse ?? "#F38181" + property string valueKeyColorDefault: cfg.keyColorDefault ?? defaults.keyColorDefault ?? "#6C757D" + // Per-category text color overrides — empty string = fall back to keyLabelColor global + property string valueKeyTextSuper: cfg.keyTextSuper ?? defaults.keyTextSuper ?? "" + property string valueKeyTextCtrl: cfg.keyTextCtrl ?? defaults.keyTextCtrl ?? "" + property string valueKeyTextShift: cfg.keyTextShift ?? defaults.keyTextShift ?? "" + property string valueKeyTextAlt: cfg.keyTextAlt ?? defaults.keyTextAlt ?? "" + property string valueKeyTextXF86: cfg.keyTextXF86 ?? defaults.keyTextXF86 ?? "" + property string valueKeyTextPrint: cfg.keyTextPrint ?? defaults.keyTextPrint ?? "" + property string valueKeyTextNumeric: cfg.keyTextNumeric ?? defaults.keyTextNumeric ?? "" + property string valueKeyTextMouse: cfg.keyTextMouse ?? defaults.keyTextMouse ?? "" + property string valueKeyTextDefault: cfg.keyTextDefault ?? defaults.keyTextDefault ?? "" + property string valueKeyLabelColor: cfg.keyLabelColor ?? defaults.keyLabelColor ?? "#FFFFFF" + // Generic X11 mods (Mod2/Mod3/Mod5): background colour, text colour, and a + // display-label override (rename e.g. Mod3 -> "Hyper" for caps:hyper). + property string valueKeyColorMod2: cfg.keyColorMod2 ?? defaults.keyColorMod2 ?? "" + property string valueKeyColorMod3: cfg.keyColorMod3 ?? defaults.keyColorMod3 ?? "" + property string valueKeyColorMod5: cfg.keyColorMod5 ?? defaults.keyColorMod5 ?? "" + property string valueKeyTextMod2: cfg.keyTextMod2 ?? defaults.keyTextMod2 ?? "" + property string valueKeyTextMod3: cfg.keyTextMod3 ?? defaults.keyTextMod3 ?? "" + property string valueKeyTextMod5: cfg.keyTextMod5 ?? defaults.keyTextMod5 ?? "" + property string valueKeyLabelMod2: cfg.keyLabelMod2 ?? defaults.keyLabelMod2 ?? "" + property string valueKeyLabelMod3: cfg.keyLabelMod3 ?? defaults.keyLabelMod3 ?? "" + property string valueKeyLabelMod5: cfg.keyLabelMod5 ?? defaults.keyLabelMod5 ?? "" + property string valueDescriptionTextColor: cfg.descriptionTextColor ?? defaults.descriptionTextColor ?? "" + + // Clipboard polling for quick-paste feature. + // Reads wl-paste periodically while Settings is visible; exposes a validated hex if found. + property string clipboardHex: "" + + Timer { + id: clipboardTimer + interval: 1500 + running: root.visible + repeat: true + triggeredOnStart: true + onTriggered: if (!clipboardProcess.running) clipboardProcess.running = true + } + + Process { + id: clipboardProcess + command: ["wl-paste", "-n"] + stdout: StdioCollector { + onStreamFinished: { + var raw = (text || "").trim(); + // Accept #RRGGBB or #RRGGBBAA (with or without leading #) + var m = raw.match(/^#?([0-9a-fA-F]{6}(?:[0-9a-fA-F]{2})?)$/); + if (m) { + root.clipboardHex = "#" + m[1].toUpperCase(); + } else { + root.clipboardHex = ""; + } + } + } + } + + Component.onCompleted: { + if (pluginApi) { + _snapshot = JSON.parse(JSON.stringify(pluginApi.pluginSettings)); + } + } + + Component.onDestruction: { + clipboardTimer.stop(); + if (clipboardProcess.running) clipboardProcess.running = false; + if (!_applied && pluginApi && _snapshot) { + pluginApi.pluginSettings = Object.assign({}, _snapshot); + } + } + + // ============ TAB BAR ============ + NTabBar { + id: tabBar + Layout.fillWidth: true + Layout.bottomMargin: Style.marginM + distributeEvenly: true + currentIndex: tabView.currentIndex + + NTabButton { + text: pluginApi?.tr("settings.tab-general") + icon: "settings" + tabIndex: 0 + checked: tabBar.currentIndex === 0 + } + NTabButton { + text: pluginApi?.tr("settings.tab-appearance") + icon: "color-picker" + tabIndex: 1 + checked: tabBar.currentIndex === 1 + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.marginS + } + + // ============ TAB VIEW ============ + NTabView { + id: tabView + currentIndex: tabBar.currentIndex + + // ============ TAB 1: GENERAL ============ + ColumnLayout { + id: generalTab + spacing: Style.marginM + + // Header + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.title") + pointSize: Style.fontSizeXXL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.description") + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeM + wrapMode: Text.WordWrap + } + + // ===== Window Size ===== + NBox { + Layout.fillWidth: true + Layout.preferredHeight: sizeContent.implicitHeight + Style.marginM * 2 + color: Color.mSurfaceVariant + + ColumnLayout { + id: sizeContent + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginS + + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.window-size") + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + RowLayout { + spacing: Style.marginM + + NText { + text: pluginApi?.tr("settings.width") + color: Color.mOnSurface + pointSize: Style.fontSizeM + Layout.preferredWidth: 120 + } + + NTextInput { + id: widthInput + Layout.preferredWidth: 100 * Style.uiScaleRatio + Layout.preferredHeight: Style.baseWidgetSize + text: root.editWindowWidth.toString() + onTextChanged: { + var val = parseInt(text); + if (!isNaN(val) && val >= 400 && val <= 3000) { + root.editWindowWidth = val; + } + } + } + + NText { + text: pluginApi?.tr("settings.width-range") + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeS + } + } + + RowLayout { + spacing: Style.marginM + + NText { + text: pluginApi?.tr("settings.height") + color: Color.mOnSurface + pointSize: Style.fontSizeM + Layout.preferredWidth: 120 + } + + NToggle { + id: autoHeightToggle + label: pluginApi?.tr("settings.auto-height") + checked: root.editAutoHeight + onToggled: function(checked) { + root.editAutoHeight = checked; + } + } + + NTextInput { + id: heightInput + Layout.preferredWidth: 100 * Style.uiScaleRatio + Layout.preferredHeight: Style.baseWidgetSize + Component.onCompleted: text = root.editWindowHeight > 0 ? root.editWindowHeight.toString() : "850" + enabled: !autoHeightToggle.checked + opacity: enabled ? 1.0 : 0.5 + onTextChanged: { + var val = parseInt(text); + if (!isNaN(val) && val >= 300 && val <= 2000) { + root.editWindowHeight = val; + } + } + } + + NText { + text: pluginApi?.tr("settings.height-range") + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeS + opacity: autoHeightToggle.checked ? 0.5 : 1.0 + } + } + + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.auto-height-hint") + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeS + wrapMode: Text.WordWrap + } + } + } + + // ===== Layout (Columns) ===== + NBox { + Layout.fillWidth: true + Layout.preferredHeight: columnContent.implicitHeight + Style.marginM * 2 + color: Color.mSurfaceVariant + + ColumnLayout { + id: columnContent + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginS + + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.layout") + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + RowLayout { + spacing: Style.marginM + + NText { + text: pluginApi?.tr("settings.columns") + color: Color.mOnSurface + pointSize: Style.fontSizeM + } + + NComboBox { + id: columnCombo + Layout.preferredWidth: 150 * Style.uiScaleRatio + Layout.preferredHeight: Style.baseWidgetSize + model: ListModel { + ListElement { name: "1"; key: "1" } + ListElement { name: "2"; key: "2" } + ListElement { name: "3"; key: "3" } + ListElement { name: "4"; key: "4" } + } + currentKey: root.editColumnCount.toString() + onSelected: key => { + root.editColumnCount = parseInt(key); + } + } + } + + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.columns-hint") + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeS + wrapMode: Text.WordWrap + } + } + } + + // ===== Parsing Options ===== + NBox { + Layout.fillWidth: true + Layout.preferredHeight: parsingContent.implicitHeight + Style.marginM * 2 + color: Color.mSurfaceVariant + + ColumnLayout { + id: parsingContent + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginS + + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.parsing-options") + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NToggle { + Layout.fillWidth: true + label: pluginApi?.tr("settings.merge-sequential") + description: pluginApi?.tr("settings.merge-sequential-hint") + checked: root.editMergeSequentialBinds + onToggled: function(checked) { + root.editMergeSequentialBinds = checked; + } + } + + NToggle { + Layout.fillWidth: true + label: pluginApi?.tr("settings.show-undescribed") + description: pluginApi?.tr("settings.show-undescribed-hint") + checked: root.editShowUndescribedBinds + onToggled: function(checked) { + root.editShowUndescribedBinds = checked; + } + } + + // Bind override management (custom descriptions / hidden binds) + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginXS + + function overrideCount(kind) { + var o = root.pluginApi?.pluginSettings?.bindOverrides || ({}); + var n = 0; + for (var k in o) { + if (kind === "hidden" && o[k] && o[k].hidden === true) n++; + else if (kind === "desc" && o[k] && o[k].desc) n++; + } + return n; + } + + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.overrides-summary", { + "hidden": parent.overrideCount("hidden"), + "custom": parent.overrideCount("desc") + }) + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeXS + wrapMode: Text.WordWrap + } + + RowLayout { + spacing: Style.marginM + + NButton { + text: pluginApi?.tr("settings.restore-hidden") + icon: "eye" + onClicked: { + var o = root.pluginApi?.pluginSettings?.bindOverrides || ({}); + var next = ({}); + for (var k in o) { + var e = ({}); + if (o[k] && o[k].desc) e.desc = o[k].desc; + if (Object.keys(e).length > 0) next[k] = e; + } + if (root.pluginApi && root.pluginApi.pluginSettings) { + root.pluginApi.pluginSettings.bindOverrides = next; + root.pluginApi.saveSettings(); + root.pluginApi.mainInstance?.refresh(); + } + ToastService.showNotice(root.pluginApi?.tr("settings.restore-hidden-message")); + } + } + + NButton { + text: pluginApi?.tr("settings.clear-overrides") + icon: "rotate" + onClicked: { + if (root.pluginApi && root.pluginApi.pluginSettings) { + root.pluginApi.pluginSettings.bindOverrides = ({}); + root.pluginApi.saveSettings(); + root.pluginApi.mainInstance?.refresh(); + } + ToastService.showNotice(root.pluginApi?.tr("settings.clear-overrides-message")); + } + } + } + } + + NToggle { + Layout.fillWidth: true + label: pluginApi?.tr("settings.split-workspaces") + description: pluginApi?.tr("settings.split-workspaces-hint") + checked: root.editSplitLargeWorkspaceCategory + onToggled: function(checked) { + root.editSplitLargeWorkspaceCategory = checked; + } + } + + RowLayout { + spacing: Style.marginM + visible: root.editSplitLargeWorkspaceCategory + + NText { + text: pluginApi?.tr("settings.split-threshold") + color: Color.mOnSurface + pointSize: Style.fontSizeM + Layout.fillWidth: true + } + + NSpinBox { + Layout.preferredWidth: 140 * Style.uiScaleRatio + value: root.editWorkspaceSplitThreshold + from: 4 + to: 50 + stepSize: 1 + onValueChanged: { + root.editWorkspaceSplitThreshold = value; + } + } + } + } + } + + // ===== Configuration Files ===== + NBox { + Layout.fillWidth: true + Layout.preferredHeight: pathsContent.implicitHeight + Style.marginM * 2 + color: Color.mSurfaceVariant + + ColumnLayout { + id: pathsContent + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.config-paths") + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.config-paths-description") + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeS + wrapMode: Text.WordWrap + } + + ColumnLayout { + spacing: Style.marginXS + + RowLayout { + spacing: Style.marginS + NIcon { + icon: "keyboard" + pointSize: Style.fontSizeM + color: Color.mOnSurface + } + NText { + text: pluginApi?.tr("settings.mod-var") + color: Color.mOnSurface + pointSize: Style.fontSizeM + font.weight: Style.fontWeightBold + } + } + + NTextInput { + id: modVarInput + Layout.fillWidth: true + Layout.preferredHeight: Style.baseWidgetSize + text: root.editModKeyVariable + placeholderText: "$mod" + onTextChanged: { + if (text.length > 0) root.editModKeyVariable = text; + } + } + + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.mod-var-hint") + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeXS + wrapMode: Text.WordWrap + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Color.mOutline + opacity: 0.3 + } + + ColumnLayout { + spacing: Style.marginXS + + RowLayout { + spacing: Style.marginS + NIcon { + icon: "terminal" + pointSize: Style.fontSizeM + color: Color.mPrimary + } + NText { + text: pluginApi?.tr("settings.hyprland-path") + color: Color.mOnSurface + pointSize: Style.fontSizeM + font.weight: Style.fontWeightBold + } + } + + NTextInput { + id: hyprlandPathInput + Layout.fillWidth: true + Layout.preferredHeight: Style.baseWidgetSize + text: root.editHyprlandConfigPath + placeholderText: "~/.config/hypr/hyprland.conf" + onTextChanged: { + if (text.length > 0) root.editHyprlandConfigPath = text; + } + } + + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.hyprland-format-hint") + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeXS + wrapMode: Text.WordWrap + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Color.mOutline + opacity: 0.3 + } + + // Hyprland Lua path + parser mode (Hyprland 0.55+) + ColumnLayout { + spacing: Style.marginXS + + RowLayout { + spacing: Style.marginS + NIcon { + icon: "terminal" + pointSize: Style.fontSizeM + color: Color.mPrimary + } + NText { + text: pluginApi?.tr("settings.hyprland-lua-path") + color: Color.mOnSurface + pointSize: Style.fontSizeM + font.weight: Style.fontWeightBold + } + } + + NTextInput { + id: hyprlandLuaPathInput + Layout.fillWidth: true + Layout.preferredHeight: Style.baseWidgetSize + text: root.editHyprlandLuaConfigPath + placeholderText: "~/.config/hypr/hyprland.lua" + onTextChanged: { + if (text.length > 0) root.editHyprlandLuaConfigPath = text; + } + } + + RowLayout { + spacing: Style.marginM + NText { + text: pluginApi?.tr("settings.hyprland-parser-mode") + color: Color.mOnSurface + pointSize: Style.fontSizeM + } + NComboBox { + id: parserModeCombo + Layout.preferredWidth: 180 * Style.uiScaleRatio + Layout.preferredHeight: Style.baseWidgetSize + model: ListModel { + ListElement { name: "Auto"; key: "auto" } + ListElement { name: "Lua (hyprctl)"; key: "lua" } + ListElement { name: "Legacy .conf"; key: "conf" } + } + currentKey: root.editHyprlandParserMode + onSelected: key => { root.editHyprlandParserMode = key; } + } + } + + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.hyprland-lua-hint") + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeXS + wrapMode: Text.WordWrap + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Color.mOutline + opacity: 0.3 + } + + ColumnLayout { + spacing: Style.marginXS + + RowLayout { + spacing: Style.marginS + NIcon { + icon: "layout" + pointSize: Style.fontSizeM + color: Color.mSecondary + } + NText { + text: pluginApi?.tr("settings.niri-path") + color: Color.mOnSurface + pointSize: Style.fontSizeM + font.weight: Style.fontWeightBold + } + } + + NTextInput { + id: niriPathInput + Layout.fillWidth: true + Layout.preferredHeight: Style.baseWidgetSize + text: root.editNiriConfigPath + placeholderText: "~/.config/niri/config.kdl" + onTextChanged: { + if (text.length > 0) root.editNiriConfigPath = text; + } + } + + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.niri-format-hint") + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeXS + wrapMode: Text.WordWrap + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Color.mOutline + opacity: 0.3 + } + + ColumnLayout { + spacing: Style.marginXS + + RowLayout { + spacing: Style.marginS + NIcon { + icon: "terminal" + pointSize: Style.fontSizeM + color: Color.mTertiary + } + NText { + text: pluginApi?.tr("settings.mango-path") + color: Color.mOnSurface + pointSize: Style.fontSizeM + font.weight: Style.fontWeightBold + } + } + + NTextInput { + id: mangoPathInput + Layout.fillWidth: true + Layout.preferredHeight: Style.baseWidgetSize + text: root.editMangoConfigPath + placeholderText: "~/.config/mango/config.conf" + onTextChanged: { + if (text.length > 0) root.editMangoConfigPath = text; + } + } + + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.mango-format-hint") + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeXS + wrapMode: Text.WordWrap + } + } + } + } + + // ===== Actions ===== + NBox { + Layout.fillWidth: true + Layout.preferredHeight: actionsContent.implicitHeight + Style.marginM * 2 + color: Color.mSurfaceVariant + + ColumnLayout { + id: actionsContent + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.actions") + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + RowLayout { + spacing: Style.marginM + + NButton { + text: pluginApi?.tr("settings.refresh-keybinds") + icon: "refresh" + onClicked: { + pluginApi?.mainInstance?.refresh(); + ToastService.showNotice(pluginApi?.tr("settings.refresh-message")); + } + } + + NButton { + text: pluginApi?.tr("settings.reset-defaults") + icon: "rotate" + onClicked: { + root.editWindowWidth = defaults.windowWidth || 1400; + root.editWindowHeight = defaults.windowHeight || 850; + root.editAutoHeight = defaults.autoHeight ?? true; + root.editColumnCount = defaults.columnCount || 3; + root.editModKeyVariable = defaults.modKeyVariable || "$mod"; + root.editHyprlandConfigPath = defaults.hyprlandConfigPath || "~/.config/hypr/hyprland.conf"; + root.editHyprlandLuaConfigPath = defaults.hyprlandLuaConfigPath || "~/.config/hypr/hyprland.lua"; + root.editHyprlandParserMode = defaults.hyprlandParserMode || "auto"; + root.editNiriConfigPath = defaults.niriConfigPath || "~/.config/niri/config.kdl"; + root.editMangoConfigPath = defaults.mangoConfigPath || "~/.config/mango/config.conf"; + root.editMergeSequentialBinds = defaults.mergeSequentialBinds ?? true; + root.editShowUndescribedBinds = defaults.showUndescribedBinds ?? false; + root.editSplitLargeWorkspaceCategory = defaults.splitLargeWorkspaceCategory ?? true; + root.editWorkspaceSplitThreshold = defaults.workspaceSplitThreshold ?? 12; + + widthInput.text = root.editWindowWidth.toString(); + heightInput.text = root.editWindowHeight.toString(); + modVarInput.text = root.editModKeyVariable; + hyprlandPathInput.text = root.editHyprlandConfigPath; + hyprlandLuaPathInput.text = root.editHyprlandLuaConfigPath; + niriPathInput.text = root.editNiriConfigPath; + mangoPathInput.text = root.editMangoConfigPath; + + if (pluginApi && pluginApi.pluginSettings) { + pluginApi.pluginSettings.cheatsheetData = []; + pluginApi.saveSettings(); + } + + ToastService.showNotice(pluginApi?.tr("settings.reset-message")); + } + } + } + } + } + + // ===== Keybind Setup ===== + NBox { + Layout.fillWidth: true + Layout.preferredHeight: keybindContent.implicitHeight + Style.marginM * 2 + color: Color.mSurfaceVariant + + ColumnLayout { + id: keybindContent + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginS + + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.keybind-setup") + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.keybind-setup-description") + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeS + wrapMode: Text.WordWrap + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: commandText.implicitHeight + Style.marginS * 2 + color: Color.mSurface + radius: Style.radiusS + + NText { + id: commandText + anchors.fill: parent + anchors.margins: Style.marginS + text: pluginApi?.tr("settings.keybind-ipc-command") + font.family: "monospace" + pointSize: Style.fontSizeS + color: Color.mPrimary + wrapMode: Text.WrapAnywhere + } + } + + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.keybind-example-hyprland") + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeXS + wrapMode: Text.WordWrap + } + + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.keybind-example-niri") + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeXS + wrapMode: Text.WordWrap + } + + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.keybind-example-mango") + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeXS + wrapMode: Text.WordWrap + } + } + } + + Item { + Layout.preferredHeight: Style.marginL + } + } + // ============ END TAB 1 ============ + + // ============ TAB 2: APPEARANCE ============ + ColumnLayout { + id: appearanceTab + spacing: Style.marginM + + // Header + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.appearance-section") + pointSize: Style.fontSizeXXL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.appearance-description") + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeM + wrapMode: Text.WordWrap + } + + // ===== Live Preview ===== + NBox { + Layout.fillWidth: true + Layout.preferredHeight: previewContent.implicitHeight + Style.marginM * 2 + color: Color.mSurfaceVariant + + ColumnLayout { + id: previewContent + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.preview-section") + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + Flow { + Layout.fillWidth: true + spacing: Style.marginS + + Component { + id: previewBadge + Rectangle { + property string label: "" + property color bg: "#888888" + property color fg: "#FFFFFF" + width: badgeText.implicitWidth + Style.marginM * 2 + height: badgeText.implicitHeight + Style.marginS * 2 + radius: Style.radiusS + color: bg + border.color: Qt.darker(bg, 1.4) + border.width: Math.max(1, Style.borderS) + + NText { + id: badgeText + anchors.centerIn: parent + text: parent.label + color: parent.fg + pointSize: Style.fontSizeM + font.weight: Style.fontWeightBold + } + } + } + + Loader { sourceComponent: previewBadge; onLoaded: { + item.label = "Super"; + item.bg = Qt.binding(function() { return root.valueKeyColorSuper || Color.mPrimary }); + item.fg = Qt.binding(function() { return root.valueKeyTextSuper || root.valueKeyLabelColor }); + } } + Loader { sourceComponent: previewBadge; onLoaded: { + item.label = "Ctrl"; + item.bg = Qt.binding(function() { return root.valueKeyColorCtrl || Color.mSecondary }); + item.fg = Qt.binding(function() { return root.valueKeyTextCtrl || root.valueKeyLabelColor }); + } } + Loader { sourceComponent: previewBadge; onLoaded: { + item.label = "Shift"; + item.bg = Qt.binding(function() { return root.valueKeyColorShift || Color.mTertiary }); + item.fg = Qt.binding(function() { return root.valueKeyTextShift || root.valueKeyLabelColor }); + } } + Loader { sourceComponent: previewBadge; onLoaded: { + item.label = "Alt"; + item.bg = Qt.binding(function() { return root.valueKeyColorAlt }); + item.fg = Qt.binding(function() { return root.valueKeyTextAlt || root.valueKeyLabelColor }); + } } + Loader { sourceComponent: previewBadge; onLoaded: { + item.label = "XF86Audio"; + item.bg = Qt.binding(function() { return root.valueKeyColorXF86 }); + item.fg = Qt.binding(function() { return root.valueKeyTextXF86 || root.valueKeyLabelColor }); + } } + Loader { sourceComponent: previewBadge; onLoaded: { + item.label = "Print"; + item.bg = Qt.binding(function() { return root.valueKeyColorPrint }); + item.fg = Qt.binding(function() { return root.valueKeyTextPrint || root.valueKeyLabelColor }); + } } + Loader { sourceComponent: previewBadge; onLoaded: { + item.label = "1-9"; + item.bg = Qt.binding(function() { return root.valueKeyColorNumeric }); + item.fg = Qt.binding(function() { return root.valueKeyTextNumeric || root.valueKeyLabelColor }); + } } + Loader { sourceComponent: previewBadge; onLoaded: { + item.label = "mouse:272"; + item.bg = Qt.binding(function() { return root.valueKeyColorMouse }); + item.fg = Qt.binding(function() { return root.valueKeyTextMouse || root.valueKeyLabelColor }); + } } + Loader { sourceComponent: previewBadge; onLoaded: { + item.label = "Q"; + item.bg = Qt.binding(function() { return root.valueKeyColorDefault }); + item.fg = Qt.binding(function() { return root.valueKeyTextDefault || root.valueKeyLabelColor }); + } } + Loader { sourceComponent: previewBadge; onLoaded: { + item.label = Qt.binding(function() { return root.valueKeyLabelMod3 || "Mod3" }); + item.bg = Qt.binding(function() { return root.valueKeyColorMod3 || root.valueKeyColorDefault }); + item.fg = Qt.binding(function() { return root.valueKeyTextMod3 || root.valueKeyLabelColor }); + } } + Loader { sourceComponent: previewBadge; onLoaded: { + item.label = Qt.binding(function() { return root.valueKeyLabelMod2 || "Mod2" }); + item.bg = Qt.binding(function() { return root.valueKeyColorMod2 || root.valueKeyColorDefault }); + item.fg = Qt.binding(function() { return root.valueKeyTextMod2 || root.valueKeyLabelColor }); + } } + Loader { sourceComponent: previewBadge; onLoaded: { + item.label = Qt.binding(function() { return root.valueKeyLabelMod5 || "Mod5" }); + item.bg = Qt.binding(function() { return root.valueKeyColorMod5 || root.valueKeyColorDefault }); + item.fg = Qt.binding(function() { return root.valueKeyTextMod5 || root.valueKeyLabelColor }); + } } + } + + // Description preview sample + NText { + Layout.fillWidth: true + Layout.topMargin: Style.marginS + text: pluginApi?.tr("settings.color-description") + " — sample" + color: root.valueDescriptionTextColor + pointSize: Style.fontSizeM + } + } + } + + // ===== Color Pickers ===== + NBox { + Layout.fillWidth: true + Layout.preferredHeight: colorContent.implicitHeight + Style.marginM * 2 + color: Color.mSurfaceVariant + + ColumnLayout { + id: colorContent + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.color-pickers") + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.color-theme-hint") + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeS + wrapMode: Text.WordWrap + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + NIcon { + icon: "clipboard" + pointSize: Style.fontSizeM + color: root.clipboardHex.length > 0 ? Color.mPrimary : Color.mOnSurfaceVariant + } + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.color-paste-hint") + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeS + wrapMode: Text.WordWrap + } + } + + // Super (modifier: empty bg = theme accent) + Local.ColorPairRow { + pluginApi: root.pluginApi + labelText: pluginApi?.tr("settings.color-super") + letter: "S" + bgValue: root.valueKeyColorSuper + textValue: root.valueKeyTextSuper + bgFallback: Color.mPrimary + textFallback: root.valueKeyLabelColor + clipboardHex: root.clipboardHex + onBgPicked: c => { root.valueKeyColorSuper = c.toString(); root._applyPreview("keyColorSuper", c.toString()); } + onTextPicked: c => { root.valueKeyTextSuper = c.toString(); root._applyPreview("keyTextSuper", c.toString()); } + onBgPasted: hex => { root.valueKeyColorSuper = hex; root._applyPreview("keyColorSuper", hex); } + onTextPasted: hex => { root.valueKeyTextSuper = hex; root._applyPreview("keyTextSuper", hex); } + onResetRequested: { + root.valueKeyColorSuper = ""; root._applyPreview("keyColorSuper", ""); + root.valueKeyTextSuper = ""; root._applyPreview("keyTextSuper", ""); + } + } + + // Ctrl + Local.ColorPairRow { + pluginApi: root.pluginApi + labelText: pluginApi?.tr("settings.color-ctrl") + letter: "C" + bgValue: root.valueKeyColorCtrl + textValue: root.valueKeyTextCtrl + bgFallback: Color.mSecondary + textFallback: root.valueKeyLabelColor + clipboardHex: root.clipboardHex + onBgPicked: c => { root.valueKeyColorCtrl = c.toString(); root._applyPreview("keyColorCtrl", c.toString()); } + onTextPicked: c => { root.valueKeyTextCtrl = c.toString(); root._applyPreview("keyTextCtrl", c.toString()); } + onBgPasted: hex => { root.valueKeyColorCtrl = hex; root._applyPreview("keyColorCtrl", hex); } + onTextPasted: hex => { root.valueKeyTextCtrl = hex; root._applyPreview("keyTextCtrl", hex); } + onResetRequested: { + root.valueKeyColorCtrl = ""; root._applyPreview("keyColorCtrl", ""); + root.valueKeyTextCtrl = ""; root._applyPreview("keyTextCtrl", ""); + } + } + + // Shift + Local.ColorPairRow { + pluginApi: root.pluginApi + labelText: pluginApi?.tr("settings.color-shift") + letter: "⇧" + bgValue: root.valueKeyColorShift + textValue: root.valueKeyTextShift + bgFallback: Color.mTertiary + textFallback: root.valueKeyLabelColor + clipboardHex: root.clipboardHex + onBgPicked: c => { root.valueKeyColorShift = c.toString(); root._applyPreview("keyColorShift", c.toString()); } + onTextPicked: c => { root.valueKeyTextShift = c.toString(); root._applyPreview("keyTextShift", c.toString()); } + onBgPasted: hex => { root.valueKeyColorShift = hex; root._applyPreview("keyColorShift", hex); } + onTextPasted: hex => { root.valueKeyTextShift = hex; root._applyPreview("keyTextShift", hex); } + onResetRequested: { + root.valueKeyColorShift = ""; root._applyPreview("keyColorShift", ""); + root.valueKeyTextShift = ""; root._applyPreview("keyTextShift", ""); + } + } + + // Alt + Local.ColorPairRow { + pluginApi: root.pluginApi + labelText: pluginApi?.tr("settings.color-alt") + letter: "A" + bgValue: root.valueKeyColorAlt + textValue: root.valueKeyTextAlt + bgFallback: defaults.keyColorAlt || "#FF6B6B" + textFallback: root.valueKeyLabelColor + clipboardHex: root.clipboardHex + onBgPicked: c => { root.valueKeyColorAlt = c.toString(); root._applyPreview("keyColorAlt", c.toString()); } + onTextPicked: c => { root.valueKeyTextAlt = c.toString(); root._applyPreview("keyTextAlt", c.toString()); } + onBgPasted: hex => { root.valueKeyColorAlt = hex; root._applyPreview("keyColorAlt", hex); } + onTextPasted: hex => { root.valueKeyTextAlt = hex; root._applyPreview("keyTextAlt", hex); } + onResetRequested: { + var d = defaults.keyColorAlt || "#FF6B6B"; + root.valueKeyColorAlt = d; root._applyPreview("keyColorAlt", d); + root.valueKeyTextAlt = ""; root._applyPreview("keyTextAlt", ""); + } + } + + // Generic X11 mods (Mod2/Mod3/Mod5): same colour controls as the other + // modifiers, plus a rename field (e.g. caps:hyper -> label Mod3 "Hyper"). + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.mod-label-hint") + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeS + wrapMode: Text.WordWrap + } + + Local.ColorPairRow { + pluginApi: root.pluginApi + labelText: pluginApi?.tr("settings.color-mod3") + letter: root.valueKeyLabelMod3.length > 0 ? root.valueKeyLabelMod3.charAt(0) : "3" + showLabel: true + labelValue: root.valueKeyLabelMod3 + labelPlaceholder: "Mod3" + bgValue: root.valueKeyColorMod3 + textValue: root.valueKeyTextMod3 + bgFallback: defaults.keyColorDefault || "#6C757D" + textFallback: root.valueKeyLabelColor + clipboardHex: root.clipboardHex + onBgPicked: c => { root.valueKeyColorMod3 = c.toString(); root._applyPreview("keyColorMod3", c.toString()); } + onTextPicked: c => { root.valueKeyTextMod3 = c.toString(); root._applyPreview("keyTextMod3", c.toString()); } + onBgPasted: hex => { root.valueKeyColorMod3 = hex; root._applyPreview("keyColorMod3", hex); } + onTextPasted: hex => { root.valueKeyTextMod3 = hex; root._applyPreview("keyTextMod3", hex); } + onLabelEdited: t => { root.valueKeyLabelMod3 = t; root._applyPreview("keyLabelMod3", t); } + onResetRequested: { + root.valueKeyColorMod3 = ""; root._applyPreview("keyColorMod3", ""); + root.valueKeyTextMod3 = ""; root._applyPreview("keyTextMod3", ""); + root.valueKeyLabelMod3 = ""; root._applyPreview("keyLabelMod3", ""); + } + } + + Local.ColorPairRow { + pluginApi: root.pluginApi + labelText: pluginApi?.tr("settings.color-mod2") + letter: root.valueKeyLabelMod2.length > 0 ? root.valueKeyLabelMod2.charAt(0) : "2" + showLabel: true + labelValue: root.valueKeyLabelMod2 + labelPlaceholder: "Mod2" + bgValue: root.valueKeyColorMod2 + textValue: root.valueKeyTextMod2 + bgFallback: defaults.keyColorDefault || "#6C757D" + textFallback: root.valueKeyLabelColor + clipboardHex: root.clipboardHex + onBgPicked: c => { root.valueKeyColorMod2 = c.toString(); root._applyPreview("keyColorMod2", c.toString()); } + onTextPicked: c => { root.valueKeyTextMod2 = c.toString(); root._applyPreview("keyTextMod2", c.toString()); } + onBgPasted: hex => { root.valueKeyColorMod2 = hex; root._applyPreview("keyColorMod2", hex); } + onTextPasted: hex => { root.valueKeyTextMod2 = hex; root._applyPreview("keyTextMod2", hex); } + onLabelEdited: t => { root.valueKeyLabelMod2 = t; root._applyPreview("keyLabelMod2", t); } + onResetRequested: { + root.valueKeyColorMod2 = ""; root._applyPreview("keyColorMod2", ""); + root.valueKeyTextMod2 = ""; root._applyPreview("keyTextMod2", ""); + root.valueKeyLabelMod2 = ""; root._applyPreview("keyLabelMod2", ""); + } + } + + Local.ColorPairRow { + pluginApi: root.pluginApi + labelText: pluginApi?.tr("settings.color-mod5") + letter: root.valueKeyLabelMod5.length > 0 ? root.valueKeyLabelMod5.charAt(0) : "5" + showLabel: true + labelValue: root.valueKeyLabelMod5 + labelPlaceholder: "Mod5" + bgValue: root.valueKeyColorMod5 + textValue: root.valueKeyTextMod5 + bgFallback: defaults.keyColorDefault || "#6C757D" + textFallback: root.valueKeyLabelColor + clipboardHex: root.clipboardHex + onBgPicked: c => { root.valueKeyColorMod5 = c.toString(); root._applyPreview("keyColorMod5", c.toString()); } + onTextPicked: c => { root.valueKeyTextMod5 = c.toString(); root._applyPreview("keyTextMod5", c.toString()); } + onBgPasted: hex => { root.valueKeyColorMod5 = hex; root._applyPreview("keyColorMod5", hex); } + onTextPasted: hex => { root.valueKeyTextMod5 = hex; root._applyPreview("keyTextMod5", hex); } + onLabelEdited: t => { root.valueKeyLabelMod5 = t; root._applyPreview("keyLabelMod5", t); } + onResetRequested: { + root.valueKeyColorMod5 = ""; root._applyPreview("keyColorMod5", ""); + root.valueKeyTextMod5 = ""; root._applyPreview("keyTextMod5", ""); + root.valueKeyLabelMod5 = ""; root._applyPreview("keyLabelMod5", ""); + } + } + + // XF86 + Local.ColorPairRow { + pluginApi: root.pluginApi + labelText: pluginApi?.tr("settings.color-xf86") + letter: "♪" + bgValue: root.valueKeyColorXF86 + textValue: root.valueKeyTextXF86 + bgFallback: defaults.keyColorXF86 || "#4ECDC4" + textFallback: root.valueKeyLabelColor + clipboardHex: root.clipboardHex + onBgPicked: c => { root.valueKeyColorXF86 = c.toString(); root._applyPreview("keyColorXF86", c.toString()); } + onTextPicked: c => { root.valueKeyTextXF86 = c.toString(); root._applyPreview("keyTextXF86", c.toString()); } + onBgPasted: hex => { root.valueKeyColorXF86 = hex; root._applyPreview("keyColorXF86", hex); } + onTextPasted: hex => { root.valueKeyTextXF86 = hex; root._applyPreview("keyTextXF86", hex); } + onResetRequested: { + var d = defaults.keyColorXF86 || "#4ECDC4"; + root.valueKeyColorXF86 = d; root._applyPreview("keyColorXF86", d); + root.valueKeyTextXF86 = ""; root._applyPreview("keyTextXF86", ""); + } + } + + // Print + Local.ColorPairRow { + pluginApi: root.pluginApi + labelText: pluginApi?.tr("settings.color-print") + letter: "P" + bgValue: root.valueKeyColorPrint + textValue: root.valueKeyTextPrint + bgFallback: defaults.keyColorPrint || "#95E1D3" + textFallback: root.valueKeyLabelColor + clipboardHex: root.clipboardHex + onBgPicked: c => { root.valueKeyColorPrint = c.toString(); root._applyPreview("keyColorPrint", c.toString()); } + onTextPicked: c => { root.valueKeyTextPrint = c.toString(); root._applyPreview("keyTextPrint", c.toString()); } + onBgPasted: hex => { root.valueKeyColorPrint = hex; root._applyPreview("keyColorPrint", hex); } + onTextPasted: hex => { root.valueKeyTextPrint = hex; root._applyPreview("keyTextPrint", hex); } + onResetRequested: { + var d = defaults.keyColorPrint || "#95E1D3"; + root.valueKeyColorPrint = d; root._applyPreview("keyColorPrint", d); + root.valueKeyTextPrint = ""; root._applyPreview("keyTextPrint", ""); + } + } + + // Numeric + Local.ColorPairRow { + pluginApi: root.pluginApi + labelText: pluginApi?.tr("settings.color-numeric") + letter: "1" + bgValue: root.valueKeyColorNumeric + textValue: root.valueKeyTextNumeric + bgFallback: defaults.keyColorNumeric || "#A8DADC" + textFallback: root.valueKeyLabelColor + clipboardHex: root.clipboardHex + onBgPicked: c => { root.valueKeyColorNumeric = c.toString(); root._applyPreview("keyColorNumeric", c.toString()); } + onTextPicked: c => { root.valueKeyTextNumeric = c.toString(); root._applyPreview("keyTextNumeric", c.toString()); } + onBgPasted: hex => { root.valueKeyColorNumeric = hex; root._applyPreview("keyColorNumeric", hex); } + onTextPasted: hex => { root.valueKeyTextNumeric = hex; root._applyPreview("keyTextNumeric", hex); } + onResetRequested: { + var d = defaults.keyColorNumeric || "#A8DADC"; + root.valueKeyColorNumeric = d; root._applyPreview("keyColorNumeric", d); + root.valueKeyTextNumeric = ""; root._applyPreview("keyTextNumeric", ""); + } + } + + // Mouse + Local.ColorPairRow { + pluginApi: root.pluginApi + labelText: pluginApi?.tr("settings.color-mouse") + letter: "M" + bgValue: root.valueKeyColorMouse + textValue: root.valueKeyTextMouse + bgFallback: defaults.keyColorMouse || "#F38181" + textFallback: root.valueKeyLabelColor + clipboardHex: root.clipboardHex + onBgPicked: c => { root.valueKeyColorMouse = c.toString(); root._applyPreview("keyColorMouse", c.toString()); } + onTextPicked: c => { root.valueKeyTextMouse = c.toString(); root._applyPreview("keyTextMouse", c.toString()); } + onBgPasted: hex => { root.valueKeyColorMouse = hex; root._applyPreview("keyColorMouse", hex); } + onTextPasted: hex => { root.valueKeyTextMouse = hex; root._applyPreview("keyTextMouse", hex); } + onResetRequested: { + var d = defaults.keyColorMouse || "#F38181"; + root.valueKeyColorMouse = d; root._applyPreview("keyColorMouse", d); + root.valueKeyTextMouse = ""; root._applyPreview("keyTextMouse", ""); + } + } + + // Default letter keys + Local.ColorPairRow { + pluginApi: root.pluginApi + labelText: pluginApi?.tr("settings.color-default") + letter: "Q" + bgValue: root.valueKeyColorDefault + textValue: root.valueKeyTextDefault + bgFallback: defaults.keyColorDefault || "#6C757D" + textFallback: root.valueKeyLabelColor + clipboardHex: root.clipboardHex + onBgPicked: c => { root.valueKeyColorDefault = c.toString(); root._applyPreview("keyColorDefault", c.toString()); } + onTextPicked: c => { root.valueKeyTextDefault = c.toString(); root._applyPreview("keyTextDefault", c.toString()); } + onBgPasted: hex => { root.valueKeyColorDefault = hex; root._applyPreview("keyColorDefault", hex); } + onTextPasted: hex => { root.valueKeyTextDefault = hex; root._applyPreview("keyTextDefault", hex); } + onResetRequested: { + var d = defaults.keyColorDefault || "#6C757D"; + root.valueKeyColorDefault = d; root._applyPreview("keyColorDefault", d); + root.valueKeyTextDefault = ""; root._applyPreview("keyTextDefault", ""); + } + } + + // Description text (text-only row — no background) + // Empty value = theme-aware (Color.mOnSurface) at render time. + Local.ColorPairRow { + pluginApi: root.pluginApi + labelText: pluginApi?.tr("settings.color-description") + letter: "T" + showBg: false + bgValue: "" + textValue: root.valueDescriptionTextColor + textFallback: Color.mOnSurface + clipboardHex: root.clipboardHex + onTextPicked: c => { root.valueDescriptionTextColor = c.toString(); root._applyPreview("descriptionTextColor", c.toString()); } + onTextPasted: hex => { root.valueDescriptionTextColor = hex; root._applyPreview("descriptionTextColor", hex); } + onResetRequested: { + root.valueDescriptionTextColor = ""; root._applyPreview("descriptionTextColor", ""); + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Color.mOutline + opacity: 0.3 + } + + NButton { + Layout.alignment: Qt.AlignRight + text: pluginApi?.tr("settings.reset-all-colors") + icon: "rotate" + onClicked: { + var dAlt = defaults.keyColorAlt || "#FF6B6B"; + var dXF86 = defaults.keyColorXF86 || "#4ECDC4"; + var dPrint = defaults.keyColorPrint || "#95E1D3"; + var dNumeric = defaults.keyColorNumeric || "#A8DADC"; + var dMouse = defaults.keyColorMouse || "#F38181"; + var dDefault = defaults.keyColorDefault || "#6C757D"; + var dLabel = defaults.keyLabelColor || "#FFFFFF"; + var dDesc = defaults.descriptionTextColor ?? ""; + + root.valueKeyColorSuper = ""; + root.valueKeyColorCtrl = ""; + root.valueKeyColorShift = ""; + root.valueKeyColorAlt = dAlt; + root.valueKeyColorXF86 = dXF86; + root.valueKeyColorPrint = dPrint; + root.valueKeyColorNumeric = dNumeric; + root.valueKeyColorMouse = dMouse; + root.valueKeyColorDefault = dDefault; + root.valueKeyLabelColor = dLabel; + root.valueDescriptionTextColor = dDesc; + + root.valueKeyTextSuper = ""; + root.valueKeyTextCtrl = ""; + root.valueKeyTextShift = ""; + root.valueKeyTextAlt = ""; + root.valueKeyTextXF86 = ""; + root.valueKeyTextPrint = ""; + root.valueKeyTextNumeric = ""; + root.valueKeyTextMouse = ""; + root.valueKeyTextDefault = ""; + + root.valueKeyColorMod2 = ""; root.valueKeyColorMod3 = ""; root.valueKeyColorMod5 = ""; + root.valueKeyTextMod2 = ""; root.valueKeyTextMod3 = ""; root.valueKeyTextMod5 = ""; + root.valueKeyLabelMod2 = ""; root.valueKeyLabelMod3 = ""; root.valueKeyLabelMod5 = ""; + + if (pluginApi) { + pluginApi.pluginSettings = Object.assign({}, pluginApi.pluginSettings, { + keyColorSuper: "", + keyColorCtrl: "", + keyColorShift: "", + keyColorAlt: dAlt, + keyColorXF86: dXF86, + keyColorPrint: dPrint, + keyColorNumeric: dNumeric, + keyColorMouse: dMouse, + keyColorDefault: dDefault, + keyLabelColor: dLabel, + descriptionTextColor: dDesc, + keyTextSuper: "", + keyTextCtrl: "", + keyTextShift: "", + keyTextAlt: "", + keyTextXF86: "", + keyTextPrint: "", + keyTextNumeric: "", + keyTextMouse: "", + keyTextDefault: "", + keyColorMod2: "", keyColorMod3: "", keyColorMod5: "", + keyTextMod2: "", keyTextMod3: "", keyTextMod5: "", + keyLabelMod2: "", keyLabelMod3: "", keyLabelMod5: "" + }); + } + } + } + } + } + + Item { + Layout.preferredHeight: Style.marginL + } + } + // ============ END TAB 2 ============ + } + // ============ END TAB VIEW ============ + + // Save function called by the shell when user clicks Apply/Save + function saveSettings() { + if (!pluginApi) return; + + pluginApi.pluginSettings.windowWidth = root.editWindowWidth; + pluginApi.pluginSettings.windowHeight = root.editWindowHeight; + pluginApi.pluginSettings.autoHeight = root.editAutoHeight; + pluginApi.pluginSettings.columnCount = root.editColumnCount; + pluginApi.pluginSettings.modKeyVariable = root.editModKeyVariable; + pluginApi.pluginSettings.hyprlandConfigPath = root.editHyprlandConfigPath; + pluginApi.pluginSettings.hyprlandLuaConfigPath = root.editHyprlandLuaConfigPath; + pluginApi.pluginSettings.hyprlandParserMode = root.editHyprlandParserMode; + pluginApi.pluginSettings.niriConfigPath = root.editNiriConfigPath; + pluginApi.pluginSettings.mangoConfigPath = root.editMangoConfigPath; + pluginApi.pluginSettings.mergeSequentialBinds = root.editMergeSequentialBinds; + pluginApi.pluginSettings.showUndescribedBinds = root.editShowUndescribedBinds; + pluginApi.pluginSettings.splitLargeWorkspaceCategory = root.editSplitLargeWorkspaceCategory; + pluginApi.pluginSettings.workspaceSplitThreshold = root.editWorkspaceSplitThreshold; + + pluginApi.pluginSettings.keyColorAlt = root.valueKeyColorAlt.toString(); + pluginApi.pluginSettings.keyColorXF86 = root.valueKeyColorXF86.toString(); + pluginApi.pluginSettings.keyColorPrint = root.valueKeyColorPrint.toString(); + pluginApi.pluginSettings.keyColorNumeric = root.valueKeyColorNumeric.toString(); + pluginApi.pluginSettings.keyColorMouse = root.valueKeyColorMouse.toString(); + pluginApi.pluginSettings.keyColorSuper = root.valueKeyColorSuper; + pluginApi.pluginSettings.keyColorCtrl = root.valueKeyColorCtrl; + pluginApi.pluginSettings.keyColorShift = root.valueKeyColorShift; + pluginApi.pluginSettings.keyColorDefault = root.valueKeyColorDefault.toString(); + pluginApi.pluginSettings.keyLabelColor = root.valueKeyLabelColor.toString(); + pluginApi.pluginSettings.descriptionTextColor = root.valueDescriptionTextColor.toString(); + + pluginApi.pluginSettings.keyTextSuper = root.valueKeyTextSuper; + pluginApi.pluginSettings.keyTextCtrl = root.valueKeyTextCtrl; + pluginApi.pluginSettings.keyTextShift = root.valueKeyTextShift; + pluginApi.pluginSettings.keyTextAlt = root.valueKeyTextAlt; + pluginApi.pluginSettings.keyTextXF86 = root.valueKeyTextXF86; + pluginApi.pluginSettings.keyTextPrint = root.valueKeyTextPrint; + pluginApi.pluginSettings.keyTextNumeric = root.valueKeyTextNumeric; + pluginApi.pluginSettings.keyTextMouse = root.valueKeyTextMouse; + pluginApi.pluginSettings.keyTextDefault = root.valueKeyTextDefault; + + pluginApi.pluginSettings.keyColorMod2 = root.valueKeyColorMod2; + pluginApi.pluginSettings.keyColorMod3 = root.valueKeyColorMod3; + pluginApi.pluginSettings.keyColorMod5 = root.valueKeyColorMod5; + pluginApi.pluginSettings.keyTextMod2 = root.valueKeyTextMod2; + pluginApi.pluginSettings.keyTextMod3 = root.valueKeyTextMod3; + pluginApi.pluginSettings.keyTextMod5 = root.valueKeyTextMod5; + pluginApi.pluginSettings.keyLabelMod2 = root.valueKeyLabelMod2; + pluginApi.pluginSettings.keyLabelMod3 = root.valueKeyLabelMod3; + pluginApi.pluginSettings.keyLabelMod5 = root.valueKeyLabelMod5; + + _applied = true; + pluginApi.saveSettings(); + + if (pluginApi.mainInstance) { + pluginApi.mainInstance.refresh(); + } + + ToastService.showNotice(pluginApi?.tr("settings.saved")); + } +} diff --git a/noctalia/.config/plugins/keybind-cheatsheet/i18n/de.json b/noctalia/.config/plugins/keybind-cheatsheet/i18n/de.json new file mode 100644 index 0000000..b106bd6 --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/i18n/de.json @@ -0,0 +1,117 @@ +{ + "default-category": "Tastenkürzel", + "error": { + "unsupported-compositor": "Nicht unterstützter Compositor", + "sway-not-supported": "Sway wird noch nicht unterstützt", + "sway-detail": "Sway-Unterstützung wird möglicherweise in einem zukünftigen Update hinzugefügt (ähnliches Format wie Hyprland)", + "labwc-not-supported": "LabWC wird nicht unterstützt.", + "labwc-detail": "LabWC verwendet das XML-Konfigurationsformat, das mit diesem Plugin nicht kompatibel ist.", + "unknown-compositor": "Unbekannter Compositor erkannt", + "unknown-detail": "Dieses Plugin unterstützt nur Niri und Hyprland als Compositor", + "hyprctl-failed": "hyprctl binds failed - is Hyprland running?" + }, + "panel": { + "loading": "Laden...", + "loading-timeout": "Loading timed out. Click Refresh to retry.", + "title": "Tastenbelegung", + "title-hyprland": "Hyprland Tastenbelegung", + "title-niri": "Niri Tastenbelegung", + "title-mango": "MangoWC Keymap", + "workspace-switching": "ARBEITSBEREICHE - WECHSELN", + "workspace-moving": "ARBEITSBEREICHE - VERSCHIEBEN", + "workspace-mouse": "ARBEITSBEREICHE - MAUS", + "search-placeholder": "Tastenkombinationen suchen...", + "no-description": "(no description)", + "other": "Other", + "undescribed": "Without Description", + "add-description": "Add description", + "add-description-placeholder": "Description...", + "save-description": "Save", + "cancel": "Cancel", + "hide-bind": "Hide this bind" + }, + "settings": { + "title": "Tastenkürzel-Ubersicht Einstellungen", + "description": "Konfigurieren Sie das Aussehen und die Quellen der Tastenkürzel fur das Panel.", + "tab-general": "General", + "tab-appearance": "Appearance", + "window-size": "Fenstergröße", + "width": "Breite:", + "height": "Höhe:", + "auto-height": "Auto", + "auto-height-hint": "Wenn 'Auto' aktiviert ist, passt sich die Höhe automatisch an den Inhalt an.", + "layout": "Layout", + "columns": "Anzahl der Spalten:", + "columns-hint": "Mehr Spalten funktionieren besser mit breiteren Fenstern.", + "parsing-options": "Parsing Options", + "merge-sequential": "Merge sequential keybinds", + "merge-sequential-hint": "Combine consecutive numeric keybinds (e.g. Super+1..5 -> Workspace 1..5) into a single ranged entry.", + "show-undescribed": "Show undescribed binds", + "show-undescribed-hint": "Include keybinds that have no comment description. They will be auto-labeled with the action.", + "split-workspaces": "Split large workspace category", + "split-workspaces-hint": "When a workspace category exceeds the threshold, split it into Switching, Moving, and Mouse subcategories.", + "split-threshold": "Split threshold (binds):", + "config-paths": "Konfigurationsdateien", + "config-paths-description": "Geben Sie benutzerdefinierte Pfade zu Ihren Tastenkürzel-Konfigurationsdateien an. Das Plugin folgt automatisch allen include/source Direktiven.", + "hyprland-format-hint": "Erwartetes Format: bind = $mod, KEY, action #\"beschreibung\"\nKategorien: # 1. KATEGORIENAME", + "niri-format-hint": "Kategorien werden erstellt mit: // #\"Kategoriename\"\nBeschreibungen: hotkey-overlay-title=\"beschreibung\"", + "actions": "Aktionen", + "refresh-keybinds": "Tastenkürzel aktualisieren", + "refresh-message": "Tastenkürzel werden beim Öffnen des Panels neu geladen.", + "reset-defaults": "Auf Standard zurücksetzen", + "reset-message": "Einstellungen wurden auf Standard zurückgesetzt.", + "saved": "Einstellungen erfolgreich gespeichert.", + "keybind-setup": "Tastenkürzel-Einrichtung", + "keybind-setup-description": "Um die Übersicht mit einer Tastenkombination zu öffnen, fuge diese Bindung zu deiner Compositor-Konfiguration hinzu:", + "keybind-example-hyprland": "Hyprland Beispiel: bind = $mod, F1, exec, qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "keybind-example-niri": "Niri Beispiel: Super+F1 { spawn \"qs\" \"-c\" \"noctalia-shell\" \"ipc\" \"call\" \"plugin\" \"togglePanel\" \"keybind-cheatsheet\"; }", + "mod-var": "Mod-Tasten-Variable", + "mod-var-hint": "Der Variablenname für Ihre Super-Taste (z.B. $mod oder $mainMod).", + "width-range": "px (400-3000)", + "height-range": "px (300-2000)", + "hyprland-path": "Hyprland Pfad", + "niri-path": "Niri Pfad", + "mango-path": "Mango Path", + "mango-format-hint": "Expected format: bind=MODS,KEY,ACTION,ARGS\nCategories: # Category Name (horizontal rules stripped)\nOptional per-bind description: trailing #\"description\"", + "keybind-example-mango": "Mango example: bind=SUPER,F1,spawn_shell,noctalia-shell ipc call plugin:keybind-cheatsheet toggle", + "appearance-section": "Appearance", + "appearance-description": "Customize the colors used for special key categories. Changes apply immediately and persist when you click Apply.", + "preview-section": "Live Preview", + "color-pickers": "Key Colors", + "color-super": "Super / Mod key", + "color-ctrl": "Ctrl key", + "color-shift": "Shift key", + "color-alt": "Alt-modifier keys", + "color-xf86": "XF86 multimedia keys", + "color-print": "Print / Screenshot keys", + "color-numeric": "Numeric keys (workspaces)", + "color-mouse": "Mouse buttons", + "color-default": "Default letter keys", + "color-label": "Key label text", + "color-description": "Description text", + "color-theme-hint": "Leave empty to use theme accent color.", + "reset-color": "Reset to default", + "reset-all-colors": "Reset all colors", + "color-auto": "auto", + "color-mod2": "Mod2 key", + "color-mod3": "Mod3 key", + "color-mod5": "Mod5 key", + "mod-label-hint": "Mod2 / Mod3 / Mod5 sind rohe X11-Modifikatoren (z. B. legt caps:hyper Caps auf Mod3). Nutze das Umbenennen-Feld, um ihnen eine verständliche Bezeichnung wie „Hyper“ zu geben. Leer lassen, um den rohen Mod-Namen anzuzeigen.", + "color-paste-hint": "Tipp: Kopiere eine Hex-Farbe (z. B. #FF6B6B) in die Zwischenablage – in der Pille erscheint ein Einfügen-Symbol für schnelles Einfügen.", + "keybind-ipc-command": "qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "hyprland-lua-path": "Hyprland Lua Path", + "hyprland-parser-mode": "Parser:", + "hyprland-lua-hint": "Auto: uses hyprland.lua via `hyprctl binds` when it exists, otherwise the legacy .conf parser.", + "undescribed-title": "Undescribed Binds", + "overrides-summary": "Hidden: {hidden} - Custom descriptions: {custom}", + "restore-hidden": "Restore hidden", + "restore-hidden-message": "Hidden binds restored.", + "clear-overrides": "Clear all overrides", + "clear-overrides-message": "All bind overrides cleared." + }, + "barwidget": { + "tooltip": "Tastenkürzel-Spickzettel", + "open": "Spickzettel öffnen", + "settings": "Plugin-Einstellungen" + } +} diff --git a/noctalia/.config/plugins/keybind-cheatsheet/i18n/en.json b/noctalia/.config/plugins/keybind-cheatsheet/i18n/en.json new file mode 100644 index 0000000..483bba1 --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/i18n/en.json @@ -0,0 +1,117 @@ +{ + "default-category": "Keybinds", + "error": { + "unsupported-compositor": "Unsupported Compositor", + "sway-not-supported": "Sway is not yet supported", + "sway-detail": "Sway support may be added in a future update (similar format to Hyprland)", + "labwc-not-supported": "LabWC is not supported", + "labwc-detail": "LabWC uses XML config format which is incompatible with this plugin", + "unknown-compositor": "Unknown compositor detected", + "unknown-detail": "This plugin only supports Hyprland and Niri compositors", + "hyprctl-failed": "hyprctl binds failed - is Hyprland running?" + }, + "panel": { + "loading": "Loading...", + "loading-timeout": "Loading timed out. Click Refresh to retry.", + "title": "Keymap", + "title-hyprland": "Hyprland Keymap", + "title-niri": "Niri Keymap", + "title-mango": "MangoWC Keymap", + "workspace-switching": "WORKSPACES - SWITCHING", + "workspace-moving": "WORKSPACES - MOVING", + "workspace-mouse": "WORKSPACES - MOUSE", + "search-placeholder": "Search keybinds...", + "no-description": "(no description)", + "other": "Other", + "undescribed": "Without Description", + "add-description": "Add description", + "add-description-placeholder": "Description...", + "save-description": "Save", + "cancel": "Cancel", + "hide-bind": "Hide this bind" + }, + "settings": { + "title": "Keybind Cheatsheet Settings", + "description": "Configure the appearance and keybind sources for the keybind cheatsheet panel.", + "tab-general": "General", + "tab-appearance": "Appearance", + "window-size": "Window Size", + "width": "Width:", + "height": "Height:", + "auto-height": "Auto", + "auto-height-hint": "When 'Auto' is enabled, height adjusts automatically based on content.", + "layout": "Layout", + "columns": "Number of columns:", + "columns-hint": "More columns work better with wider windows.", + "parsing-options": "Parsing Options", + "merge-sequential": "Merge sequential keybinds", + "merge-sequential-hint": "Combine consecutive numeric keybinds (e.g. Super+1..5 -> Workspace 1..5) into a single ranged entry.", + "show-undescribed": "Show undescribed binds", + "show-undescribed-hint": "Include keybinds that have no comment description. They will be auto-labeled with the action.", + "split-workspaces": "Split large workspace category", + "split-workspaces-hint": "When a workspace category exceeds the threshold, split it into Switching, Moving, and Mouse subcategories.", + "split-threshold": "Split threshold (binds):", + "config-paths": "Configuration Files", + "config-paths-description": "Specify custom paths to your keybind configuration files. The plugin will automatically parse the appropriate file based on your compositor and follow all include/source directives.", + "hyprland-format-hint": "Expected format: bind = $mod, KEY, action #\"description\"\nCategories: # 1. CATEGORY NAME", + "niri-format-hint": "Categories are created with: // #\"Category Name\"\nDescriptions: hotkey-overlay-title=\"description\"", + "actions": "Actions", + "refresh-keybinds": "Refresh Keybinds", + "refresh-message": "Keybinds will be reloaded when you open the panel.", + "reset-defaults": "Reset to Defaults", + "reset-message": "Settings have been reset to defaults.", + "saved": "Settings saved successfully.", + "keybind-setup": "Keybind Setup", + "keybind-setup-description": "To open the keybind cheatsheet with a keyboard shortcut, add this bind to your compositor config:", + "keybind-ipc-command": "qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "keybind-example-hyprland": "Hyprland example: bind = $mod, F1, exec, qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "keybind-example-niri": "Niri example: Super+F1 { spawn \"qs\" \"-c\" \"noctalia-shell\" \"ipc\" \"call\" \"plugin\" \"togglePanel\" \"keybind-cheatsheet\"; }", + "mod-var": "Mod Key Variable", + "mod-var-hint": "The variable name used for your Super key (e.g., $mod or $mainMod).", + "width-range": "px (400-3000)", + "height-range": "px (300-2000)", + "hyprland-path": "Hyprland Path", + "niri-path": "Niri Path", + "mango-path": "Mango Path", + "mango-format-hint": "Expected format: bind=MODS,KEY,ACTION,ARGS\nCategories: # Category Name (horizontal rules stripped)\nOptional per-bind description: trailing #\"description\"", + "keybind-example-mango": "Mango example: bind=SUPER,F1,spawn_shell,noctalia-shell ipc call plugin:keybind-cheatsheet toggle", + "appearance-section": "Appearance", + "appearance-description": "Customize the colors used for special key categories. Changes apply immediately and persist when you click Apply.", + "preview-section": "Live Preview", + "color-pickers": "Key Colors", + "color-super": "Super / Mod key", + "color-ctrl": "Ctrl key", + "color-shift": "Shift key", + "color-alt": "Alt-modifier keys", + "color-xf86": "XF86 multimedia keys", + "color-print": "Print / Screenshot keys", + "color-numeric": "Numeric keys (workspaces)", + "color-mouse": "Mouse buttons", + "color-default": "Default letter keys", + "color-label": "Key label text", + "color-description": "Description text", + "color-theme-hint": "Leave empty to use theme accent color.", + "color-paste-hint": "Tip: copy any hex color (e.g. #FF6B6B) to your clipboard and a paste icon will appear inside the pill for quick paste.", + "reset-color": "Reset to default", + "reset-all-colors": "Reset all colors", + "color-auto": "auto", + "color-mod2": "Mod2 key", + "color-mod3": "Mod3 key", + "color-mod5": "Mod5 key", + "mod-label-hint": "Mod2 / Mod3 / Mod5 are raw X11 modifiers (e.g. caps:hyper puts Caps on Mod3). Use the rename field to give them a friendly label such as \"Hyper\". Leave it empty to show the raw mod name.", + "hyprland-lua-path": "Hyprland Lua Path", + "hyprland-parser-mode": "Parser:", + "hyprland-lua-hint": "Auto: uses hyprland.lua via `hyprctl binds` when it exists, otherwise the legacy .conf parser.", + "undescribed-title": "Undescribed Binds", + "overrides-summary": "Hidden: {hidden} - Custom descriptions: {custom}", + "restore-hidden": "Restore hidden", + "restore-hidden-message": "Hidden binds restored.", + "clear-overrides": "Clear all overrides", + "clear-overrides-message": "All bind overrides cleared." + }, + "barwidget": { + "tooltip": "Keybind Cheatsheet", + "open": "Open Cheatsheet", + "settings": "Plugin Settings" + } +} diff --git a/noctalia/.config/plugins/keybind-cheatsheet/i18n/es.json b/noctalia/.config/plugins/keybind-cheatsheet/i18n/es.json new file mode 100644 index 0000000..8c3a7b6 --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/i18n/es.json @@ -0,0 +1,117 @@ +{ + "default-category": "Atajos de teclado", + "error": { + "unsupported-compositor": "Compositor no compatible", + "sway-not-supported": "Sway aún no es compatible", + "sway-detail": "El soporte de Sway podría agregarse en una actualización futura (formato similar a Hyprland)", + "labwc-not-supported": "LabWC no es compatible", + "labwc-detail": "LabWC usa formato de configuración XML que es incompatible con este plugin", + "unknown-compositor": "Compositor desconocido detectado", + "unknown-detail": "Este plugin solo admite los compositores Hyprland y Niri", + "hyprctl-failed": "hyprctl binds failed - is Hyprland running?" + }, + "panel": { + "loading": "Cargando...", + "loading-timeout": "Loading timed out. Click Refresh to retry.", + "title": "Mapa de teclas", + "title-hyprland": "Mapa de teclas Hyprland", + "title-niri": "Mapa de teclas Niri", + "title-mango": "MangoWC Keymap", + "workspace-switching": "ESPACIOS DE TRABAJO - CAMBIAR", + "workspace-moving": "ESPACIOS DE TRABAJO - MOVER", + "workspace-mouse": "ESPACIOS DE TRABAJO - RATÓN", + "search-placeholder": "Buscar atajos...", + "no-description": "(no description)", + "other": "Other", + "undescribed": "Without Description", + "add-description": "Add description", + "add-description-placeholder": "Description...", + "save-description": "Save", + "cancel": "Cancel", + "hide-bind": "Hide this bind" + }, + "settings": { + "title": "Configuracion de Atajos de Teclado", + "description": "Configure la apariencia y las fuentes de atajos de teclado para el panel.", + "tab-general": "General", + "tab-appearance": "Appearance", + "window-size": "Tamano de Ventana", + "width": "Ancho:", + "height": "Alto:", + "auto-height": "Auto", + "auto-height-hint": "Cuando 'Auto' esta activado, la altura se ajusta automaticamente al contenido.", + "layout": "Diseno", + "columns": "Numero de columnas:", + "columns-hint": "Mas columnas funcionan mejor con ventanas mas anchas.", + "parsing-options": "Parsing Options", + "merge-sequential": "Merge sequential keybinds", + "merge-sequential-hint": "Combine consecutive numeric keybinds (e.g. Super+1..5 -> Workspace 1..5) into a single ranged entry.", + "show-undescribed": "Show undescribed binds", + "show-undescribed-hint": "Include keybinds that have no comment description. They will be auto-labeled with the action.", + "split-workspaces": "Split large workspace category", + "split-workspaces-hint": "When a workspace category exceeds the threshold, split it into Switching, Moving, and Mouse subcategories.", + "split-threshold": "Split threshold (binds):", + "config-paths": "Archivos de Configuracion", + "config-paths-description": "Especifique rutas personalizadas a sus archivos de configuracion de atajos. El plugin seguira automaticamente todas las directivas include/source.", + "hyprland-format-hint": "Formato esperado: bind = $mod, KEY, action #\"descripcion\"\nCategorias: # 1. NOMBRE DE CATEGORIA", + "niri-format-hint": "Las categorias se crean con: // #\"Nombre de categoria\"\nDescripciones: hotkey-overlay-title=\"descripcion\"", + "actions": "Acciones", + "refresh-keybinds": "Actualizar Atajos", + "refresh-message": "Los atajos se recargaran al abrir el panel.", + "reset-defaults": "Restablecer Predeterminados", + "reset-message": "La configuracion se ha restablecido a los valores predeterminados.", + "saved": "Configuracion guardada correctamente.", + "keybind-setup": "Configuracion de Atajo", + "keybind-setup-description": "Para abrir el cheatsheet con un atajo de teclado, agrega este bind a la configuracion de tu compositor:", + "keybind-example-hyprland": "Hyprland: bind = $mod, F1, exec, qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "keybind-example-niri": "Niri: Super+F1 { spawn \"qs\" \"-c\" \"noctalia-shell\" \"ipc\" \"call\" \"plugin\" \"togglePanel\" \"keybind-cheatsheet\"; }", + "mod-var": "Variable de Tecla Mod", + "mod-var-hint": "El nombre de la variable usada para su tecla Super (ej. $mod o $mainMod).", + "width-range": "px (400-3000)", + "height-range": "px (300-2000)", + "hyprland-path": "Ruta Hyprland", + "niri-path": "Ruta Niri", + "mango-path": "Mango Path", + "mango-format-hint": "Expected format: bind=MODS,KEY,ACTION,ARGS\nCategories: # Category Name (horizontal rules stripped)\nOptional per-bind description: trailing #\"description\"", + "keybind-example-mango": "Mango example: bind=SUPER,F1,spawn_shell,noctalia-shell ipc call plugin:keybind-cheatsheet toggle", + "appearance-section": "Appearance", + "appearance-description": "Customize the colors used for special key categories. Changes apply immediately and persist when you click Apply.", + "preview-section": "Live Preview", + "color-pickers": "Key Colors", + "color-super": "Super / Mod key", + "color-ctrl": "Ctrl key", + "color-shift": "Shift key", + "color-alt": "Alt-modifier keys", + "color-xf86": "XF86 multimedia keys", + "color-print": "Print / Screenshot keys", + "color-numeric": "Numeric keys (workspaces)", + "color-mouse": "Mouse buttons", + "color-default": "Default letter keys", + "color-label": "Key label text", + "color-description": "Description text", + "color-theme-hint": "Leave empty to use theme accent color.", + "reset-color": "Reset to default", + "reset-all-colors": "Reset all colors", + "color-auto": "auto", + "color-mod2": "Mod2 key", + "color-mod3": "Mod3 key", + "color-mod5": "Mod5 key", + "mod-label-hint": "Mod2 / Mod3 / Mod5 son modificadores X11 en bruto (p. ej. caps:hyper asigna Caps a Mod3). Usa el campo de renombrado para darles una etiqueta comprensible como «Hyper». Déjalo vacío para mostrar el nombre de modificador en bruto.", + "color-paste-hint": "Consejo: copia un color hex (p. ej. #FF6B6B) al portapapeles y aparecerá un icono de pegar dentro de la pastilla.", + "keybind-ipc-command": "qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "hyprland-lua-path": "Hyprland Lua Path", + "hyprland-parser-mode": "Parser:", + "hyprland-lua-hint": "Auto: uses hyprland.lua via `hyprctl binds` when it exists, otherwise the legacy .conf parser.", + "undescribed-title": "Undescribed Binds", + "overrides-summary": "Hidden: {hidden} - Custom descriptions: {custom}", + "restore-hidden": "Restore hidden", + "restore-hidden-message": "Hidden binds restored.", + "clear-overrides": "Clear all overrides", + "clear-overrides-message": "All bind overrides cleared." + }, + "barwidget": { + "tooltip": "Hoja de trucos de atajos", + "open": "Abrir hoja de trucos", + "settings": "Configuración del plugin" + } +} diff --git a/noctalia/.config/plugins/keybind-cheatsheet/i18n/fr.json b/noctalia/.config/plugins/keybind-cheatsheet/i18n/fr.json new file mode 100644 index 0000000..b70de98 --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/i18n/fr.json @@ -0,0 +1,117 @@ +{ + "default-category": "Raccourcis clavier", + "error": { + "unsupported-compositor": "Compositeur non pris en charge", + "sway-not-supported": "Sway n'est pas encore pris en charge", + "sway-detail": "La prise en charge de Sway pourrait être ajoutée dans une future mise à jour (format similaire à Hyprland)", + "labwc-not-supported": "LabWC n'est pas pris en charge", + "labwc-detail": "LabWC utilise un format de configuration XML incompatible avec ce plugin", + "unknown-compositor": "Compositeur inconnu détecté", + "unknown-detail": "Ce plugin ne prend en charge que les compositeurs Hyprland et Niri", + "hyprctl-failed": "hyprctl binds failed - is Hyprland running?" + }, + "panel": { + "loading": "Chargement...", + "loading-timeout": "Loading timed out. Click Refresh to retry.", + "title": "Carte des touches", + "title-hyprland": "Carte des touches Hyprland", + "title-niri": "Carte des touches Niri", + "title-mango": "MangoWC Keymap", + "workspace-switching": "ESPACES DE TRAVAIL - BASCULER", + "workspace-moving": "ESPACES DE TRAVAIL - DÉPLACER", + "workspace-mouse": "ESPACES DE TRAVAIL - SOURIS", + "search-placeholder": "Rechercher des raccourcis...", + "no-description": "(no description)", + "other": "Other", + "undescribed": "Without Description", + "add-description": "Add description", + "add-description-placeholder": "Description...", + "save-description": "Save", + "cancel": "Cancel", + "hide-bind": "Hide this bind" + }, + "settings": { + "title": "Parametres des Raccourcis Clavier", + "description": "Configurez l'apparence et les sources des raccourcis clavier pour le panneau.", + "tab-general": "General", + "tab-appearance": "Appearance", + "window-size": "Taille de la Fenetre", + "width": "Largeur:", + "height": "Hauteur:", + "auto-height": "Auto", + "auto-height-hint": "Lorsque 'Auto' est active, la hauteur s'ajuste automatiquement au contenu.", + "layout": "Disposition", + "columns": "Nombre de colonnes:", + "columns-hint": "Plus de colonnes fonctionnent mieux avec des fenetres plus larges.", + "parsing-options": "Parsing Options", + "merge-sequential": "Merge sequential keybinds", + "merge-sequential-hint": "Combine consecutive numeric keybinds (e.g. Super+1..5 -> Workspace 1..5) into a single ranged entry.", + "show-undescribed": "Show undescribed binds", + "show-undescribed-hint": "Include keybinds that have no comment description. They will be auto-labeled with the action.", + "split-workspaces": "Split large workspace category", + "split-workspaces-hint": "When a workspace category exceeds the threshold, split it into Switching, Moving, and Mouse subcategories.", + "split-threshold": "Split threshold (binds):", + "config-paths": "Fichiers de Configuration", + "config-paths-description": "Specifiez des chemins personnalises vers vos fichiers de configuration de raccourcis. Le plugin suivra automatiquement toutes les directives include/source.", + "hyprland-format-hint": "Format attendu: bind = $mod, KEY, action #\"description\"\nCategories: # 1. NOM DE CATEGORIE", + "niri-format-hint": "Les categories sont creees avec: // #\"Nom de categorie\"\nDescriptions: hotkey-overlay-title=\"description\"", + "actions": "Actions", + "refresh-keybinds": "Actualiser les Raccourcis", + "refresh-message": "Les raccourcis seront recharges a l'ouverture du panneau.", + "reset-defaults": "Reinitialiser par Defaut", + "reset-message": "Les parametres ont ete reinitialises par defaut.", + "saved": "Parametres enregistres avec succes.", + "keybind-setup": "Configuration du Raccourci", + "keybind-setup-description": "Pour ouvrir le cheatsheet avec un raccourci clavier, ajoutez ce bind a la configuration de votre compositeur:", + "keybind-example-hyprland": "Hyprland: bind = $mod, F1, exec, qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "keybind-example-niri": "Niri: Super+F1 { spawn \"qs\" \"-c\" \"noctalia-shell\" \"ipc\" \"call\" \"plugin\" \"togglePanel\" \"keybind-cheatsheet\"; }", + "mod-var": "Variable de Touche Mod", + "mod-var-hint": "Le nom de la variable utilisée pour votre touche Super (ex. $mod ou $mainMod).", + "width-range": "px (400-3000)", + "height-range": "px (300-2000)", + "hyprland-path": "Chemin Hyprland", + "niri-path": "Chemin Niri", + "mango-path": "Mango Path", + "mango-format-hint": "Expected format: bind=MODS,KEY,ACTION,ARGS\nCategories: # Category Name (horizontal rules stripped)\nOptional per-bind description: trailing #\"description\"", + "keybind-example-mango": "Mango example: bind=SUPER,F1,spawn_shell,noctalia-shell ipc call plugin:keybind-cheatsheet toggle", + "appearance-section": "Appearance", + "appearance-description": "Customize the colors used for special key categories. Changes apply immediately and persist when you click Apply.", + "preview-section": "Live Preview", + "color-pickers": "Key Colors", + "color-super": "Super / Mod key", + "color-ctrl": "Ctrl key", + "color-shift": "Shift key", + "color-alt": "Alt-modifier keys", + "color-xf86": "XF86 multimedia keys", + "color-print": "Print / Screenshot keys", + "color-numeric": "Numeric keys (workspaces)", + "color-mouse": "Mouse buttons", + "color-default": "Default letter keys", + "color-label": "Key label text", + "color-description": "Description text", + "color-theme-hint": "Leave empty to use theme accent color.", + "reset-color": "Reset to default", + "reset-all-colors": "Reset all colors", + "color-auto": "auto", + "color-mod2": "Mod2 key", + "color-mod3": "Mod3 key", + "color-mod5": "Mod5 key", + "mod-label-hint": "Mod2 / Mod3 / Mod5 sont des modificateurs X11 bruts (par ex. caps:hyper place Caps sur Mod3). Utilisez le champ de renommage pour leur donner un libellé lisible comme « Hyper ». Laissez-le vide pour afficher le nom brut du modificateur.", + "color-paste-hint": "Astuce : copiez une couleur hex (ex. #FF6B6B) dans le presse-papiers et une icône de collage apparaîtra dans la pastille.", + "keybind-ipc-command": "qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "hyprland-lua-path": "Hyprland Lua Path", + "hyprland-parser-mode": "Parser:", + "hyprland-lua-hint": "Auto: uses hyprland.lua via `hyprctl binds` when it exists, otherwise the legacy .conf parser.", + "undescribed-title": "Undescribed Binds", + "overrides-summary": "Hidden: {hidden} - Custom descriptions: {custom}", + "restore-hidden": "Restore hidden", + "restore-hidden-message": "Hidden binds restored.", + "clear-overrides": "Clear all overrides", + "clear-overrides-message": "All bind overrides cleared." + }, + "barwidget": { + "tooltip": "Aide-mémoire des raccourcis", + "open": "Ouvrir l'aide-mémoire", + "settings": "Paramètres du plugin" + } +} diff --git a/noctalia/.config/plugins/keybind-cheatsheet/i18n/hn.json b/noctalia/.config/plugins/keybind-cheatsheet/i18n/hn.json new file mode 100644 index 0000000..f1f0a0d --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/i18n/hn.json @@ -0,0 +1,117 @@ +{ + "default-category": "कुञ्जी बाइन्डहरू", + "error": { + "unsupported-compositor": "असमर्थित कम्पोजिटर", + "sway-not-supported": "Sway अझै समर्थित छैन", + "sway-detail": "Sway समर्थन भविष्यको अपडेटमा थप्न सकिन्छ (Hyprland जस्तै ढाँचा)", + "labwc-not-supported": "LabWC समर्थित छैन", + "labwc-detail": "LabWC ले XML कन्फिगरेसन ढाँचा प्रयोग गर्दछ जुन यो प्लगइनसँग मिल्दैन", + "unknown-compositor": "अज्ञात कम्पोजिटर पत्ता लाग्यो", + "unknown-detail": "यो प्लगइनले Hyprland र Niri कम्पोजिटरहरू मात्र समर्थन गर्दछ", + "hyprctl-failed": "hyprctl binds failed - is Hyprland running?" + }, + "panel": { + "loading": "लोड हो रहा है...", + "loading-timeout": "Loading timed out. Click Refresh to retry.", + "title": "कीमैप", + "title-hyprland": "Hyprland कीमैप", + "title-niri": "Niri कीमैप", + "title-mango": "MangoWC Keymap", + "workspace-switching": "कार्यक्षेत्र - स्विच करना", + "workspace-moving": "कार्यक्षेत्र - स्थानांतरण", + "workspace-mouse": "कार्यक्षेत्र - माउस", + "search-placeholder": "कीबाइंड खोजें...", + "no-description": "(no description)", + "other": "Other", + "undescribed": "Without Description", + "add-description": "Add description", + "add-description-placeholder": "Description...", + "save-description": "Save", + "cancel": "Cancel", + "hide-bind": "Hide this bind" + }, + "settings": { + "title": "कीबाइंड चीटशीट सेटिंग्स", + "description": "कीबाइंड चीटशीट पैनल के लिए उपस्थिति और कीबाइंड स्रोतों को कॉन्फ़िगर करें।", + "tab-general": "General", + "tab-appearance": "Appearance", + "window-size": "विंडो आकार", + "width": "चौड़ाई:", + "height": "ऊंचाई:", + "auto-height": "स्वचालित", + "auto-height-hint": "जब 'स्वचालित' सक्षम है, ऊंचाई सामग्री के आधार पर स्वचालित रूप से समायोजित होती है।", + "layout": "लेआउट", + "columns": "कॉलम की संख्या:", + "columns-hint": "अधिक कॉलम चौड़ी विंडो के साथ बेहतर काम करते हैं।", + "parsing-options": "Parsing Options", + "merge-sequential": "Merge sequential keybinds", + "merge-sequential-hint": "Combine consecutive numeric keybinds (e.g. Super+1..5 -> Workspace 1..5) into a single ranged entry.", + "show-undescribed": "Show undescribed binds", + "show-undescribed-hint": "Include keybinds that have no comment description. They will be auto-labeled with the action.", + "split-workspaces": "Split large workspace category", + "split-workspaces-hint": "When a workspace category exceeds the threshold, split it into Switching, Moving, and Mouse subcategories.", + "split-threshold": "Split threshold (binds):", + "config-paths": "कॉन्फ़िगरेशन फ़ाइलें", + "config-paths-description": "अपनी कीबाइंड कॉन्फ़िगरेशन फ़ाइलों के लिए कस्टम पथ निर्दिष्ट करें। प्लगइन स्वचालित रूप से आपके कंपोजिटर के आधार पर उपयुक्त फ़ाइल को पार्स करेगा।", + "hyprland-format-hint": "अपेक्षित प्रारूप: bind = $mod, KEY, action #\"description\"\nश्रेणियाँ: # 1. CATEGORY NAME", + "niri-format-hint": "श्रेणियाँ: // #\"Category Name\"\nविवरण: hotkey-overlay-title=\"description\"", + "actions": "क्रियाएं", + "refresh-keybinds": "कीबाइंड रीफ्रेश करें", + "refresh-message": "जब आप पैनल खोलेंगे तो कीबाइंड पुनः लोड होंगे।", + "reset-defaults": "डिफ़ॉल्ट पर रीसेट करें", + "reset-message": "सेटिंग्स डिफ़ॉल्ट पर रीसेट कर दी गई हैं।", + "saved": "सेटिंग्स सफलतापूर्वक सहेजी गईं।", + "keybind-setup": "कीबाइंड सेटअप", + "keybind-setup-description": "कीबोर्ड शॉर्टकट के साथ कीबाइंड चीटशीट खोलने के लिए, अपने कंपोजिटर कॉन्फ़िग में यह बाइंड जोड़ें:", + "keybind-example-hyprland": "Hyprland उदाहरण: bind = $mod, F1, exec, qs -c \"noctalia-shell\" ipc call plugin togglePanel keybind-cheatsheet", + "keybind-example-niri": "Niri उदाहरण: Super+F1 { spawn \"qs\" \"-c\" \"noctalia-shell\" \"ipc\" \"call\" \"plugin\" \"togglePanel\" \"keybind-cheatsheet\"; }", + "mod-var": "मॉड कुंजी वेरिएबल", + "mod-var-hint": "आपकी Super कुंजी के लिए उपयोग किया जाने वाला वेरिएबल नाम (जैसे, $mod या $mainMod)।", + "width-range": "px (400-3000)", + "height-range": "px (300-2000)", + "hyprland-path": "Hyprland पथ", + "niri-path": "Niri पथ", + "mango-path": "Mango Path", + "mango-format-hint": "Expected format: bind=MODS,KEY,ACTION,ARGS\nCategories: # Category Name (horizontal rules stripped)\nOptional per-bind description: trailing #\"description\"", + "keybind-example-mango": "Mango example: bind=SUPER,F1,spawn_shell,noctalia-shell ipc call plugin:keybind-cheatsheet toggle", + "appearance-section": "Appearance", + "appearance-description": "Customize the colors used for special key categories. Changes apply immediately and persist when you click Apply.", + "preview-section": "Live Preview", + "color-pickers": "Key Colors", + "color-super": "Super / Mod key", + "color-ctrl": "Ctrl key", + "color-shift": "Shift key", + "color-alt": "Alt-modifier keys", + "color-xf86": "XF86 multimedia keys", + "color-print": "Print / Screenshot keys", + "color-numeric": "Numeric keys (workspaces)", + "color-mouse": "Mouse buttons", + "color-default": "Default letter keys", + "color-label": "Key label text", + "color-description": "Description text", + "color-theme-hint": "Leave empty to use theme accent color.", + "reset-color": "Reset to default", + "reset-all-colors": "Reset all colors", + "color-auto": "auto", + "color-mod2": "Mod2 key", + "color-mod3": "Mod3 key", + "color-mod5": "Mod5 key", + "mod-label-hint": "Mod2 / Mod3 / Mod5 कच्चे X11 मॉडिफ़ायर हैं (जैसे caps:hyper, Caps को Mod3 पर सेट करता है)। इन्हें \"Hyper\" जैसा आसान लेबल देने के लिए नाम बदलने वाले फ़ील्ड का उपयोग करें। कच्चा मॉड नाम दिखाने के लिए इसे खाली छोड़ें।", + "color-paste-hint": "सुझाव: क्लिपबोर्ड में कोई हेक्स रंग (जैसे #FF6B6B) कॉपी करें, पिल में पेस्ट आइकन दिखेगा।", + "keybind-ipc-command": "qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "hyprland-lua-path": "Hyprland Lua Path", + "hyprland-parser-mode": "Parser:", + "hyprland-lua-hint": "Auto: uses hyprland.lua via `hyprctl binds` when it exists, otherwise the legacy .conf parser.", + "undescribed-title": "Undescribed Binds", + "overrides-summary": "Hidden: {hidden} - Custom descriptions: {custom}", + "restore-hidden": "Restore hidden", + "restore-hidden-message": "Hidden binds restored.", + "clear-overrides": "Clear all overrides", + "clear-overrides-message": "All bind overrides cleared." + }, + "barwidget": { + "tooltip": "कीबाइंड चीटशीट", + "open": "चीटशीट खोलें", + "settings": "प्लगइन सेटिंग्स" + } +} diff --git a/noctalia/.config/plugins/keybind-cheatsheet/i18n/hu.json b/noctalia/.config/plugins/keybind-cheatsheet/i18n/hu.json new file mode 100644 index 0000000..b1c3e96 --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/i18n/hu.json @@ -0,0 +1,117 @@ +{ + "default-category": "Billentyűparancsok", + "error": { + "unsupported-compositor": "Nem támogatott kompozitor", + "sway-not-supported": "A Sway még nem támogatott", + "sway-detail": "A Sway támogatás egy jövőbeli frissítésben hozzáadható (hasonló formátum, mint a Hyprland)", + "labwc-not-supported": "A LabWC nem támogatott", + "labwc-detail": "A LabWC XML konfigurációs formátumot használ, amely nem kompatibilis ezzel a bővítménnyel", + "unknown-compositor": "Ismeretlen kompozitor észlelve", + "unknown-detail": "Ez a bővítmény csak a Hyprland és Niri kompozitorokat támogatja", + "hyprctl-failed": "hyprctl binds failed - is Hyprland running?" + }, + "panel": { + "loading": "Betoltes...", + "loading-timeout": "Loading timed out. Click Refresh to retry.", + "title": "Billentyűzetkiosztás", + "title-hyprland": "Hyprland billentyűzetkiosztás", + "title-niri": "Niri billentyűzetkiosztás", + "title-mango": "MangoWC Keymap", + "workspace-switching": "MUNKATERÜLETEK - VÁLTÁS", + "workspace-moving": "MUNKATERÜLETEK - ÁTHELYEZÉS", + "workspace-mouse": "MUNKATERÜLETEK - EGÉR", + "search-placeholder": "Billentyűparancsok keresése...", + "no-description": "(no description)", + "other": "Other", + "undescribed": "Without Description", + "add-description": "Add description", + "add-description-placeholder": "Description...", + "save-description": "Save", + "cancel": "Cancel", + "hide-bind": "Hide this bind" + }, + "settings": { + "title": "Billentyuparancsok Beallitasai", + "description": "Allitsa be a billentyuparancsok megjeleniteset es forrасait a panelhez.", + "tab-general": "General", + "tab-appearance": "Appearance", + "window-size": "Ablak Merete", + "width": "Szelesseg:", + "height": "Magassag:", + "auto-height": "Auto", + "auto-height-hint": "Ha az 'Automatikus' be van kapcsolva, a magassag automatikusan igazodik a tartalomhoz.", + "layout": "Elrendezes", + "columns": "Oszlopok szama:", + "columns-hint": "Tobb oszlop jobban mukodik szelessebb ablakokkal.", + "parsing-options": "Parsing Options", + "merge-sequential": "Merge sequential keybinds", + "merge-sequential-hint": "Combine consecutive numeric keybinds (e.g. Super+1..5 -> Workspace 1..5) into a single ranged entry.", + "show-undescribed": "Show undescribed binds", + "show-undescribed-hint": "Include keybinds that have no comment description. They will be auto-labeled with the action.", + "split-workspaces": "Split large workspace category", + "split-workspaces-hint": "When a workspace category exceeds the threshold, split it into Switching, Moving, and Mouse subcategories.", + "split-threshold": "Split threshold (binds):", + "config-paths": "Konfiguracios Fajlok", + "config-paths-description": "Adjon meg egyedi utvonalakat a billentyuparancs-konfiguracios fajlokhoz. A beepulo automatikusan koveti az osszes include/source direktívat.", + "hyprland-format-hint": "Elvart formatum: bind = $mod, KEY, action #\"leiras\"\nKategoriak: # 1. KATEGORIA NEV", + "niri-format-hint": "A kategoriakat így hozzuk letre: // #\"Kategoria nev\"\nLeirasok: hotkey-overlay-title=\"leiras\"", + "actions": "Muveletek", + "refresh-keybinds": "Billentyuparancsok Frissitese", + "refresh-message": "A billentyuparancsok ujratoltodnek a panel megnyitasakor.", + "reset-defaults": "Alapertelmezettek Visszaallitasa", + "reset-message": "A beallitasok visszaallitva az alapertelmezettre.", + "saved": "Beallitasok sikeresen mentve.", + "keybind-setup": "Billentyuparancs Beallitasa", + "keybind-setup-description": "A cheatsheet billentyuparancsal valo megnyitasahoz adja hozzá ezt a bind-ot a kompozitor konfiguraciojához:", + "keybind-example-hyprland": "Hyprland: bind = $mod, F1, exec, qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "keybind-example-niri": "Niri: Super+F1 { spawn \"qs\" \"-c\" \"noctalia-shell\" \"ipc\" \"call\" \"plugin\" \"togglePanel\" \"keybind-cheatsheet\"; }", + "mod-var": "Mod Billentyuvaltozo", + "mod-var-hint": "A Super billentyuhoz hasznalt valtozó neve (pl. $mod vagy $mainMod).", + "width-range": "px (400-3000)", + "height-range": "px (300-2000)", + "hyprland-path": "Hyprland útvonal", + "niri-path": "Niri útvonal", + "mango-path": "Mango Path", + "mango-format-hint": "Expected format: bind=MODS,KEY,ACTION,ARGS\nCategories: # Category Name (horizontal rules stripped)\nOptional per-bind description: trailing #\"description\"", + "keybind-example-mango": "Mango example: bind=SUPER,F1,spawn_shell,noctalia-shell ipc call plugin:keybind-cheatsheet toggle", + "appearance-section": "Appearance", + "appearance-description": "Customize the colors used for special key categories. Changes apply immediately and persist when you click Apply.", + "preview-section": "Live Preview", + "color-pickers": "Key Colors", + "color-super": "Super / Mod key", + "color-ctrl": "Ctrl key", + "color-shift": "Shift key", + "color-alt": "Alt-modifier keys", + "color-xf86": "XF86 multimedia keys", + "color-print": "Print / Screenshot keys", + "color-numeric": "Numeric keys (workspaces)", + "color-mouse": "Mouse buttons", + "color-default": "Default letter keys", + "color-label": "Key label text", + "color-description": "Description text", + "color-theme-hint": "Leave empty to use theme accent color.", + "reset-color": "Reset to default", + "reset-all-colors": "Reset all colors", + "color-auto": "auto", + "color-mod2": "Mod2 key", + "color-mod3": "Mod3 key", + "color-mod5": "Mod5 key", + "mod-label-hint": "A Mod2 / Mod3 / Mod5 nyers X11 módosítók (pl. a caps:hyper a Capset a Mod3-ra teszi). Az átnevezés mezővel adhatsz nekik érthető címkét, például „Hyper“. Hagyd üresen a nyers módosítónév megjelenítéséhez.", + "color-paste-hint": "Tipp: másolj egy hex színt (pl. #FF6B6B) a vágólapra, és a pirulában megjelenik egy beillesztés ikon.", + "keybind-ipc-command": "qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "hyprland-lua-path": "Hyprland Lua Path", + "hyprland-parser-mode": "Parser:", + "hyprland-lua-hint": "Auto: uses hyprland.lua via `hyprctl binds` when it exists, otherwise the legacy .conf parser.", + "undescribed-title": "Undescribed Binds", + "overrides-summary": "Hidden: {hidden} - Custom descriptions: {custom}", + "restore-hidden": "Restore hidden", + "restore-hidden-message": "Hidden binds restored.", + "clear-overrides": "Clear all overrides", + "clear-overrides-message": "All bind overrides cleared." + }, + "barwidget": { + "tooltip": "Billentyűparancs súgó", + "open": "Súgó megnyitása", + "settings": "Plugin beállítások" + } +} diff --git a/noctalia/.config/plugins/keybind-cheatsheet/i18n/it.json b/noctalia/.config/plugins/keybind-cheatsheet/i18n/it.json new file mode 100644 index 0000000..d1f2b14 --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/i18n/it.json @@ -0,0 +1,117 @@ +{ + "default-category": "Scorciatoie da tastiera", + "error": { + "unsupported-compositor": "Compositor non supportato", + "sway-not-supported": "Sway non è ancora supportato", + "sway-detail": "Il supporto per Sway potrebbe essere aggiunto in un aggiornamento futuro (formato simile a Hyprland)", + "labwc-not-supported": "LabWC non è supportato", + "labwc-detail": "LabWC utilizza un formato di configurazione XML incompatibile con questo plugin", + "unknown-compositor": "Rilevato compositor sconosciuto", + "unknown-detail": "Questo plugin supporta solo i compositor Hyprland e Niri", + "hyprctl-failed": "hyprctl binds failed - is Hyprland running?" + }, + "panel": { + "loading": "Caricamento...", + "loading-timeout": "Loading timed out. Click Refresh to retry.", + "title": "Mappa tasti", + "title-hyprland": "Mappa tasti Hyprland", + "title-niri": "Mappa tasti Niri", + "title-mango": "MangoWC Keymap", + "workspace-switching": "SPAZI DI LAVORO - CAMBIO", + "workspace-moving": "SPAZI DI LAVORO - SPOSTAMENTO", + "workspace-mouse": "SPAZI DI LAVORO - MOUSE", + "search-placeholder": "Cerca scorciatoie...", + "no-description": "(no description)", + "other": "Other", + "undescribed": "Without Description", + "add-description": "Add description", + "add-description-placeholder": "Description...", + "save-description": "Save", + "cancel": "Cancel", + "hide-bind": "Hide this bind" + }, + "settings": { + "title": "Impostazioni Scorciatoie da Tastiera", + "description": "Configura l'aspetto e le fonti delle scorciatoie per il pannello.", + "tab-general": "General", + "tab-appearance": "Appearance", + "window-size": "Dimensione Finestra", + "width": "Larghezza:", + "height": "Altezza:", + "auto-height": "Auto", + "auto-height-hint": "Quando 'Auto' e attivo, l'altezza si adatta automaticamente al contenuto.", + "layout": "Layout", + "columns": "Numero di colonne:", + "columns-hint": "Piu colonne funzionano meglio con finestre piu larghe.", + "parsing-options": "Parsing Options", + "merge-sequential": "Merge sequential keybinds", + "merge-sequential-hint": "Combine consecutive numeric keybinds (e.g. Super+1..5 -> Workspace 1..5) into a single ranged entry.", + "show-undescribed": "Show undescribed binds", + "show-undescribed-hint": "Include keybinds that have no comment description. They will be auto-labeled with the action.", + "split-workspaces": "Split large workspace category", + "split-workspaces-hint": "When a workspace category exceeds the threshold, split it into Switching, Moving, and Mouse subcategories.", + "split-threshold": "Split threshold (binds):", + "config-paths": "File di Configurazione", + "config-paths-description": "Specifica percorsi personalizzati per i file di configurazione delle scorciatoie. Il plugin seguira automaticamente tutte le direttive include/source.", + "hyprland-format-hint": "Formato previsto: bind = $mod, KEY, action #\"descrizione\"\nCategorie: # 1. NOME CATEGORIA", + "niri-format-hint": "Le categorie vengono create con: // #\"Nome categoria\"\nDescrizioni: hotkey-overlay-title=\"descrizione\"", + "actions": "Azioni", + "refresh-keybinds": "Aggiorna Scorciatoie", + "refresh-message": "Le scorciatoie verranno ricaricate all'apertura del pannello.", + "reset-defaults": "Ripristina Predefiniti", + "reset-message": "Le impostazioni sono state ripristinate ai valori predefiniti.", + "saved": "Impostazioni salvate con successo.", + "keybind-setup": "Configurazione Scorciatoia", + "keybind-setup-description": "Per aprire il cheatsheet con una scorciatoia, aggiungi questo bind alla configurazione del tuo compositor:", + "keybind-example-hyprland": "Hyprland: bind = $mod, F1, exec, qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "keybind-example-niri": "Niri: Super+F1 { spawn \"qs\" \"-c\" \"noctalia-shell\" \"ipc\" \"call\" \"plugin\" \"togglePanel\" \"keybind-cheatsheet\"; }", + "mod-var": "Variabile Tasto Mod", + "mod-var-hint": "Il nome della variabile usata per il tasto Super (es. $mod o $mainMod).", + "width-range": "px (400-3000)", + "height-range": "px (300-2000)", + "hyprland-path": "Percorso Hyprland", + "niri-path": "Percorso Niri", + "mango-path": "Mango Path", + "mango-format-hint": "Expected format: bind=MODS,KEY,ACTION,ARGS\nCategories: # Category Name (horizontal rules stripped)\nOptional per-bind description: trailing #\"description\"", + "keybind-example-mango": "Mango example: bind=SUPER,F1,spawn_shell,noctalia-shell ipc call plugin:keybind-cheatsheet toggle", + "appearance-section": "Appearance", + "appearance-description": "Customize the colors used for special key categories. Changes apply immediately and persist when you click Apply.", + "preview-section": "Live Preview", + "color-pickers": "Key Colors", + "color-super": "Super / Mod key", + "color-ctrl": "Ctrl key", + "color-shift": "Shift key", + "color-alt": "Alt-modifier keys", + "color-xf86": "XF86 multimedia keys", + "color-print": "Print / Screenshot keys", + "color-numeric": "Numeric keys (workspaces)", + "color-mouse": "Mouse buttons", + "color-default": "Default letter keys", + "color-label": "Key label text", + "color-description": "Description text", + "color-theme-hint": "Leave empty to use theme accent color.", + "reset-color": "Reset to default", + "reset-all-colors": "Reset all colors", + "color-auto": "auto", + "color-mod2": "Mod2 key", + "color-mod3": "Mod3 key", + "color-mod5": "Mod5 key", + "mod-label-hint": "Mod2 / Mod3 / Mod5 sono modificatori X11 grezzi (es. caps:hyper assegna Caps a Mod3). Usa il campo di rinomina per dare loro un'etichetta comprensibile come «Hyper». Lascialo vuoto per mostrare il nome grezzo del modificatore.", + "color-paste-hint": "Suggerimento: copia un colore hex (es. #FF6B6B) negli appunti e apparirà un'icona di incolla dentro la pillola.", + "keybind-ipc-command": "qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "hyprland-lua-path": "Hyprland Lua Path", + "hyprland-parser-mode": "Parser:", + "hyprland-lua-hint": "Auto: uses hyprland.lua via `hyprctl binds` when it exists, otherwise the legacy .conf parser.", + "undescribed-title": "Undescribed Binds", + "overrides-summary": "Hidden: {hidden} - Custom descriptions: {custom}", + "restore-hidden": "Restore hidden", + "restore-hidden-message": "Hidden binds restored.", + "clear-overrides": "Clear all overrides", + "clear-overrides-message": "All bind overrides cleared." + }, + "barwidget": { + "tooltip": "Scheda scorciatoie", + "open": "Apri scheda", + "settings": "Impostazioni plugin" + } +} diff --git a/noctalia/.config/plugins/keybind-cheatsheet/i18n/ja.json b/noctalia/.config/plugins/keybind-cheatsheet/i18n/ja.json new file mode 100644 index 0000000..8a6afde --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/i18n/ja.json @@ -0,0 +1,117 @@ +{ + "default-category": "キーバインド", + "error": { + "unsupported-compositor": "サポートされていないコンポジター", + "sway-not-supported": "Swayはまだサポートされていません", + "sway-detail": "Swayのサポートは将来のアップデートで追加される可能性があります(Hyprlandと同様の形式)", + "labwc-not-supported": "LabWCはサポートされていません", + "labwc-detail": "LabWCはこのプラグインと互換性のないXML設定形式を使用しています", + "unknown-compositor": "不明なコンポジターが検出されました", + "unknown-detail": "このプラグインはHyprlandとNiriコンポジターのみをサポートしています", + "hyprctl-failed": "hyprctl binds failed - is Hyprland running?" + }, + "panel": { + "loading": "読み込み中...", + "loading-timeout": "Loading timed out. Click Refresh to retry.", + "title": "キーマップ", + "title-hyprland": "Hyprland キーマップ", + "title-niri": "Niri キーマップ", + "title-mango": "MangoWC Keymap", + "workspace-switching": "ワークスペース - 切り替え", + "workspace-moving": "ワークスペース - 移動", + "workspace-mouse": "ワークスペース - マウス", + "search-placeholder": "キーバインドを検索...", + "no-description": "(no description)", + "other": "Other", + "undescribed": "Without Description", + "add-description": "Add description", + "add-description-placeholder": "Description...", + "save-description": "Save", + "cancel": "Cancel", + "hide-bind": "Hide this bind" + }, + "settings": { + "title": "キーバインド一覧の設定", + "description": "パネルの外観とキーバインドソースを設定します。", + "tab-general": "General", + "tab-appearance": "Appearance", + "window-size": "ウィンドウサイズ", + "width": "幅:", + "height": "高さ:", + "auto-height": "自動", + "auto-height-hint": "「自動」が有効な場合、高さはコンテンツに合わせて自動調整されます。", + "layout": "レイアウト", + "columns": "列数:", + "columns-hint": "列が多いほど、幅の広いウィンドウで効果的です。", + "parsing-options": "Parsing Options", + "merge-sequential": "Merge sequential keybinds", + "merge-sequential-hint": "Combine consecutive numeric keybinds (e.g. Super+1..5 -> Workspace 1..5) into a single ranged entry.", + "show-undescribed": "Show undescribed binds", + "show-undescribed-hint": "Include keybinds that have no comment description. They will be auto-labeled with the action.", + "split-workspaces": "Split large workspace category", + "split-workspaces-hint": "When a workspace category exceeds the threshold, split it into Switching, Moving, and Mouse subcategories.", + "split-threshold": "Split threshold (binds):", + "config-paths": "設定ファイル", + "config-paths-description": "キーバインド設定ファイルへのカスタムパスを指定します。プラグインは自動的にすべてのinclude/sourceディレクティブに従います。", + "hyprland-format-hint": "期待される形式: bind = $mod, KEY, action #\"説明\"\nカテゴリ: # 1. カテゴリ名", + "niri-format-hint": "カテゴリは次の形式で作成: // #\"カテゴリ名\"\n説明: hotkey-overlay-title=\"説明\"", + "actions": "アクション", + "refresh-keybinds": "キーバインドを更新", + "refresh-message": "パネルを開くとキーバインドが再読み込みされます。", + "reset-defaults": "デフォルトにリセット", + "reset-message": "設定がデフォルトにリセットされました。", + "saved": "設定が正常に保存されました。", + "keybind-setup": "キーバインド設定", + "keybind-setup-description": "キーボードショートカットでチートシートを開くには、このバインドをコンポジター設定に追加してください:", + "keybind-example-hyprland": "Hyprland: bind = $mod, F1, exec, qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "keybind-example-niri": "Niri: Super+F1 { spawn \"qs\" \"-c\" \"noctalia-shell\" \"ipc\" \"call\" \"plugin\" \"togglePanel\" \"keybind-cheatsheet\"; }", + "mod-var": "Modキー変数", + "mod-var-hint": "Superキーに使用される変数名(例: $mod または $mainMod)。", + "width-range": "px (400-3000)", + "height-range": "px (300-2000)", + "hyprland-path": "Hyprland パス", + "niri-path": "Niri パス", + "mango-path": "Mango Path", + "mango-format-hint": "Expected format: bind=MODS,KEY,ACTION,ARGS\nCategories: # Category Name (horizontal rules stripped)\nOptional per-bind description: trailing #\"description\"", + "keybind-example-mango": "Mango example: bind=SUPER,F1,spawn_shell,noctalia-shell ipc call plugin:keybind-cheatsheet toggle", + "appearance-section": "Appearance", + "appearance-description": "Customize the colors used for special key categories. Changes apply immediately and persist when you click Apply.", + "preview-section": "Live Preview", + "color-pickers": "Key Colors", + "color-super": "Super / Mod key", + "color-ctrl": "Ctrl key", + "color-shift": "Shift key", + "color-alt": "Alt-modifier keys", + "color-xf86": "XF86 multimedia keys", + "color-print": "Print / Screenshot keys", + "color-numeric": "Numeric keys (workspaces)", + "color-mouse": "Mouse buttons", + "color-default": "Default letter keys", + "color-label": "Key label text", + "color-description": "Description text", + "color-theme-hint": "Leave empty to use theme accent color.", + "reset-color": "Reset to default", + "reset-all-colors": "Reset all colors", + "color-auto": "自動", + "color-mod2": "Mod2 key", + "color-mod3": "Mod3 key", + "color-mod5": "Mod5 key", + "mod-label-hint": "Mod2 / Mod3 / Mod5 は素の X11 修飾キーです(例: caps:hyper は Caps を Mod3 に割り当てます)。名前変更フィールドで「Hyper」などの分かりやすいラベルを付けられます。空にすると素の修飾キー名を表示します。", + "color-paste-hint": "ヒント: 16 進カラー (例: #FF6B6B) をクリップボードにコピーすると、ピル内に貼り付けアイコンが表示されます。", + "keybind-ipc-command": "qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "hyprland-lua-path": "Hyprland Lua Path", + "hyprland-parser-mode": "Parser:", + "hyprland-lua-hint": "Auto: uses hyprland.lua via `hyprctl binds` when it exists, otherwise the legacy .conf parser.", + "undescribed-title": "Undescribed Binds", + "overrides-summary": "Hidden: {hidden} - Custom descriptions: {custom}", + "restore-hidden": "Restore hidden", + "restore-hidden-message": "Hidden binds restored.", + "clear-overrides": "Clear all overrides", + "clear-overrides-message": "All bind overrides cleared." + }, + "barwidget": { + "tooltip": "キーバインドチートシート", + "open": "チートシートを開く", + "settings": "プラグイン設定" + } +} diff --git a/noctalia/.config/plugins/keybind-cheatsheet/i18n/ko-KR.json b/noctalia/.config/plugins/keybind-cheatsheet/i18n/ko-KR.json new file mode 100644 index 0000000..58d2c48 --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/i18n/ko-KR.json @@ -0,0 +1,117 @@ +{ + "default-category": "키바인드", + "error": { + "unsupported-compositor": "지원되지 않는 컴포지터", + "sway-not-supported": "Sway는 아직 지원되지 않습니다", + "sway-detail": "Sway 지원은 향후 업데이트에서 추가될 수 있습니다 (Hyprland와 유사한 형식)", + "labwc-not-supported": "LabWC는 지원되지 않습니다", + "labwc-detail": "LabWC는 이 플러그인과 호환되지 않는 XML 구성 형식을 사용합니다", + "unknown-compositor": "알 수 없는 컴포지터가 감지되었습니다", + "unknown-detail": "이 플러그인은 Hyprland 및 Niri 컴포지터만 지원합니다", + "hyprctl-failed": "hyprctl binds failed - is Hyprland running?" + }, + "panel": { + "loading": "로딩 중...", + "loading-timeout": "Loading timed out. Click Refresh to retry.", + "title": "키맵", + "title-hyprland": "Hyprland 키맵", + "title-niri": "Niri 키맵", + "title-mango": "MangoWC Keymap", + "workspace-switching": "작업공간 - 전환", + "workspace-moving": "작업공간 - 이동", + "workspace-mouse": "작업공간 - 마우스", + "search-placeholder": "단축키 검색...", + "no-description": "(no description)", + "other": "Other", + "undescribed": "Without Description", + "add-description": "Add description", + "add-description-placeholder": "Description...", + "save-description": "Save", + "cancel": "Cancel", + "hide-bind": "Hide this bind" + }, + "settings": { + "title": "키바인드 치트시트 설정", + "description": "키바인드 치트시트 패널의 모양과 키바인드 소스를 구성합니다.", + "tab-general": "General", + "tab-appearance": "Appearance", + "window-size": "창 크기", + "width": "너비:", + "height": "높이:", + "auto-height": "자동", + "auto-height-hint": "'자동'이 활성화되면 높이가 콘텐츠에 따라 자동으로 조정됩니다.", + "layout": "레이아웃", + "columns": "열 수:", + "columns-hint": "더 많은 열은 넓은 창에서 더 잘 작동합니다.", + "parsing-options": "Parsing Options", + "merge-sequential": "Merge sequential keybinds", + "merge-sequential-hint": "Combine consecutive numeric keybinds (e.g. Super+1..5 -> Workspace 1..5) into a single ranged entry.", + "show-undescribed": "Show undescribed binds", + "show-undescribed-hint": "Include keybinds that have no comment description. They will be auto-labeled with the action.", + "split-workspaces": "Split large workspace category", + "split-workspaces-hint": "When a workspace category exceeds the threshold, split it into Switching, Moving, and Mouse subcategories.", + "split-threshold": "Split threshold (binds):", + "config-paths": "구성 파일", + "config-paths-description": "키바인드 구성 파일의 사용자 정의 경로를 지정하세요. 플러그인은 컴포지터에 따라 적절한 파일을 자동으로 파싱합니다.", + "hyprland-format-hint": "예상 형식: bind = $mod, KEY, action #\"description\"\n카테고리: # 1. CATEGORY NAME", + "niri-format-hint": "카테고리: // #\"Category Name\"\n설명: hotkey-overlay-title=\"description\"", + "actions": "작업", + "refresh-keybinds": "키바인드 새로고침", + "refresh-message": "패널을 열면 키바인드가 다시 로드됩니다.", + "reset-defaults": "기본값으로 재설정", + "reset-message": "설정이 기본값으로 재설정되었습니다.", + "saved": "설정이 성공적으로 저장되었습니다.", + "keybind-setup": "키바인드 설정", + "keybind-setup-description": "키보드 단축키로 키바인드 치트시트를 열려면 컴포지터 구성에 이 바인드를 추가하세요:", + "keybind-example-hyprland": "Hyprland 예시: bind = $mod, F1, exec, qs -c \"noctalia-shell\" ipc call plugin togglePanel keybind-cheatsheet", + "keybind-example-niri": "Niri 예시: Super+F1 { spawn \"qs\" \"-c\" \"noctalia-shell\" \"ipc\" \"call\" \"plugin\" \"togglePanel\" \"keybind-cheatsheet\"; }", + "mod-var": "Mod 키 변수", + "mod-var-hint": "Super 키에 사용되는 변수 이름 (예: $mod 또는 $mainMod).", + "width-range": "px (400-3000)", + "height-range": "px (300-2000)", + "hyprland-path": "Hyprland 경로", + "niri-path": "Niri 경로", + "mango-path": "Mango Path", + "mango-format-hint": "Expected format: bind=MODS,KEY,ACTION,ARGS\nCategories: # Category Name (horizontal rules stripped)\nOptional per-bind description: trailing #\"description\"", + "keybind-example-mango": "Mango example: bind=SUPER,F1,spawn_shell,noctalia-shell ipc call plugin:keybind-cheatsheet toggle", + "appearance-section": "Appearance", + "appearance-description": "Customize the colors used for special key categories. Changes apply immediately and persist when you click Apply.", + "preview-section": "Live Preview", + "color-pickers": "Key Colors", + "color-super": "Super / Mod key", + "color-ctrl": "Ctrl key", + "color-shift": "Shift key", + "color-alt": "Alt-modifier keys", + "color-xf86": "XF86 multimedia keys", + "color-print": "Print / Screenshot keys", + "color-numeric": "Numeric keys (workspaces)", + "color-mouse": "Mouse buttons", + "color-default": "Default letter keys", + "color-label": "Key label text", + "color-description": "Description text", + "color-theme-hint": "Leave empty to use theme accent color.", + "reset-color": "Reset to default", + "reset-all-colors": "Reset all colors", + "color-auto": "자동", + "color-mod2": "Mod2 key", + "color-mod3": "Mod3 key", + "color-mod5": "Mod5 key", + "mod-label-hint": "Mod2 / Mod3 / Mod5는 원시 X11 수정자 키입니다(예: caps:hyper는 Caps를 Mod3에 매핑합니다). 이름 변경 필드로 \"Hyper\"처럼 알아보기 쉬운 라벨을 지정할 수 있습니다. 비워 두면 원시 수정자 이름이 표시됩니다.", + "color-paste-hint": "팁: 16진수 색상(예: #FF6B6B)을 클립보드에 복사하면 알약 내부에 붙여넣기 아이콘이 나타납니다.", + "keybind-ipc-command": "qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "hyprland-lua-path": "Hyprland Lua Path", + "hyprland-parser-mode": "Parser:", + "hyprland-lua-hint": "Auto: uses hyprland.lua via `hyprctl binds` when it exists, otherwise the legacy .conf parser.", + "undescribed-title": "Undescribed Binds", + "overrides-summary": "Hidden: {hidden} - Custom descriptions: {custom}", + "restore-hidden": "Restore hidden", + "restore-hidden-message": "Hidden binds restored.", + "clear-overrides": "Clear all overrides", + "clear-overrides-message": "All bind overrides cleared." + }, + "barwidget": { + "tooltip": "키바인드 치트시트", + "open": "치트시트 열기", + "settings": "플러그인 설정" + } +} diff --git a/noctalia/.config/plugins/keybind-cheatsheet/i18n/ku.json b/noctalia/.config/plugins/keybind-cheatsheet/i18n/ku.json new file mode 100644 index 0000000..78cac28 --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/i18n/ku.json @@ -0,0 +1,117 @@ +{ + "default-category": "بەستنەوەی کلیلەکان", + "error": { + "unsupported-compositor": "کۆمپۆزیتەرێکی پشتگیری نەکراو", + "sway-not-supported": "Sway هێشتا پشتگیری ناکرێت", + "sway-detail": "پشتگیری Sway دەکرێت لە نوێکردنەوەیەکی داهاتوودا زیاد بکرێت (فۆرماتێکی هاوشێوەی Hyprland)", + "labwc-not-supported": "LabWC پشتگیری ناکرێت", + "labwc-detail": "LabWC فۆرماتی ڕێکخستنی XML بەکاردەهێنێت کە لەگەڵ ئەم پێوەکراوەدا گونجاو نییە", + "unknown-compositor": "کۆمپۆزیتەرێکی نەناسراو دۆزرایەوە", + "unknown-detail": "ئەم پێوەکراوە تەنها کۆمپۆزیتەرەکانی Hyprland و Niri پشتگیری دەکات", + "hyprctl-failed": "hyprctl binds failed - is Hyprland running?" + }, + "panel": { + "loading": "Barkirin...", + "loading-timeout": "Loading timed out. Click Refresh to retry.", + "title": "Nexşeya Bişkokan", + "title-hyprland": "Nexşeya Bişkokan a Hyprland", + "title-niri": "Nexşeya Bişkokan a Niri", + "title-mango": "MangoWC Keymap", + "workspace-switching": "QADÊN XEBATÊ - GUHEZTIN", + "workspace-moving": "QADÊN XEBATÊ - VEGUHESTIN", + "workspace-mouse": "QADÊN XEBATÊ - MIŞK", + "search-placeholder": "Lêgerîna kurteyan...", + "no-description": "(no description)", + "other": "Other", + "undescribed": "Without Description", + "add-description": "Add description", + "add-description-placeholder": "Description...", + "save-description": "Save", + "cancel": "Cancel", + "hide-bind": "Hide this bind" + }, + "settings": { + "title": "Mîhengên Kurterêyên Klavyeyê", + "description": "Xuyanî û çavkaniyên kurterêyên klavyeyê ji bo panelê veavakin.", + "tab-general": "General", + "tab-appearance": "Appearance", + "window-size": "Mezinahiya Paceyê", + "width": "Firehî:", + "height": "Bilindî:", + "auto-height": "Xweber", + "auto-height-hint": "Dema 'Xweber' çalak be, bilindî li gorî naverokê xweber tê guhartin.", + "layout": "Rêzkirin", + "columns": "Hejmara stûnan:", + "columns-hint": "Stûnên zêdetir bi paceyên berfirehtir baştir dixebitin.", + "parsing-options": "Parsing Options", + "merge-sequential": "Merge sequential keybinds", + "merge-sequential-hint": "Combine consecutive numeric keybinds (e.g. Super+1..5 -> Workspace 1..5) into a single ranged entry.", + "show-undescribed": "Show undescribed binds", + "show-undescribed-hint": "Include keybinds that have no comment description. They will be auto-labeled with the action.", + "split-workspaces": "Split large workspace category", + "split-workspaces-hint": "When a workspace category exceeds the threshold, split it into Switching, Moving, and Mouse subcategories.", + "split-threshold": "Split threshold (binds):", + "config-paths": "Pelên Veavakirinê", + "config-paths-description": "Rêyên taybet bo pelên veavakirina kurterêyên klavyeyê diyar bikin. Pêvek bi xweber hemû rêbernameyên include/source dişopîne.", + "hyprland-format-hint": "Formata hêvîdar: bind = $mod, KEY, action #\"rave\"\nBeşan: # 1. NAVÊ BEŞÊ", + "niri-format-hint": "Beş bi vî awayî têne afirandin: // #\"Navê beşê\"\nRave: hotkey-overlay-title=\"rave\"", + "actions": "Çalakî", + "refresh-keybinds": "Kurterêyan Nûve Bike", + "refresh-message": "Kurterêyên klavyeyê dema panelê vebûyî nû dibin.", + "reset-defaults": "Vegerîne Bingehîn", + "reset-message": "Mîhengan vegeriyan nirxên bingehîn.", + "saved": "Mîhengan bi serfirazî hate tomarkirin.", + "keybind-setup": "Mîhengkirina Kurterêyê", + "keybind-setup-description": "Ji bo vekirana cheatsheet bi kurterêyek, ev bind li veavakirina compositor xwe zêde bikin:", + "keybind-example-hyprland": "Hyprland: bind = $mod, F1, exec, qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "keybind-example-niri": "Niri: Super+F1 { spawn \"qs\" \"-c\" \"noctalia-shell\" \"ipc\" \"call\" \"plugin\" \"togglePanel\" \"keybind-cheatsheet\"; }", + "mod-var": "Guhêrbarê Bişkoja Mod", + "mod-var-hint": "Navê guhêrbarê ku ji bo bişkoja Super tê bikaranîn (mînak $mod an $mainMod).", + "width-range": "px (400-3000)", + "height-range": "px (300-2000)", + "hyprland-path": "Rêça Hyprland", + "niri-path": "Rêça Niri", + "mango-path": "Mango Path", + "mango-format-hint": "Expected format: bind=MODS,KEY,ACTION,ARGS\nCategories: # Category Name (horizontal rules stripped)\nOptional per-bind description: trailing #\"description\"", + "keybind-example-mango": "Mango example: bind=SUPER,F1,spawn_shell,noctalia-shell ipc call plugin:keybind-cheatsheet toggle", + "appearance-section": "Appearance", + "appearance-description": "Customize the colors used for special key categories. Changes apply immediately and persist when you click Apply.", + "preview-section": "Live Preview", + "color-pickers": "Key Colors", + "color-super": "Super / Mod key", + "color-ctrl": "Ctrl key", + "color-shift": "Shift key", + "color-alt": "Alt-modifier keys", + "color-xf86": "XF86 multimedia keys", + "color-print": "Print / Screenshot keys", + "color-numeric": "Numeric keys (workspaces)", + "color-mouse": "Mouse buttons", + "color-default": "Default letter keys", + "color-label": "Key label text", + "color-description": "Description text", + "color-theme-hint": "Leave empty to use theme accent color.", + "reset-color": "Reset to default", + "reset-all-colors": "Reset all colors", + "color-auto": "auto", + "color-mod2": "Mod2 key", + "color-mod3": "Mod3 key", + "color-mod5": "Mod5 key", + "mod-label-hint": "Mod2 / Mod3 / Mod5 modîfîyerên X11 yên xav in (mînak caps:hyper Capsê datîne ser Mod3). Zeviya nav-guhertinê bikar bîne da ku etîketeke fêmbar wek \"Hyper\" bidî wan. Vala bihêle da ku navê xav ê modê nîşan bide.", + "color-paste-hint": "Şîret: rengekî hex (mînak #FF6B6B) kopî bike li clipboard, îkona paste di pill de xuya dibe.", + "keybind-ipc-command": "qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "hyprland-lua-path": "Hyprland Lua Path", + "hyprland-parser-mode": "Parser:", + "hyprland-lua-hint": "Auto: uses hyprland.lua via `hyprctl binds` when it exists, otherwise the legacy .conf parser.", + "undescribed-title": "Undescribed Binds", + "overrides-summary": "Hidden: {hidden} - Custom descriptions: {custom}", + "restore-hidden": "Restore hidden", + "restore-hidden-message": "Hidden binds restored.", + "clear-overrides": "Clear all overrides", + "clear-overrides-message": "All bind overrides cleared." + }, + "barwidget": { + "tooltip": "Çavkaniya Kurterêyan", + "open": "Çavkaniyê veke", + "settings": "Mîhengên Pêvek" + } +} diff --git a/noctalia/.config/plugins/keybind-cheatsheet/i18n/nl.json b/noctalia/.config/plugins/keybind-cheatsheet/i18n/nl.json new file mode 100644 index 0000000..2bc353b --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/i18n/nl.json @@ -0,0 +1,117 @@ +{ + "default-category": "Sneltoetsen", + "error": { + "unsupported-compositor": "Niet-ondersteunde compositor", + "sway-not-supported": "Sway wordt nog niet ondersteund", + "sway-detail": "Sway-ondersteuning kan worden toegevoegd in een toekomstige update (vergelijkbaar formaat met Hyprland)", + "labwc-not-supported": "LabWC wordt niet ondersteund", + "labwc-detail": "LabWC gebruikt XML-configuratieformaat dat incompatibel is met deze plugin", + "unknown-compositor": "Onbekende compositor gedetecteerd", + "unknown-detail": "Deze plugin ondersteunt alleen Hyprland- en Niri-compositors", + "hyprctl-failed": "hyprctl binds failed - is Hyprland running?" + }, + "panel": { + "loading": "Laden...", + "loading-timeout": "Loading timed out. Click Refresh to retry.", + "title": "Toetsenbordindeling", + "title-hyprland": "Hyprland toetsenbordindeling", + "title-niri": "Niri toetsenbordindeling", + "title-mango": "MangoWC Keymap", + "workspace-switching": "WERKRUIMTEN - WISSELEN", + "workspace-moving": "WERKRUIMTEN - VERPLAATSEN", + "workspace-mouse": "WERKRUIMTEN - MUIS", + "search-placeholder": "Zoek sneltoetsen...", + "no-description": "(no description)", + "other": "Other", + "undescribed": "Without Description", + "add-description": "Add description", + "add-description-placeholder": "Description...", + "save-description": "Save", + "cancel": "Cancel", + "hide-bind": "Hide this bind" + }, + "settings": { + "title": "Sneltoetsen Overzicht Instellingen", + "description": "Configureer het uiterlijk en de bronnen van sneltoetsen voor het paneel.", + "tab-general": "General", + "tab-appearance": "Appearance", + "window-size": "Venstergrootte", + "width": "Breedte:", + "height": "Hoogte:", + "auto-height": "Auto", + "auto-height-hint": "Wanneer 'Auto' is ingeschakeld, past de hoogte zich automatisch aan de inhoud aan.", + "layout": "Indeling", + "columns": "Aantal kolommen:", + "columns-hint": "Meer kolommen werken beter met bredere vensters.", + "parsing-options": "Parsing Options", + "merge-sequential": "Merge sequential keybinds", + "merge-sequential-hint": "Combine consecutive numeric keybinds (e.g. Super+1..5 -> Workspace 1..5) into a single ranged entry.", + "show-undescribed": "Show undescribed binds", + "show-undescribed-hint": "Include keybinds that have no comment description. They will be auto-labeled with the action.", + "split-workspaces": "Split large workspace category", + "split-workspaces-hint": "When a workspace category exceeds the threshold, split it into Switching, Moving, and Mouse subcategories.", + "split-threshold": "Split threshold (binds):", + "config-paths": "Configuratiebestanden", + "config-paths-description": "Geef aangepaste paden op naar uw sneltoetsen configuratiebestanden. De plugin volgt automatisch alle include/source richtlijnen.", + "hyprland-format-hint": "Verwacht formaat: bind = $mod, KEY, action #\"beschrijving\"\nCategorieen: # 1. CATEGORIENAAM", + "niri-format-hint": "Categorieen worden aangemaakt met: // #\"Categorienaam\"\nBeschrijvingen: hotkey-overlay-title=\"beschrijving\"", + "actions": "Acties", + "refresh-keybinds": "Sneltoetsen Vernieuwen", + "refresh-message": "Sneltoetsen worden opnieuw geladen wanneer u het paneel opent.", + "reset-defaults": "Standaardwaarden Herstellen", + "reset-message": "Instellingen zijn teruggezet naar standaardwaarden.", + "saved": "Instellingen succesvol opgeslagen.", + "keybind-setup": "Sneltoets Instelling", + "keybind-setup-description": "Om het cheatsheet met een sneltoets te openen, voeg deze binding toe aan je compositor configuratie:", + "keybind-example-hyprland": "Hyprland: bind = $mod, F1, exec, qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "keybind-example-niri": "Niri: Super+F1 { spawn \"qs\" \"-c\" \"noctalia-shell\" \"ipc\" \"call\" \"plugin\" \"togglePanel\" \"keybind-cheatsheet\"; }", + "mod-var": "Mod-toets Variabele", + "mod-var-hint": "De variabelenaam die wordt gebruikt voor uw Super-toets (bijv. $mod of $mainMod).", + "width-range": "px (400-3000)", + "height-range": "px (300-2000)", + "hyprland-path": "Hyprland pad", + "niri-path": "Niri pad", + "mango-path": "Mango Path", + "mango-format-hint": "Expected format: bind=MODS,KEY,ACTION,ARGS\nCategories: # Category Name (horizontal rules stripped)\nOptional per-bind description: trailing #\"description\"", + "keybind-example-mango": "Mango example: bind=SUPER,F1,spawn_shell,noctalia-shell ipc call plugin:keybind-cheatsheet toggle", + "appearance-section": "Appearance", + "appearance-description": "Customize the colors used for special key categories. Changes apply immediately and persist when you click Apply.", + "preview-section": "Live Preview", + "color-pickers": "Key Colors", + "color-super": "Super / Mod key", + "color-ctrl": "Ctrl key", + "color-shift": "Shift key", + "color-alt": "Alt-modifier keys", + "color-xf86": "XF86 multimedia keys", + "color-print": "Print / Screenshot keys", + "color-numeric": "Numeric keys (workspaces)", + "color-mouse": "Mouse buttons", + "color-default": "Default letter keys", + "color-label": "Key label text", + "color-description": "Description text", + "color-theme-hint": "Leave empty to use theme accent color.", + "reset-color": "Reset to default", + "reset-all-colors": "Reset all colors", + "color-auto": "auto", + "color-mod2": "Mod2 key", + "color-mod3": "Mod3 key", + "color-mod5": "Mod5 key", + "mod-label-hint": "Mod2 / Mod3 / Mod5 zijn ruwe X11-modifiers (bijv. caps:hyper zet Caps op Mod3). Gebruik het hernoemveld om ze een begrijpelijk label zoals \"Hyper\" te geven. Laat het leeg om de ruwe mod-naam te tonen.", + "color-paste-hint": "Tip: kopieer een hex-kleur (bijv. #FF6B6B) naar het klembord en er verschijnt een plak-icoon in de pil.", + "keybind-ipc-command": "qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "hyprland-lua-path": "Hyprland Lua Path", + "hyprland-parser-mode": "Parser:", + "hyprland-lua-hint": "Auto: uses hyprland.lua via `hyprctl binds` when it exists, otherwise the legacy .conf parser.", + "undescribed-title": "Undescribed Binds", + "overrides-summary": "Hidden: {hidden} - Custom descriptions: {custom}", + "restore-hidden": "Restore hidden", + "restore-hidden-message": "Hidden binds restored.", + "clear-overrides": "Clear all overrides", + "clear-overrides-message": "All bind overrides cleared." + }, + "barwidget": { + "tooltip": "Sneltoetsen Spiekbriefje", + "open": "Spiekbriefje openen", + "settings": "Plugin instellingen" + } +} diff --git a/noctalia/.config/plugins/keybind-cheatsheet/i18n/nn-NO.json b/noctalia/.config/plugins/keybind-cheatsheet/i18n/nn-NO.json new file mode 100644 index 0000000..8364715 --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/i18n/nn-NO.json @@ -0,0 +1,117 @@ +{ + "default-category": "Tastebindingar", + "error": { + "unsupported-compositor": "Ikkje støtta compositor", + "sway-not-supported": "Sway er ikkje støtta enno", + "sway-detail": "Sway-støtte kan leggast til i ei framtidig oppdatering (liknande format som Hyprland)", + "labwc-not-supported": "LabWC er ikkje støtta", + "labwc-detail": "LabWC brukar XML-konfigurasjonsformat som er inkompatibelt med denne plugin-en", + "unknown-compositor": "Ukjend compositor oppdaga", + "unknown-detail": "Denne plugin-en støttar berre Hyprland- og Niri-compositorar", + "hyprctl-failed": "hyprctl binds failed - is Hyprland running?" + }, + "panel": { + "loading": "Lastar...", + "loading-timeout": "Loading timed out. Click Refresh to retry.", + "title": "Tastatur-kart", + "title-hyprland": "Hyprland tastatur-kart", + "title-niri": "Niri tastatur-kart", + "title-mango": "MangoWC Keymap", + "workspace-switching": "ARBEIDSOMRÅDE - BYTTE", + "workspace-moving": "ARBEIDSOMRÅDE - FLYTTE", + "workspace-mouse": "ARBEIDSOMRÅDE - MUS", + "search-placeholder": "Søk snarvegar...", + "no-description": "(no description)", + "other": "Other", + "undescribed": "Without Description", + "add-description": "Add description", + "add-description-placeholder": "Description...", + "save-description": "Save", + "cancel": "Cancel", + "hide-bind": "Hide this bind" + }, + "settings": { + "title": "Tastesnarveg-jukselapp Innstillingar", + "description": "Konfigurer utsjånad og tastesnarveg-kjelder for jukselapp-panelet.", + "tab-general": "General", + "tab-appearance": "Appearance", + "window-size": "Vindaugsstorleik", + "width": "Breidde:", + "height": "Høgde:", + "auto-height": "Auto", + "auto-height-hint": "Når 'Auto' er aktivert, justerer høgda seg automatisk basert på innhald.", + "layout": "Oppsett", + "columns": "Tal på kolonnar:", + "columns-hint": "Fleire kolonnar fungerer betre med breiare vindauge.", + "parsing-options": "Parsing Options", + "merge-sequential": "Merge sequential keybinds", + "merge-sequential-hint": "Combine consecutive numeric keybinds (e.g. Super+1..5 -> Workspace 1..5) into a single ranged entry.", + "show-undescribed": "Show undescribed binds", + "show-undescribed-hint": "Include keybinds that have no comment description. They will be auto-labeled with the action.", + "split-workspaces": "Split large workspace category", + "split-workspaces-hint": "When a workspace category exceeds the threshold, split it into Switching, Moving, and Mouse subcategories.", + "split-threshold": "Split threshold (binds):", + "config-paths": "Konfigurasjonsfiler", + "config-paths-description": "Spesifiser eigne stiar til tastesnarveg-konfigurasjonsfilene dine. Programtillegget vil automatisk parse rett fil basert på kompositoren din.", + "hyprland-format-hint": "Forventa format: bind = $mod, KEY, action #\"description\"\nKategoriar: # 1. CATEGORY NAME", + "niri-format-hint": "Kategoriar: // #\"Category Name\"\nSkildringar: hotkey-overlay-title=\"description\"", + "actions": "Handlingar", + "refresh-keybinds": "Oppdater tastesnarvegar", + "refresh-message": "Tastesnarvegar vil bli lasta på nytt når du opnar panelet.", + "reset-defaults": "Tilbakestill til standard", + "reset-message": "Innstillingar er tilbakestilt til standard.", + "saved": "Innstillingar lagra.", + "keybind-setup": "Tastesnarveg-oppsett", + "keybind-setup-description": "For å opne tastesnarveg-jukselappen med ein hurtigtast, legg til denne bindinga i kompositor-konfigurasjonen din:", + "keybind-example-hyprland": "Hyprland eksempel: bind = $mod, F1, exec, qs -c \"noctalia-shell\" ipc call plugin togglePanel keybind-cheatsheet", + "keybind-example-niri": "Niri eksempel: Super+F1 { spawn \"qs\" \"-c\" \"noctalia-shell\" \"ipc\" \"call\" \"plugin\" \"togglePanel\" \"keybind-cheatsheet\"; }", + "mod-var": "Mod-tast variabel", + "mod-var-hint": "Variabelnamnet brukt for Super-tasten din (t.d. $mod eller $mainMod).", + "width-range": "px (400-3000)", + "height-range": "px (300-2000)", + "hyprland-path": "Hyprland-sti", + "niri-path": "Niri-sti", + "mango-path": "Mango Path", + "mango-format-hint": "Expected format: bind=MODS,KEY,ACTION,ARGS\nCategories: # Category Name (horizontal rules stripped)\nOptional per-bind description: trailing #\"description\"", + "keybind-example-mango": "Mango example: bind=SUPER,F1,spawn_shell,noctalia-shell ipc call plugin:keybind-cheatsheet toggle", + "appearance-section": "Appearance", + "appearance-description": "Customize the colors used for special key categories. Changes apply immediately and persist when you click Apply.", + "preview-section": "Live Preview", + "color-pickers": "Key Colors", + "color-super": "Super / Mod key", + "color-ctrl": "Ctrl key", + "color-shift": "Shift key", + "color-alt": "Alt-modifier keys", + "color-xf86": "XF86 multimedia keys", + "color-print": "Print / Screenshot keys", + "color-numeric": "Numeric keys (workspaces)", + "color-mouse": "Mouse buttons", + "color-default": "Default letter keys", + "color-label": "Key label text", + "color-description": "Description text", + "color-theme-hint": "Leave empty to use theme accent color.", + "reset-color": "Reset to default", + "reset-all-colors": "Reset all colors", + "color-auto": "auto", + "color-mod2": "Mod2 key", + "color-mod3": "Mod3 key", + "color-mod5": "Mod5 key", + "mod-label-hint": "Mod2 / Mod3 / Mod5 er rå X11-modifikatorar (t.d. legg caps:hyper Caps på Mod3). Bruk omdøypingsfeltet for å gje dei ei forståeleg etikett som «Hyper». Lat det stå tomt for å visa det rå modifikatornamnet.", + "color-paste-hint": "Tips: kopier ein hex-farge (t.d. #FF6B6B) til utklippstavla, så kjem eit lim-inn-ikon i pilla.", + "keybind-ipc-command": "qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "hyprland-lua-path": "Hyprland Lua Path", + "hyprland-parser-mode": "Parser:", + "hyprland-lua-hint": "Auto: uses hyprland.lua via `hyprctl binds` when it exists, otherwise the legacy .conf parser.", + "undescribed-title": "Undescribed Binds", + "overrides-summary": "Hidden: {hidden} - Custom descriptions: {custom}", + "restore-hidden": "Restore hidden", + "restore-hidden-message": "Hidden binds restored.", + "clear-overrides": "Clear all overrides", + "clear-overrides-message": "All bind overrides cleared." + }, + "barwidget": { + "tooltip": "Tastesnarveg-jukselapp", + "open": "Opne jukselapp", + "settings": "Programtillegg-innstillingar" + } +} diff --git a/noctalia/.config/plugins/keybind-cheatsheet/i18n/pl.json b/noctalia/.config/plugins/keybind-cheatsheet/i18n/pl.json new file mode 100644 index 0000000..049ab6b --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/i18n/pl.json @@ -0,0 +1,117 @@ +{ + "default-category": "Skróty klawiszowe", + "error": { + "unsupported-compositor": "Nieobsługiwany kompozytor", + "sway-not-supported": "Sway nie jest jeszcze obsługiwany", + "sway-detail": "Obsługa Sway może zostać dodana w przyszłej aktualizacji (podobny format do Hyprland)", + "labwc-not-supported": "LabWC nie jest obsługiwany", + "labwc-detail": "LabWC używa formatu konfiguracji XML, który jest niekompatybilny z tą wtyczką", + "unknown-compositor": "Wykryto nieznany kompozytor", + "unknown-detail": "Ta wtyczka obsługuje tylko kompozytory Hyprland i Niri", + "hyprctl-failed": "hyprctl binds nie powiodło się - czy Hyprland działa?" + }, + "panel": { + "loading": "Ladowanie...", + "loading-timeout": "Loading timed out. Click Refresh to retry.", + "title": "Mapa klawiszy", + "title-hyprland": "Mapa klawiszy Hyprland", + "title-niri": "Mapa klawiszy Niri", + "title-mango": "MangoWC Keymap", + "workspace-switching": "OBSZARY ROBOCZE - PRZEŁĄCZANIE", + "workspace-moving": "OBSZARY ROBOCZE - PRZENOSZENIE", + "workspace-mouse": "OBSZARY ROBOCZE - MYSZ", + "search-placeholder": "Szukaj skrótów...", + "no-description": "(brak opisu)", + "other": "Inne", + "undescribed": "Bez opisu", + "add-description": "Dodaj opis", + "add-description-placeholder": "Opis...", + "save-description": "Zapisz", + "cancel": "Anuluj", + "hide-bind": "Ukryj ten skrót" + }, + "settings": { + "title": "Ustawienia Skrotow Klawiszowych", + "description": "Skonfiguruj wyglad i zrodla skrotow klawiszowych dla panelu.", + "tab-general": "General", + "tab-appearance": "Appearance", + "window-size": "Rozmiar Okna", + "width": "Szerokosc:", + "height": "Wysokosc:", + "auto-height": "Auto", + "auto-height-hint": "Gdy 'Auto' jest wlaczone, wysokosc dostosowuje sie automatycznie do zawartosci.", + "layout": "Uklad", + "columns": "Liczba kolumn:", + "columns-hint": "Wiecej kolumn dziala lepiej z szerszymi oknami.", + "parsing-options": "Parsing Options", + "merge-sequential": "Merge sequential keybinds", + "merge-sequential-hint": "Combine consecutive numeric keybinds (e.g. Super+1..5 -> Workspace 1..5) into a single ranged entry.", + "show-undescribed": "Show undescribed binds", + "show-undescribed-hint": "Include keybinds that have no comment description. They will be auto-labeled with the action.", + "split-workspaces": "Split large workspace category", + "split-workspaces-hint": "When a workspace category exceeds the threshold, split it into Switching, Moving, and Mouse subcategories.", + "split-threshold": "Split threshold (binds):", + "config-paths": "Pliki Konfiguracyjne", + "config-paths-description": "Okresl wlasne sciezki do plikow konfiguracyjnych skrotow klawiszowych. Plugin automatycznie przetworzy odpowiedni plik na podstawie Twojego kompozytora i podazy za dyrektywami include/source.", + "hyprland-format-hint": "Oczekiwany format: bind = $mod, KEY, action #\"opis\"\nKategorie: # 1. NAZWA KATEGORII", + "niri-format-hint": "Kategorie tworzy sie przez: // #\"Nazwa kategorii\"\nOpisy: hotkey-overlay-title=\"opis\"", + "actions": "Akcje", + "refresh-keybinds": "Odswiez Skroty", + "refresh-message": "Skroty zostana przeladowane po otwarciu panelu.", + "reset-defaults": "Przywroc Domyslne", + "reset-message": "Ustawienia zostaly przywrocone do domyslnych.", + "saved": "Ustawienia zapisane pomyslnie.", + "keybind-setup": "Konfiguracja Skrotu", + "keybind-setup-description": "Aby otworzyc cheatsheet skrotem klawiszowym, dodaj ten bind do konfiguracji kompozytora:", + "keybind-example-hyprland": "Przyklad Hyprland: bind = $mod, F1, exec, qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "keybind-example-niri": "Przyklad Niri: Super+F1 { spawn \"qs\" \"-c\" \"noctalia-shell\" \"ipc\" \"call\" \"plugin\" \"togglePanel\" \"keybind-cheatsheet\"; }", + "mod-var": "Zmienna klawisza Mod", + "mod-var-hint": "Nazwa zmiennej używanej dla klawisza Super (np. $mod lub $mainMod).", + "width-range": "px (400-3000)", + "height-range": "px (300-2000)", + "hyprland-path": "Ścieżka Hyprland", + "niri-path": "Ścieżka Niri", + "mango-path": "Mango Path", + "mango-format-hint": "Expected format: bind=MODS,KEY,ACTION,ARGS\nCategories: # Category Name (horizontal rules stripped)\nOptional per-bind description: trailing #\"description\"", + "keybind-example-mango": "Mango example: bind=SUPER,F1,spawn_shell,noctalia-shell ipc call plugin:keybind-cheatsheet toggle", + "appearance-section": "Appearance", + "appearance-description": "Customize the colors used for special key categories. Changes apply immediately and persist when you click Apply.", + "preview-section": "Live Preview", + "color-pickers": "Key Colors", + "color-super": "Super / Mod key", + "color-ctrl": "Ctrl key", + "color-shift": "Shift key", + "color-alt": "Alt-modifier keys", + "color-xf86": "XF86 multimedia keys", + "color-print": "Print / Screenshot keys", + "color-numeric": "Numeric keys (workspaces)", + "color-mouse": "Mouse buttons", + "color-default": "Default letter keys", + "color-label": "Key label text", + "color-description": "Description text", + "color-theme-hint": "Leave empty to use theme accent color.", + "reset-color": "Reset to default", + "reset-all-colors": "Reset all colors", + "color-auto": "auto", + "color-mod2": "Mod2 key", + "color-mod3": "Mod3 key", + "color-mod5": "Mod5 key", + "mod-label-hint": "Mod2 / Mod3 / Mod5 to surowe modyfikatory X11 (np. caps:hyper przypisuje Caps do Mod3). Użyj pola zmiany nazwy, aby nadać im przyjazną etykietę, np. \"Hyper\". Pozostaw puste, aby pokazać surową nazwę modyfikatora.", + "color-paste-hint": "Wskazówka: skopiuj kod koloru hex (np. #FF6B6B) do schowka, a w pastylce pojawi się ikona wklejania.", + "keybind-ipc-command": "qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "hyprland-lua-path": "Ścieżka Hyprland Lua", + "hyprland-parser-mode": "Parser:", + "hyprland-lua-hint": "Auto: używa hyprland.lua przez `hyprctl binds` jeśli istnieje, w innym wypadku starego parsera .conf.", + "undescribed-title": "Skróty bez opisu", + "overrides-summary": "Ukryte: {hidden} - Własne opisy: {custom}", + "restore-hidden": "Przywróć ukryte", + "restore-hidden-message": "Ukryte skróty przywrócone.", + "clear-overrides": "Wyczyść wszystkie nadpisania", + "clear-overrides-message": "Wszystkie nadpisania skrótów wyczyszczone." + }, + "barwidget": { + "tooltip": "Ściągawka skrótów klawiszowych", + "open": "Otwórz ściągawkę", + "settings": "Ustawienia wtyczki" + } +} diff --git a/noctalia/.config/plugins/keybind-cheatsheet/i18n/pt.json b/noctalia/.config/plugins/keybind-cheatsheet/i18n/pt.json new file mode 100644 index 0000000..716ae19 --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/i18n/pt.json @@ -0,0 +1,117 @@ +{ + "default-category": "Atalhos de teclado", + "error": { + "unsupported-compositor": "Compositor não suportado", + "sway-not-supported": "Sway ainda não é suportado", + "sway-detail": "O suporte ao Sway pode ser adicionado em uma atualização futura (formato semelhante ao Hyprland)", + "labwc-not-supported": "LabWC não é suportado", + "labwc-detail": "LabWC usa formato de configuração XML que é incompatível com este plugin", + "unknown-compositor": "Compositor desconhecido detectado", + "unknown-detail": "Este plugin suporta apenas os compositores Hyprland e Niri", + "hyprctl-failed": "hyprctl binds failed - is Hyprland running?" + }, + "panel": { + "loading": "Carregando...", + "loading-timeout": "Loading timed out. Click Refresh to retry.", + "title": "Mapa de teclas", + "title-hyprland": "Mapa de teclas Hyprland", + "title-niri": "Mapa de teclas Niri", + "title-mango": "MangoWC Keymap", + "workspace-switching": "ESPAÇOS DE TRABALHO - ALTERNAR", + "workspace-moving": "ESPAÇOS DE TRABALHO - MOVER", + "workspace-mouse": "ESPAÇOS DE TRABALHO - MOUSE", + "search-placeholder": "Buscar atalhos...", + "no-description": "(no description)", + "other": "Other", + "undescribed": "Without Description", + "add-description": "Add description", + "add-description-placeholder": "Description...", + "save-description": "Save", + "cancel": "Cancel", + "hide-bind": "Hide this bind" + }, + "settings": { + "title": "Configuracoes de Atalhos de Teclado", + "description": "Configure a aparencia e as fontes de atalhos de teclado para o painel.", + "tab-general": "General", + "tab-appearance": "Appearance", + "window-size": "Tamanho da Janela", + "width": "Largura:", + "height": "Altura:", + "auto-height": "Auto", + "auto-height-hint": "Quando 'Auto' esta ativado, a altura se ajusta automaticamente ao conteudo.", + "layout": "Layout", + "columns": "Numero de colunas:", + "columns-hint": "Mais colunas funcionam melhor com janelas mais largas.", + "parsing-options": "Parsing Options", + "merge-sequential": "Merge sequential keybinds", + "merge-sequential-hint": "Combine consecutive numeric keybinds (e.g. Super+1..5 -> Workspace 1..5) into a single ranged entry.", + "show-undescribed": "Show undescribed binds", + "show-undescribed-hint": "Include keybinds that have no comment description. They will be auto-labeled with the action.", + "split-workspaces": "Split large workspace category", + "split-workspaces-hint": "When a workspace category exceeds the threshold, split it into Switching, Moving, and Mouse subcategories.", + "split-threshold": "Split threshold (binds):", + "config-paths": "Arquivos de Configuracao", + "config-paths-description": "Especifique caminhos personalizados para seus arquivos de configuracao de atalhos. O plugin seguira automaticamente todas as diretivas include/source.", + "hyprland-format-hint": "Formato esperado: bind = $mod, KEY, action #\"descricao\"\nCategorias: # 1. NOME DA CATEGORIA", + "niri-format-hint": "Categorias sao criadas com: // #\"Nome da categoria\"\nDescricoes: hotkey-overlay-title=\"descricao\"", + "actions": "Acoes", + "refresh-keybinds": "Atualizar Atalhos", + "refresh-message": "Os atalhos serao recarregados ao abrir o painel.", + "reset-defaults": "Restaurar Padroes", + "reset-message": "As configuracoes foram restauradas para os valores padrao.", + "saved": "Configuracoes salvas com sucesso.", + "keybind-setup": "Configuracao de Atalho", + "keybind-setup-description": "Para abrir o cheatsheet com um atalho de teclado, adicione este bind a configuracao do seu compositor:", + "keybind-example-hyprland": "Hyprland: bind = $mod, F1, exec, qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "keybind-example-niri": "Niri: Super+F1 { spawn \"qs\" \"-c\" \"noctalia-shell\" \"ipc\" \"call\" \"plugin\" \"togglePanel\" \"keybind-cheatsheet\"; }", + "mod-var": "Variável da Tecla Mod", + "mod-var-hint": "O nome da variável usada para sua tecla Super (ex. $mod ou $mainMod).", + "width-range": "px (400-3000)", + "height-range": "px (300-2000)", + "hyprland-path": "Caminho Hyprland", + "niri-path": "Caminho Niri", + "mango-path": "Mango Path", + "mango-format-hint": "Expected format: bind=MODS,KEY,ACTION,ARGS\nCategories: # Category Name (horizontal rules stripped)\nOptional per-bind description: trailing #\"description\"", + "keybind-example-mango": "Mango example: bind=SUPER,F1,spawn_shell,noctalia-shell ipc call plugin:keybind-cheatsheet toggle", + "appearance-section": "Appearance", + "appearance-description": "Customize the colors used for special key categories. Changes apply immediately and persist when you click Apply.", + "preview-section": "Live Preview", + "color-pickers": "Key Colors", + "color-super": "Super / Mod key", + "color-ctrl": "Ctrl key", + "color-shift": "Shift key", + "color-alt": "Alt-modifier keys", + "color-xf86": "XF86 multimedia keys", + "color-print": "Print / Screenshot keys", + "color-numeric": "Numeric keys (workspaces)", + "color-mouse": "Mouse buttons", + "color-default": "Default letter keys", + "color-label": "Key label text", + "color-description": "Description text", + "color-theme-hint": "Leave empty to use theme accent color.", + "reset-color": "Reset to default", + "reset-all-colors": "Reset all colors", + "color-auto": "auto", + "color-mod2": "Mod2 key", + "color-mod3": "Mod3 key", + "color-mod5": "Mod5 key", + "mod-label-hint": "Mod2 / Mod3 / Mod5 são modificadores X11 brutos (ex.: caps:hyper coloca Caps em Mod3). Use o campo de renomeação para dar-lhes um rótulo amigável como \"Hyper\". Deixe vazio para mostrar o nome bruto do modificador.", + "color-paste-hint": "Dica: copie uma cor hex (ex. #FF6B6B) para a área de transferência e um ícone de colar aparecerá dentro da pílula.", + "keybind-ipc-command": "qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "hyprland-lua-path": "Hyprland Lua Path", + "hyprland-parser-mode": "Parser:", + "hyprland-lua-hint": "Auto: uses hyprland.lua via `hyprctl binds` when it exists, otherwise the legacy .conf parser.", + "undescribed-title": "Undescribed Binds", + "overrides-summary": "Hidden: {hidden} - Custom descriptions: {custom}", + "restore-hidden": "Restore hidden", + "restore-hidden-message": "Hidden binds restored.", + "clear-overrides": "Clear all overrides", + "clear-overrides-message": "All bind overrides cleared." + }, + "barwidget": { + "tooltip": "Folha de dicas de atalhos", + "open": "Abrir folha de dicas", + "settings": "Configurações do plugin" + } +} diff --git a/noctalia/.config/plugins/keybind-cheatsheet/i18n/ru.json b/noctalia/.config/plugins/keybind-cheatsheet/i18n/ru.json new file mode 100644 index 0000000..f38d70e --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/i18n/ru.json @@ -0,0 +1,117 @@ +{ + "default-category": "Горячие клавиши", + "error": { + "unsupported-compositor": "Неподдерживаемый композитор", + "sway-not-supported": "Sway пока не поддерживается", + "sway-detail": "Поддержка Sway может быть добавлена в будущем обновлении (формат похож на Hyprland)", + "labwc-not-supported": "LabWC не поддерживается", + "labwc-detail": "LabWC использует формат конфигурации XML, несовместимый с этим плагином", + "unknown-compositor": "Обнаружен неизвестный композитор", + "unknown-detail": "Этот плагин поддерживает только композиторы Hyprland и Niri", + "hyprctl-failed": "hyprctl binds failed - is Hyprland running?" + }, + "panel": { + "loading": "Загрузка...", + "loading-timeout": "Loading timed out. Click Refresh to retry.", + "title": "Раскладка клавиш", + "title-hyprland": "Раскладка клавиш Hyprland", + "title-niri": "Раскладка клавиш Niri", + "title-mango": "MangoWC Keymap", + "workspace-switching": "РАБОЧИЕ ОБЛАСТИ - ПЕРЕКЛЮЧЕНИЕ", + "workspace-moving": "РАБОЧИЕ ОБЛАСТИ - ПЕРЕМЕЩЕНИЕ", + "workspace-mouse": "РАБОЧИЕ ОБЛАСТИ - МЫШЬ", + "search-placeholder": "Поиск сочетаний...", + "no-description": "(no description)", + "other": "Other", + "undescribed": "Without Description", + "add-description": "Add description", + "add-description-placeholder": "Description...", + "save-description": "Save", + "cancel": "Cancel", + "hide-bind": "Hide this bind" + }, + "settings": { + "title": "Настройки горячих клавиш", + "description": "Настройте внешний вид и источники горячих клавиш для панели.", + "tab-general": "General", + "tab-appearance": "Appearance", + "window-size": "Размер окна", + "width": "Ширина:", + "height": "Высота:", + "auto-height": "Авто", + "auto-height-hint": "Когда включено 'Авто', высота автоматически подстраивается под содержимое.", + "layout": "Макет", + "columns": "Количество колонок:", + "columns-hint": "Больше колонок лучше работает с более широкими окнами.", + "parsing-options": "Parsing Options", + "merge-sequential": "Merge sequential keybinds", + "merge-sequential-hint": "Combine consecutive numeric keybinds (e.g. Super+1..5 -> Workspace 1..5) into a single ranged entry.", + "show-undescribed": "Show undescribed binds", + "show-undescribed-hint": "Include keybinds that have no comment description. They will be auto-labeled with the action.", + "split-workspaces": "Split large workspace category", + "split-workspaces-hint": "When a workspace category exceeds the threshold, split it into Switching, Moving, and Mouse subcategories.", + "split-threshold": "Split threshold (binds):", + "config-paths": "Файлы конфигурации", + "config-paths-description": "Укажите пользовательские пути к файлам конфигурации горячих клавиш. Плагин автоматически следует всем директивам include/source.", + "hyprland-format-hint": "Ожидаемый формат: bind = $mod, KEY, action #\"описание\"\nКатегории: # 1. НАЗВАНИЕ КАТЕГОРИИ", + "niri-format-hint": "Категории создаются с помощью: // #\"Название категории\"\nОписания: hotkey-overlay-title=\"описание\"", + "actions": "Действия", + "refresh-keybinds": "Обновить клавиши", + "refresh-message": "Горячие клавиши будут перезагружены при открытии панели.", + "reset-defaults": "Сбросить настройки", + "reset-message": "Настройки сброшены до значений по умолчанию.", + "saved": "Настройки успешно сохранены.", + "keybind-setup": "Настройка клавиши", + "keybind-setup-description": "Чтобы открыть шпаргалку с помощью клавиши, добавьте этот bind в конфигурацию композитора:", + "keybind-example-hyprland": "Hyprland: bind = $mod, F1, exec, qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "keybind-example-niri": "Niri: Super+F1 { spawn \"qs\" \"-c\" \"noctalia-shell\" \"ipc\" \"call\" \"plugin\" \"togglePanel\" \"keybind-cheatsheet\"; }", + "mod-var": "Переменная клавиши Mod", + "mod-var-hint": "Имя переменной, используемой для клавиши Super (например, $mod или $mainMod).", + "width-range": "px (400-3000)", + "height-range": "px (300-2000)", + "hyprland-path": "Путь Hyprland", + "niri-path": "Путь Niri", + "mango-path": "Mango Path", + "mango-format-hint": "Expected format: bind=MODS,KEY,ACTION,ARGS\nCategories: # Category Name (horizontal rules stripped)\nOptional per-bind description: trailing #\"description\"", + "keybind-example-mango": "Mango example: bind=SUPER,F1,spawn_shell,noctalia-shell ipc call plugin:keybind-cheatsheet toggle", + "appearance-section": "Appearance", + "appearance-description": "Customize the colors used for special key categories. Changes apply immediately and persist when you click Apply.", + "preview-section": "Live Preview", + "color-pickers": "Key Colors", + "color-super": "Super / Mod key", + "color-ctrl": "Ctrl key", + "color-shift": "Shift key", + "color-alt": "Alt-modifier keys", + "color-xf86": "XF86 multimedia keys", + "color-print": "Print / Screenshot keys", + "color-numeric": "Numeric keys (workspaces)", + "color-mouse": "Mouse buttons", + "color-default": "Default letter keys", + "color-label": "Key label text", + "color-description": "Description text", + "color-theme-hint": "Leave empty to use theme accent color.", + "reset-color": "Reset to default", + "reset-all-colors": "Reset all colors", + "color-auto": "авто", + "color-mod2": "Mod2 key", + "color-mod3": "Mod3 key", + "color-mod5": "Mod5 key", + "mod-label-hint": "Mod2 / Mod3 / Mod5 — это «сырые» модификаторы X11 (например, caps:hyper назначает Caps на Mod3). Используйте поле переименования, чтобы задать понятную метку, например «Hyper». Оставьте пустым, чтобы показать исходное имя модификатора.", + "color-paste-hint": "Совет: скопируйте hex-цвет (например #FF6B6B) в буфер обмена — в пилюле появится иконка вставки.", + "keybind-ipc-command": "qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "hyprland-lua-path": "Hyprland Lua Path", + "hyprland-parser-mode": "Parser:", + "hyprland-lua-hint": "Auto: uses hyprland.lua via `hyprctl binds` when it exists, otherwise the legacy .conf parser.", + "undescribed-title": "Undescribed Binds", + "overrides-summary": "Hidden: {hidden} - Custom descriptions: {custom}", + "restore-hidden": "Restore hidden", + "restore-hidden-message": "Hidden binds restored.", + "clear-overrides": "Clear all overrides", + "clear-overrides-message": "All bind overrides cleared." + }, + "barwidget": { + "tooltip": "Шпаргалка горячих клавиш", + "open": "Открыть шпаргалку", + "settings": "Настройки плагина" + } +} diff --git a/noctalia/.config/plugins/keybind-cheatsheet/i18n/sv.json b/noctalia/.config/plugins/keybind-cheatsheet/i18n/sv.json new file mode 100644 index 0000000..052ee9d --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/i18n/sv.json @@ -0,0 +1,117 @@ +{ + "default-category": "Tangentbindningar", + "error": { + "unsupported-compositor": "Compositor stöds inte", + "sway-not-supported": "Sway stöds ännu inte", + "sway-detail": "Sway-stöd kan läggas till i en framtida uppdatering (liknande format som Hyprland)", + "labwc-not-supported": "LabWC stöds inte", + "labwc-detail": "LabWC använder XML-konfigurationsformat som är inkompatibelt med detta plugin", + "unknown-compositor": "Okänd compositor upptäckt", + "unknown-detail": "Detta plugin stöder endast Hyprland- och Niri-compositors", + "hyprctl-failed": "hyprctl binds failed - is Hyprland running?" + }, + "panel": { + "loading": "Laddar...", + "loading-timeout": "Loading timed out. Click Refresh to retry.", + "title": "Tangentkarta", + "title-hyprland": "Hyprland tangentkarta", + "title-niri": "Niri tangentkarta", + "title-mango": "MangoWC Keymap", + "workspace-switching": "ARBETSYTOR - BYTA", + "workspace-moving": "ARBETSYTOR - FLYTTA", + "workspace-mouse": "ARBETSYTOR - MUS", + "search-placeholder": "Sök genvägar...", + "no-description": "(no description)", + "other": "Other", + "undescribed": "Without Description", + "add-description": "Add description", + "add-description-placeholder": "Description...", + "save-description": "Save", + "cancel": "Cancel", + "hide-bind": "Hide this bind" + }, + "settings": { + "title": "Tangentbordsgenvägar Inställningar", + "description": "Konfigurera utseende och tangentbordsgenvägskällor för fuskbladspanelen.", + "tab-general": "General", + "tab-appearance": "Appearance", + "window-size": "Fönsterstorlek", + "width": "Bredd:", + "height": "Höjd:", + "auto-height": "Auto", + "auto-height-hint": "När 'Auto' är aktiverat justeras höjden automatiskt baserat på innehåll.", + "layout": "Layout", + "columns": "Antal kolumner:", + "columns-hint": "Fler kolumner fungerar bättre med bredare fönster.", + "parsing-options": "Parsing Options", + "merge-sequential": "Merge sequential keybinds", + "merge-sequential-hint": "Combine consecutive numeric keybinds (e.g. Super+1..5 -> Workspace 1..5) into a single ranged entry.", + "show-undescribed": "Show undescribed binds", + "show-undescribed-hint": "Include keybinds that have no comment description. They will be auto-labeled with the action.", + "split-workspaces": "Split large workspace category", + "split-workspaces-hint": "When a workspace category exceeds the threshold, split it into Switching, Moving, and Mouse subcategories.", + "split-threshold": "Split threshold (binds):", + "config-paths": "Konfigurationsfiler", + "config-paths-description": "Ange anpassade sökvägar till dina tangentbordsgenvägskonfigurationsfiler. Tillägget kommer automatiskt att tolka rätt fil baserat på din compositor.", + "hyprland-format-hint": "Förväntat format: bind = $mod, KEY, action #\"description\"\nKategorier: # 1. CATEGORY NAME", + "niri-format-hint": "Kategorier: // #\"Category Name\"\nBeskrivningar: hotkey-overlay-title=\"description\"", + "actions": "Åtgärder", + "refresh-keybinds": "Uppdatera tangentbordsgenvägar", + "refresh-message": "Tangentbordsgenvägar laddas om när du öppnar panelen.", + "reset-defaults": "Återställ till standard", + "reset-message": "Inställningarna har återställts till standard.", + "saved": "Inställningar sparade.", + "keybind-setup": "Tangentbordsgenväg inställning", + "keybind-setup-description": "För att öppna tangentbordsgenvägsfuskbladet med en kortkommando, lägg till denna bindning i din compositor-konfiguration:", + "keybind-example-hyprland": "Hyprland exempel: bind = $mod, F1, exec, qs -c \"noctalia-shell\" ipc call plugin togglePanel keybind-cheatsheet", + "keybind-example-niri": "Niri exempel: Super+F1 { spawn \"qs\" \"-c\" \"noctalia-shell\" \"ipc\" \"call\" \"plugin\" \"togglePanel\" \"keybind-cheatsheet\"; }", + "mod-var": "Mod-tangent variabel", + "mod-var-hint": "Variabelnamnet som används för din Super-tangent (t.ex. $mod eller $mainMod).", + "width-range": "px (400-3000)", + "height-range": "px (300-2000)", + "hyprland-path": "Hyprland-sökväg", + "niri-path": "Niri-sökväg", + "mango-path": "Mango Path", + "mango-format-hint": "Expected format: bind=MODS,KEY,ACTION,ARGS\nCategories: # Category Name (horizontal rules stripped)\nOptional per-bind description: trailing #\"description\"", + "keybind-example-mango": "Mango example: bind=SUPER,F1,spawn_shell,noctalia-shell ipc call plugin:keybind-cheatsheet toggle", + "appearance-section": "Appearance", + "appearance-description": "Customize the colors used for special key categories. Changes apply immediately and persist when you click Apply.", + "preview-section": "Live Preview", + "color-pickers": "Key Colors", + "color-super": "Super / Mod key", + "color-ctrl": "Ctrl key", + "color-shift": "Shift key", + "color-alt": "Alt-modifier keys", + "color-xf86": "XF86 multimedia keys", + "color-print": "Print / Screenshot keys", + "color-numeric": "Numeric keys (workspaces)", + "color-mouse": "Mouse buttons", + "color-default": "Default letter keys", + "color-label": "Key label text", + "color-description": "Description text", + "color-theme-hint": "Leave empty to use theme accent color.", + "reset-color": "Reset to default", + "reset-all-colors": "Reset all colors", + "color-auto": "auto", + "color-mod2": "Mod2 key", + "color-mod3": "Mod3 key", + "color-mod5": "Mod5 key", + "mod-label-hint": "Mod2 / Mod3 / Mod5 är råa X11-modifierare (t.ex. lägger caps:hyper Caps på Mod3). Använd namnbytesfältet för att ge dem en begriplig etikett som ”Hyper”. Lämna tomt för att visa det råa modifierarnamnet.", + "color-paste-hint": "Tips: kopiera en hex-färg (t.ex. #FF6B6B) till urklipp och en klistra in-ikon visas i pillret.", + "keybind-ipc-command": "qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "hyprland-lua-path": "Hyprland Lua Path", + "hyprland-parser-mode": "Parser:", + "hyprland-lua-hint": "Auto: uses hyprland.lua via `hyprctl binds` when it exists, otherwise the legacy .conf parser.", + "undescribed-title": "Undescribed Binds", + "overrides-summary": "Hidden: {hidden} - Custom descriptions: {custom}", + "restore-hidden": "Restore hidden", + "restore-hidden-message": "Hidden binds restored.", + "clear-overrides": "Clear all overrides", + "clear-overrides-message": "All bind overrides cleared." + }, + "barwidget": { + "tooltip": "Tangentbordsgenvägar", + "open": "Öppna fuskblad", + "settings": "Plugin-inställningar" + } +} diff --git a/noctalia/.config/plugins/keybind-cheatsheet/i18n/tr.json b/noctalia/.config/plugins/keybind-cheatsheet/i18n/tr.json new file mode 100644 index 0000000..c1f8f13 --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/i18n/tr.json @@ -0,0 +1,117 @@ +{ + "default-category": "Kısayol tuşları", + "error": { + "unsupported-compositor": "Desteklenmeyen kompozitör", + "sway-not-supported": "Sway henüz desteklenmiyor", + "sway-detail": "Sway desteği gelecekteki bir güncellemede eklenebilir (Hyprland'a benzer format)", + "labwc-not-supported": "LabWC desteklenmiyor", + "labwc-detail": "LabWC bu eklentiyle uyumlu olmayan XML yapılandırma formatı kullanıyor", + "unknown-compositor": "Bilinmeyen kompozitör algılandı", + "unknown-detail": "Bu eklenti yalnızca Hyprland ve Niri kompozitörlerini destekler", + "hyprctl-failed": "hyprctl binds failed - is Hyprland running?" + }, + "panel": { + "loading": "Yukleniyor...", + "loading-timeout": "Loading timed out. Click Refresh to retry.", + "title": "Tuş haritası", + "title-hyprland": "Hyprland tuş haritası", + "title-niri": "Niri tuş haritası", + "title-mango": "MangoWC Keymap", + "workspace-switching": "ÇALIŞMA ALANLARI - GEÇİŞ", + "workspace-moving": "ÇALIŞMA ALANLARI - TAŞIMA", + "workspace-mouse": "ÇALIŞMA ALANLARI - FARE", + "search-placeholder": "Kısayol ara...", + "no-description": "(no description)", + "other": "Other", + "undescribed": "Without Description", + "add-description": "Add description", + "add-description-placeholder": "Description...", + "save-description": "Save", + "cancel": "Cancel", + "hide-bind": "Hide this bind" + }, + "settings": { + "title": "Klavye Kisayollari Ayarlari", + "description": "Panel icin klavye kisayollarinin gorunumunu ve kaynaklarini yapilandirin.", + "tab-general": "General", + "tab-appearance": "Appearance", + "window-size": "Pencere Boyutu", + "width": "Genislik:", + "height": "Yukseklik:", + "auto-height": "Otomatik", + "auto-height-hint": "'Otomatik' etkinlestirildiginde, yukseklik icerge gore otomatik olarak ayarlanir.", + "layout": "Duzen", + "columns": "Sutun sayisi:", + "columns-hint": "Daha fazla sutun daha genis pencerelerle daha iyi calisir.", + "parsing-options": "Parsing Options", + "merge-sequential": "Merge sequential keybinds", + "merge-sequential-hint": "Combine consecutive numeric keybinds (e.g. Super+1..5 -> Workspace 1..5) into a single ranged entry.", + "show-undescribed": "Show undescribed binds", + "show-undescribed-hint": "Include keybinds that have no comment description. They will be auto-labeled with the action.", + "split-workspaces": "Split large workspace category", + "split-workspaces-hint": "When a workspace category exceeds the threshold, split it into Switching, Moving, and Mouse subcategories.", + "split-threshold": "Split threshold (binds):", + "config-paths": "Yapilandirma Dosyalari", + "config-paths-description": "Klavye kisayolu yapilandirma dosyalariniza ozel yollar belirtin. Eklenti otomatik olarak tum include/source yonergelerini takip eder.", + "hyprland-format-hint": "Beklenen format: bind = $mod, KEY, action #\"aciklama\"\nKategoriler: # 1. KATEGORI ADI", + "niri-format-hint": "Kategoriler su sekilde olusturulur: // #\"Kategori adi\"\nAciklamalar: hotkey-overlay-title=\"aciklama\"", + "actions": "Islemler", + "refresh-keybinds": "Kisayollari Yenile", + "refresh-message": "Paneli actinizda kisayollar yeniden yuklenecektir.", + "reset-defaults": "Varsayilanlara Sifirla", + "reset-message": "Ayarlar varsayilan degerlere sifirlandi.", + "saved": "Ayarlar basariyla kaydedildi.", + "keybind-setup": "Kisayol Ayari", + "keybind-setup-description": "Cheatsheet'i bir klavye kisayoluyla acmak icin, bu bind'i compositor yapilandirmaniza ekleyin:", + "keybind-example-hyprland": "Hyprland: bind = $mod, F1, exec, qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "keybind-example-niri": "Niri: Super+F1 { spawn \"qs\" \"-c\" \"noctalia-shell\" \"ipc\" \"call\" \"plugin\" \"togglePanel\" \"keybind-cheatsheet\"; }", + "mod-var": "Mod Tusu Degiskeni", + "mod-var-hint": "Super tusunuz icin kullanilan degisken adi (orn. $mod veya $mainMod).", + "width-range": "px (400-3000)", + "height-range": "px (300-2000)", + "hyprland-path": "Hyprland yolu", + "niri-path": "Niri yolu", + "mango-path": "Mango Path", + "mango-format-hint": "Expected format: bind=MODS,KEY,ACTION,ARGS\nCategories: # Category Name (horizontal rules stripped)\nOptional per-bind description: trailing #\"description\"", + "keybind-example-mango": "Mango example: bind=SUPER,F1,spawn_shell,noctalia-shell ipc call plugin:keybind-cheatsheet toggle", + "appearance-section": "Appearance", + "appearance-description": "Customize the colors used for special key categories. Changes apply immediately and persist when you click Apply.", + "preview-section": "Live Preview", + "color-pickers": "Key Colors", + "color-super": "Super / Mod key", + "color-ctrl": "Ctrl key", + "color-shift": "Shift key", + "color-alt": "Alt-modifier keys", + "color-xf86": "XF86 multimedia keys", + "color-print": "Print / Screenshot keys", + "color-numeric": "Numeric keys (workspaces)", + "color-mouse": "Mouse buttons", + "color-default": "Default letter keys", + "color-label": "Key label text", + "color-description": "Description text", + "color-theme-hint": "Leave empty to use theme accent color.", + "reset-color": "Reset to default", + "reset-all-colors": "Reset all colors", + "color-auto": "auto", + "color-mod2": "Mod2 key", + "color-mod3": "Mod3 key", + "color-mod5": "Mod5 key", + "mod-label-hint": "Mod2 / Mod3 / Mod5 ham X11 değiştiricileridir (örn. caps:hyper, Caps'i Mod3'e atar). Onlara \"Hyper\" gibi anlaşılır bir etiket vermek için yeniden adlandırma alanını kullanın. Ham değiştirici adını göstermek için boş bırakın.", + "color-paste-hint": "İpucu: bir hex rengi (ör. #FF6B6B) panoya kopyalayın; hapın içinde hızlı yapıştırma simgesi görünecek.", + "keybind-ipc-command": "qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "hyprland-lua-path": "Hyprland Lua Path", + "hyprland-parser-mode": "Parser:", + "hyprland-lua-hint": "Auto: uses hyprland.lua via `hyprctl binds` when it exists, otherwise the legacy .conf parser.", + "undescribed-title": "Undescribed Binds", + "overrides-summary": "Hidden: {hidden} - Custom descriptions: {custom}", + "restore-hidden": "Restore hidden", + "restore-hidden-message": "Hidden binds restored.", + "clear-overrides": "Clear all overrides", + "clear-overrides-message": "All bind overrides cleared." + }, + "barwidget": { + "tooltip": "Kısayol Kopya Kağıdı", + "open": "Kopya kağıdını aç", + "settings": "Eklenti ayarları" + } +} diff --git a/noctalia/.config/plugins/keybind-cheatsheet/i18n/uk-UA.json b/noctalia/.config/plugins/keybind-cheatsheet/i18n/uk-UA.json new file mode 100644 index 0000000..4f85b7e --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/i18n/uk-UA.json @@ -0,0 +1,117 @@ +{ + "default-category": "Гарячі клавіші", + "error": { + "unsupported-compositor": "Непідтримуваний композитор", + "sway-not-supported": "Sway поки не підтримується", + "sway-detail": "Підтримку Sway може бути додано в майбутньому оновленні (формат схожий на Hyprland)", + "labwc-not-supported": "LabWC не підтримується", + "labwc-detail": "LabWC використовує формат конфігурації XML, несумісний з цим плагіном", + "unknown-compositor": "Виявлено невідомий композитор", + "unknown-detail": "Цей плагін підтримує лише композитори Hyprland та Niri", + "hyprctl-failed": "hyprctl binds failed - is Hyprland running?" + }, + "panel": { + "loading": "Завантаження...", + "loading-timeout": "Loading timed out. Click Refresh to retry.", + "title": "Карта клавіш", + "title-hyprland": "Карта клавіш Hyprland", + "title-niri": "Карта клавіш Niri", + "title-mango": "MangoWC Keymap", + "workspace-switching": "РОБОЧІ ОБЛАСТІ - ПЕРЕМИКАННЯ", + "workspace-moving": "РОБОЧІ ОБЛАСТІ - ПЕРЕМІЩЕННЯ", + "workspace-mouse": "РОБОЧІ ОБЛАСТІ - МИША", + "search-placeholder": "Пошук скорочень...", + "no-description": "(no description)", + "other": "Other", + "undescribed": "Without Description", + "add-description": "Add description", + "add-description-placeholder": "Description...", + "save-description": "Save", + "cancel": "Cancel", + "hide-bind": "Hide this bind" + }, + "settings": { + "title": "Налаштування гарячих клавіш", + "description": "Налаштуйте зовнішній вигляд та джерела гарячих клавіш для панелі.", + "tab-general": "General", + "tab-appearance": "Appearance", + "window-size": "Розмір вікна", + "width": "Ширина:", + "height": "Висота:", + "auto-height": "Авто", + "auto-height-hint": "Коли увімкнено 'Авто', висота автоматично підлаштовується під вміст.", + "layout": "Макет", + "columns": "Кількість колонок:", + "columns-hint": "Більше колонок краще працює з ширшими вікнами.", + "parsing-options": "Parsing Options", + "merge-sequential": "Merge sequential keybinds", + "merge-sequential-hint": "Combine consecutive numeric keybinds (e.g. Super+1..5 -> Workspace 1..5) into a single ranged entry.", + "show-undescribed": "Show undescribed binds", + "show-undescribed-hint": "Include keybinds that have no comment description. They will be auto-labeled with the action.", + "split-workspaces": "Split large workspace category", + "split-workspaces-hint": "When a workspace category exceeds the threshold, split it into Switching, Moving, and Mouse subcategories.", + "split-threshold": "Split threshold (binds):", + "config-paths": "Файли конфігурації", + "config-paths-description": "Вкажіть власні шляхи до файлів конфігурації гарячих клавіш. Плагін автоматично слідує всім директивам include/source.", + "hyprland-format-hint": "Очікуваний формат: bind = $mod, KEY, action #\"опис\"\nКатегорії: # 1. НАЗВА КАТЕГОРІЇ", + "niri-format-hint": "Категорії створюються за допомогою: // #\"Назва категорії\"\nОписи: hotkey-overlay-title=\"опис\"", + "actions": "Дії", + "refresh-keybinds": "Оновити клавіші", + "refresh-message": "Гарячі клавіші будуть перезавантажені при відкритті панелі.", + "reset-defaults": "Скинути налаштування", + "reset-message": "Налаштування скинуто до значень за замовчуванням.", + "saved": "Налаштування успішно збережено.", + "keybind-setup": "Налаштування клавіші", + "keybind-setup-description": "Щоб відкрити шпаргалку за допомогою клавіші, додайте цей bind до конфігурації композитора:", + "keybind-example-hyprland": "Hyprland: bind = $mod, F1, exec, qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "keybind-example-niri": "Niri: Super+F1 { spawn \"qs\" \"-c\" \"noctalia-shell\" \"ipc\" \"call\" \"plugin\" \"togglePanel\" \"keybind-cheatsheet\"; }", + "mod-var": "Змінна клавіші Mod", + "mod-var-hint": "Ім'я змінної, що використовується для клавіші Super (наприклад, $mod або $mainMod).", + "width-range": "px (400-3000)", + "height-range": "px (300-2000)", + "hyprland-path": "Шлях Hyprland", + "niri-path": "Шлях Niri", + "mango-path": "Mango Path", + "mango-format-hint": "Expected format: bind=MODS,KEY,ACTION,ARGS\nCategories: # Category Name (horizontal rules stripped)\nOptional per-bind description: trailing #\"description\"", + "keybind-example-mango": "Mango example: bind=SUPER,F1,spawn_shell,noctalia-shell ipc call plugin:keybind-cheatsheet toggle", + "appearance-section": "Appearance", + "appearance-description": "Customize the colors used for special key categories. Changes apply immediately and persist when you click Apply.", + "preview-section": "Live Preview", + "color-pickers": "Key Colors", + "color-super": "Super / Mod key", + "color-ctrl": "Ctrl key", + "color-shift": "Shift key", + "color-alt": "Alt-modifier keys", + "color-xf86": "XF86 multimedia keys", + "color-print": "Print / Screenshot keys", + "color-numeric": "Numeric keys (workspaces)", + "color-mouse": "Mouse buttons", + "color-default": "Default letter keys", + "color-label": "Key label text", + "color-description": "Description text", + "color-theme-hint": "Leave empty to use theme accent color.", + "reset-color": "Reset to default", + "reset-all-colors": "Reset all colors", + "color-auto": "авто", + "color-mod2": "Mod2 key", + "color-mod3": "Mod3 key", + "color-mod5": "Mod5 key", + "mod-label-hint": "Mod2 / Mod3 / Mod5 — це «сирі» модифікатори X11 (наприклад, caps:hyper призначає Caps на Mod3). Використовуйте поле перейменування, щоб задати зрозумілу мітку, як-от «Hyper». Залиште порожнім, щоб показати початкову назву модифікатора.", + "color-paste-hint": "Порада: скопіюйте hex-колір (напр. #FF6B6B) у буфер обміну — у пілюлі з'явиться іконка вставки.", + "keybind-ipc-command": "qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "hyprland-lua-path": "Hyprland Lua Path", + "hyprland-parser-mode": "Parser:", + "hyprland-lua-hint": "Auto: uses hyprland.lua via `hyprctl binds` when it exists, otherwise the legacy .conf parser.", + "undescribed-title": "Undescribed Binds", + "overrides-summary": "Hidden: {hidden} - Custom descriptions: {custom}", + "restore-hidden": "Restore hidden", + "restore-hidden-message": "Hidden binds restored.", + "clear-overrides": "Clear all overrides", + "clear-overrides-message": "All bind overrides cleared." + }, + "barwidget": { + "tooltip": "Шпаргалка гарячих клавіш", + "open": "Відкрити шпаргалку", + "settings": "Налаштування плагіна" + } +} diff --git a/noctalia/.config/plugins/keybind-cheatsheet/i18n/zh-CN.json b/noctalia/.config/plugins/keybind-cheatsheet/i18n/zh-CN.json new file mode 100644 index 0000000..9b9cbb4 --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/i18n/zh-CN.json @@ -0,0 +1,117 @@ +{ + "default-category": "快捷键", + "error": { + "unsupported-compositor": "不支持的合成器", + "sway-not-supported": "尚不支持 Sway", + "sway-detail": "Sway 支持可能会在未来更新中添加(与 Hyprland 格式类似)", + "labwc-not-supported": "不支持 LabWC", + "labwc-detail": "LabWC 使用与此插件不兼容的 XML 配置格式", + "unknown-compositor": "检测到未知的合成器", + "unknown-detail": "此插件仅支持 Hyprland 和 Niri 合成器", + "hyprctl-failed": "hyprctl binds failed - is Hyprland running?" + }, + "panel": { + "loading": "加载中...", + "loading-timeout": "Loading timed out. Click Refresh to retry.", + "title": "按键映射", + "title-hyprland": "Hyprland 按键映射", + "title-niri": "Niri 按键映射", + "title-mango": "MangoWC Keymap", + "workspace-switching": "工作区 - 切换", + "workspace-moving": "工作区 - 移动", + "workspace-mouse": "工作区 - 鼠标", + "search-placeholder": "搜索快捷键...", + "no-description": "(no description)", + "other": "Other", + "undescribed": "Without Description", + "add-description": "Add description", + "add-description-placeholder": "Description...", + "save-description": "Save", + "cancel": "Cancel", + "hide-bind": "Hide this bind" + }, + "settings": { + "title": "快捷键速查表设置", + "description": "配置面板的外观和快捷键来源。", + "tab-general": "General", + "tab-appearance": "Appearance", + "window-size": "窗口大小", + "width": "宽度:", + "height": "高度:", + "auto-height": "自动", + "auto-height-hint": "启用\"自动\"时,高度会根据内容自动调整。", + "layout": "布局", + "columns": "列数:", + "columns-hint": "更多列在较宽的窗口中效果更好。", + "parsing-options": "Parsing Options", + "merge-sequential": "Merge sequential keybinds", + "merge-sequential-hint": "Combine consecutive numeric keybinds (e.g. Super+1..5 -> Workspace 1..5) into a single ranged entry.", + "show-undescribed": "Show undescribed binds", + "show-undescribed-hint": "Include keybinds that have no comment description. They will be auto-labeled with the action.", + "split-workspaces": "Split large workspace category", + "split-workspaces-hint": "When a workspace category exceeds the threshold, split it into Switching, Moving, and Mouse subcategories.", + "split-threshold": "Split threshold (binds):", + "config-paths": "配置文件", + "config-paths-description": "指定快捷键配置文件的自定义路径。插件会自动跟随所有include/source指令。", + "hyprland-format-hint": "预期格式: bind = $mod, KEY, action #\"描述\"\n类别: # 1. 类别名称", + "niri-format-hint": "类别使用以下格式创建: // #\"类别名称\"\n描述: hotkey-overlay-title=\"描述\"", + "actions": "操作", + "refresh-keybinds": "刷新快捷键", + "refresh-message": "打开面板时将重新加载快捷键。", + "reset-defaults": "恢复默认", + "reset-message": "设置已恢复为默认值。", + "saved": "设置保存成功。", + "keybind-setup": "快捷键设置", + "keybind-setup-description": "要使用键盘快捷键打开速查表,请将此绑定添加到合成器配置中:", + "keybind-example-hyprland": "Hyprland: bind = $mod, F1, exec, qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "keybind-example-niri": "Niri: Super+F1 { spawn \"qs\" \"-c\" \"noctalia-shell\" \"ipc\" \"call\" \"plugin\" \"togglePanel\" \"keybind-cheatsheet\"; }", + "mod-var": "Mod 键变量", + "mod-var-hint": "用于 Super 键的变量名称(例如 $mod 或 $mainMod)。", + "width-range": "px (400-3000)", + "height-range": "px (300-2000)", + "hyprland-path": "Hyprland 路径", + "niri-path": "Niri 路径", + "mango-path": "Mango Path", + "mango-format-hint": "Expected format: bind=MODS,KEY,ACTION,ARGS\nCategories: # Category Name (horizontal rules stripped)\nOptional per-bind description: trailing #\"description\"", + "keybind-example-mango": "Mango example: bind=SUPER,F1,spawn_shell,noctalia-shell ipc call plugin:keybind-cheatsheet toggle", + "appearance-section": "Appearance", + "appearance-description": "Customize the colors used for special key categories. Changes apply immediately and persist when you click Apply.", + "preview-section": "Live Preview", + "color-pickers": "Key Colors", + "color-super": "Super / Mod key", + "color-ctrl": "Ctrl key", + "color-shift": "Shift key", + "color-alt": "Alt-modifier keys", + "color-xf86": "XF86 multimedia keys", + "color-print": "Print / Screenshot keys", + "color-numeric": "Numeric keys (workspaces)", + "color-mouse": "Mouse buttons", + "color-default": "Default letter keys", + "color-label": "Key label text", + "color-description": "Description text", + "color-theme-hint": "Leave empty to use theme accent color.", + "reset-color": "Reset to default", + "reset-all-colors": "Reset all colors", + "color-auto": "自动", + "color-mod2": "Mod2 key", + "color-mod3": "Mod3 key", + "color-mod5": "Mod5 key", + "mod-label-hint": "Mod2 / Mod3 / Mod5 是原始的 X11 修饰键(例如 caps:hyper 会把 Caps 映射到 Mod3)。使用重命名字段可以给它们起一个友好的标签,比如“Hyper”。留空则显示原始的修饰键名称。", + "color-paste-hint": "提示:将十六进制颜色 (如 #FF6B6B) 复制到剪贴板,胶囊内会出现粘贴图标以便快速粘贴。", + "keybind-ipc-command": "qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "hyprland-lua-path": "Hyprland Lua Path", + "hyprland-parser-mode": "Parser:", + "hyprland-lua-hint": "Auto: uses hyprland.lua via `hyprctl binds` when it exists, otherwise the legacy .conf parser.", + "undescribed-title": "Undescribed Binds", + "overrides-summary": "Hidden: {hidden} - Custom descriptions: {custom}", + "restore-hidden": "Restore hidden", + "restore-hidden-message": "Hidden binds restored.", + "clear-overrides": "Clear all overrides", + "clear-overrides-message": "All bind overrides cleared." + }, + "barwidget": { + "tooltip": "快捷键速查表", + "open": "打开速查表", + "settings": "插件设置" + } +} diff --git a/noctalia/.config/plugins/keybind-cheatsheet/i18n/zh-TW.json b/noctalia/.config/plugins/keybind-cheatsheet/i18n/zh-TW.json new file mode 100644 index 0000000..161e629 --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/i18n/zh-TW.json @@ -0,0 +1,117 @@ +{ + "default-category": "快捷鍵", + "error": { + "unsupported-compositor": "不支援的合成器", + "sway-not-supported": "尚未支援 Sway", + "sway-detail": "Sway 支援可能會在未來更新中新增(與 Hyprland 格式類似)", + "labwc-not-supported": "不支援 LabWC", + "labwc-detail": "LabWC 使用與此外掛程式不相容的 XML 設定格式", + "unknown-compositor": "偵測到未知的合成器", + "unknown-detail": "此外掛程式僅支援 Hyprland 和 Niri 合成器", + "hyprctl-failed": "hyprctl binds failed - is Hyprland running?" + }, + "panel": { + "loading": "載入中...", + "loading-timeout": "Loading timed out. Click Refresh to retry.", + "title": "按鍵映射", + "title-hyprland": "Hyprland 按鍵映射", + "title-niri": "Niri 按鍵映射", + "title-mango": "MangoWC Keymap", + "workspace-switching": "工作區 - 切換", + "workspace-moving": "工作區 - 移動", + "workspace-mouse": "工作區 - 滑鼠", + "search-placeholder": "搜尋快捷鍵...", + "no-description": "(no description)", + "other": "Other", + "undescribed": "Without Description", + "add-description": "Add description", + "add-description-placeholder": "Description...", + "save-description": "Save", + "cancel": "Cancel", + "hide-bind": "Hide this bind" + }, + "settings": { + "title": "快捷鍵速查表設定", + "description": "設定面板的外觀和快捷鍵來源。", + "tab-general": "General", + "tab-appearance": "Appearance", + "window-size": "視窗大小", + "width": "寬度:", + "height": "高度:", + "auto-height": "自動", + "auto-height-hint": "啟用「自動」時,高度會根據內容自動調整。", + "layout": "佈局", + "columns": "欄位數:", + "columns-hint": "更多欄位在較寬的視窗中效果更好。", + "parsing-options": "Parsing Options", + "merge-sequential": "Merge sequential keybinds", + "merge-sequential-hint": "Combine consecutive numeric keybinds (e.g. Super+1..5 -> Workspace 1..5) into a single ranged entry.", + "show-undescribed": "Show undescribed binds", + "show-undescribed-hint": "Include keybinds that have no comment description. They will be auto-labeled with the action.", + "split-workspaces": "Split large workspace category", + "split-workspaces-hint": "When a workspace category exceeds the threshold, split it into Switching, Moving, and Mouse subcategories.", + "split-threshold": "Split threshold (binds):", + "config-paths": "設定檔", + "config-paths-description": "指定快捷鍵設定檔的自訂路徑。外掛程式會自動跟隨所有 include/source 指令。", + "hyprland-format-hint": "預期格式: bind = $mod, KEY, action #\"描述\"\n類別: # 1. 類別名稱", + "niri-format-hint": "類別使用以下格式建立: // #\"類別名稱\"\n描述: hotkey-overlay-title=\"描述\"", + "actions": "操作", + "refresh-keybinds": "重新整理快捷鍵", + "refresh-message": "開啟面板時將重新載入快捷鍵。", + "reset-defaults": "恢復預設值", + "reset-message": "設定已恢復為預設值。", + "saved": "設定儲存成功。", + "keybind-setup": "快捷鍵設定", + "keybind-setup-description": "要使用鍵盤快捷鍵開啟速查表,請將此繫結新增至合成器設定中:", + "keybind-example-hyprland": "Hyprland: bind = $mod, F1, exec, qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "keybind-example-niri": "Niri: Super+F1 { spawn \"qs\" \"-c\" \"noctalia-shell\" \"ipc\" \"call\" \"plugin\" \"togglePanel\" \"keybind-cheatsheet\"; }", + "mod-var": "Mod 鍵變數", + "mod-var-hint": "用於 Super 鍵的變數名稱(例如 $mod 或 $mainMod)。", + "width-range": "px (400-3000)", + "height-range": "px (300-2000)", + "hyprland-path": "Hyprland 路徑", + "niri-path": "Niri 路徑", + "mango-path": "Mango Path", + "mango-format-hint": "Expected format: bind=MODS,KEY,ACTION,ARGS\nCategories: # Category Name (horizontal rules stripped)\nOptional per-bind description: trailing #\"description\"", + "keybind-example-mango": "Mango example: bind=SUPER,F1,spawn_shell,noctalia-shell ipc call plugin:keybind-cheatsheet toggle", + "appearance-section": "Appearance", + "appearance-description": "Customize the colors used for special key categories. Changes apply immediately and persist when you click Apply.", + "preview-section": "Live Preview", + "color-pickers": "Key Colors", + "color-super": "Super / Mod key", + "color-ctrl": "Ctrl key", + "color-shift": "Shift key", + "color-alt": "Alt-modifier keys", + "color-xf86": "XF86 multimedia keys", + "color-print": "Print / Screenshot keys", + "color-numeric": "Numeric keys (workspaces)", + "color-mouse": "Mouse buttons", + "color-default": "Default letter keys", + "color-label": "Key label text", + "color-description": "Description text", + "color-theme-hint": "Leave empty to use theme accent color.", + "reset-color": "Reset to default", + "reset-all-colors": "Reset all colors", + "color-auto": "自動", + "color-mod2": "Mod2 key", + "color-mod3": "Mod3 key", + "color-mod5": "Mod5 key", + "mod-label-hint": "Mod2 / Mod3 / Mod5 是原始的 X11 修飾鍵(例如 caps:hyper 會把 Caps 對應到 Mod3)。使用重新命名欄位可以為它們設定易懂的標籤,例如「Hyper」。留空則顯示原始的修飾鍵名稱。", + "color-paste-hint": "提示:將十六進位顏色 (如 #FF6B6B) 複製到剪貼簿,膠囊內會出現貼上圖示以便快速貼上。", + "keybind-ipc-command": "qs -c \"noctalia-shell\" ipc call plugin:keybind-cheatsheet toggle", + "hyprland-lua-path": "Hyprland Lua Path", + "hyprland-parser-mode": "Parser:", + "hyprland-lua-hint": "Auto: uses hyprland.lua via `hyprctl binds` when it exists, otherwise the legacy .conf parser.", + "undescribed-title": "Undescribed Binds", + "overrides-summary": "Hidden: {hidden} - Custom descriptions: {custom}", + "restore-hidden": "Restore hidden", + "restore-hidden-message": "Hidden binds restored.", + "clear-overrides": "Clear all overrides", + "clear-overrides-message": "All bind overrides cleared." + }, + "barwidget": { + "tooltip": "快捷鍵速查表", + "open": "開啟速查表", + "settings": "外掛設定" + } +} diff --git a/noctalia/.config/plugins/keybind-cheatsheet/manifest.json b/noctalia/.config/plugins/keybind-cheatsheet/manifest.json new file mode 100644 index 0000000..a4a5b6a --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/manifest.json @@ -0,0 +1,78 @@ +{ + "id": "keybind-cheatsheet", + "name": "Keybind Cheatsheet", + "version": "3.7.4", + "minNoctaliaVersion": "3.6.0", + "author": "blacku", + "license": "MIT", + "repository": "https://github.com/noctalia-dev/noctalia-plugins", + "description": "Universal keyboard shortcuts keymap that automatically detects and displays keybindings for Hyprland, Niri, or MangoWC compositors.", + "tags": [ + "Bar", + "Panel", + "System", + "Indicator", + "Hyprland", + "Niri", + "MangoWC" + ], + "entryPoints": { + "main": "Main.qml", + "barWidget": "BarWidget.qml", + "panel": "Panel.qml", + "settings": "Settings.qml" + }, + "dependencies": { + "plugins": [] + }, + "metadata": { + "defaultSettings": { + "cheatsheetData": [], + "detectedCompositor": "", + "windowWidth": 1400, + "windowHeight": 850, + "autoHeight": true, + "columnCount": 3, + "modKeyVariable": "$mod", + "hyprlandConfigPath": "~/.config/hypr/hyprland.conf", + "hyprlandLuaConfigPath": "~/.config/hypr/hyprland.lua", + "hyprlandParserMode": "auto", + "niriConfigPath": "~/.config/niri/config.kdl", + "mangoConfigPath": "~/.config/mango/config.conf", + "mergeSequentialBinds": true, + "showUndescribedBinds": true, + "splitLargeWorkspaceCategory": true, + "workspaceSplitThreshold": 12, + "keyColorAlt": "#FF6B6B", + "keyColorXF86": "#4ECDC4", + "keyColorPrint": "#95E1D3", + "keyColorNumeric": "#A8DADC", + "keyColorMouse": "#F38181", + "keyColorSuper": "", + "keyColorCtrl": "", + "keyColorShift": "", + "keyColorDefault": "#6C757D", + "keyLabelColor": "#FFFFFF", + "keyTextSuper": "", + "keyTextCtrl": "", + "keyTextShift": "", + "keyTextAlt": "", + "keyTextXF86": "", + "keyTextPrint": "", + "keyTextNumeric": "", + "keyTextMouse": "", + "keyTextDefault": "", + "keyColorMod2": "", + "keyColorMod3": "", + "keyColorMod5": "", + "keyTextMod2": "", + "keyTextMod3": "", + "keyTextMod5": "", + "keyLabelMod2": "", + "keyLabelMod3": "", + "keyLabelMod5": "", + "descriptionTextColor": "", + "bindOverrides": {} + } + } +} diff --git a/noctalia/.config/plugins/keybind-cheatsheet/preview.png b/noctalia/.config/plugins/keybind-cheatsheet/preview.png Binary files differnew file mode 100644 index 0000000..c315ef6 --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/preview.png diff --git a/noctalia/.config/plugins/keybind-cheatsheet/settings.json b/noctalia/.config/plugins/keybind-cheatsheet/settings.json new file mode 100644 index 0000000..82d6cbd --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/settings.json @@ -0,0 +1,815 @@ +{ + "cheatsheetData": [ + { + "title": "Mod", + "binds": [ + { + "keys": "Super + Shift + Slash", + "desc": "Show Hotkey Overlay", + "_verb": "show-hotkey-overlay", + "_mods": "Super+Shift", + "_mainKey": "Slash" + }, + { + "keys": "Super + Return", + "desc": "Open a Terminal: ghostty", + "_verb": "spawn", + "_mods": "Super", + "_mainKey": "Return" + }, + { + "keys": "Super + D", + "desc": "Run an Application: fuzzel", + "_verb": "spawn", + "_mods": "Super", + "_mainKey": "D" + }, + { + "keys": "Super + Alt + L", + "desc": "Lock the Screen: swaylock", + "_verb": "spawn", + "_mods": "Super+Alt", + "_mainKey": "L" + } + ] + }, + { + "title": "-l 1.0", + "binds": [ + { + "keys": "Vol Up", + "desc": "Run: wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.1+ -l 1.0", + "_verb": "spawn-sh", + "_mods": "", + "_mainKey": "XF86AudioRaiseVolume" + }, + { + "keys": "Vol Down", + "desc": "Run: wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.1-", + "_verb": "spawn-sh", + "_mods": "", + "_mainKey": "XF86AudioLowerVolume" + }, + { + "keys": "Mute", + "desc": "Run: wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle", + "_verb": "spawn-sh", + "_mods": "", + "_mainKey": "XF86AudioMute" + }, + { + "keys": "Mic Mute", + "desc": "Run: wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle", + "_verb": "spawn-sh", + "_mods": "", + "_mainKey": "XF86AudioMicMute" + }, + { + "keys": "Play", + "desc": "Run: playerctl play-pause", + "_verb": "spawn-sh", + "_mods": "", + "_mainKey": "XF86AudioPlay" + }, + { + "keys": "Stop", + "desc": "Run: playerctl stop", + "_verb": "spawn-sh", + "_mods": "", + "_mainKey": "XF86AudioStop" + }, + { + "keys": "Prev", + "desc": "Run: playerctl previous", + "_verb": "spawn-sh", + "_mods": "", + "_mainKey": "XF86AudioPrev" + }, + { + "keys": "Next", + "desc": "Run: playerctl next", + "_verb": "spawn-sh", + "_mods": "", + "_mainKey": "XF86AudioNext" + }, + { + "keys": "Bright Up", + "desc": "Run: brightnessctl", + "_verb": "spawn", + "_mods": "", + "_mainKey": "XF86MonBrightnessUp" + }, + { + "keys": "Bright Down", + "desc": "Run: brightnessctl", + "_verb": "spawn", + "_mods": "", + "_mainKey": "XF86MonBrightnessDown" + }, + { + "keys": "Super + O", + "desc": "Toggle Overview", + "_verb": "toggle-overview", + "_mods": "Super", + "_mainKey": "O" + }, + { + "keys": "Super + Q", + "desc": "Close Window", + "_verb": "close-window", + "_mods": "Super", + "_mainKey": "Q" + }, + { + "keys": "Super + C", + "desc": "Run: wtype", + "_verb": "spawn", + "_mods": "Super", + "_mainKey": "C" + }, + { + "keys": "Super + V", + "desc": "Run: wtype", + "_verb": "spawn", + "_mods": "Super", + "_mainKey": "V" + }, + { + "keys": "Super + X", + "desc": "Run: wtype", + "_verb": "spawn", + "_mods": "Super", + "_mainKey": "X" + }, + { + "keys": "Super + Left", + "desc": "Focus Column Left", + "_verb": "focus-column-left", + "_mods": "Super", + "_mainKey": "Left" + }, + { + "keys": "Super + Down", + "desc": "Focus Window Down", + "_verb": "focus-window-down", + "_mods": "Super", + "_mainKey": "Down" + }, + { + "keys": "Super + Up", + "desc": "Focus Window Up", + "_verb": "focus-window-up", + "_mods": "Super", + "_mainKey": "Up" + }, + { + "keys": "Super + Right", + "desc": "Focus Column Right", + "_verb": "focus-column-right", + "_mods": "Super", + "_mainKey": "Right" + }, + { + "keys": "Super + H", + "desc": "Focus Column Left", + "_verb": "focus-column-left", + "_mods": "Super", + "_mainKey": "H" + }, + { + "keys": "Super + J", + "desc": "Focus Window Down", + "_verb": "focus-window-down", + "_mods": "Super", + "_mainKey": "J" + }, + { + "keys": "Super + K", + "desc": "Focus Window Up", + "_verb": "focus-window-up", + "_mods": "Super", + "_mainKey": "K" + }, + { + "keys": "Super + L", + "desc": "Focus Column Right", + "_verb": "focus-column-right", + "_mods": "Super", + "_mainKey": "L" + }, + { + "keys": "Super + Ctrl + Left", + "desc": "Move Column Left", + "_verb": "move-column-left", + "_mods": "Super+Ctrl", + "_mainKey": "Left" + }, + { + "keys": "Super + Ctrl + Down", + "desc": "Move Window Down", + "_verb": "move-window-down", + "_mods": "Super+Ctrl", + "_mainKey": "Down" + }, + { + "keys": "Super + Ctrl + Up", + "desc": "Move Window Up", + "_verb": "move-window-up", + "_mods": "Super+Ctrl", + "_mainKey": "Up" + }, + { + "keys": "Super + Ctrl + Right", + "desc": "Move Column Right", + "_verb": "move-column-right", + "_mods": "Super+Ctrl", + "_mainKey": "Right" + }, + { + "keys": "Super + Ctrl + H", + "desc": "Move Column Left", + "_verb": "move-column-left", + "_mods": "Super+Ctrl", + "_mainKey": "H" + }, + { + "keys": "Super + Ctrl + J", + "desc": "Move Window Down", + "_verb": "move-window-down", + "_mods": "Super+Ctrl", + "_mainKey": "J" + }, + { + "keys": "Super + Ctrl + K", + "desc": "Move Window Up", + "_verb": "move-window-up", + "_mods": "Super+Ctrl", + "_mainKey": "K" + }, + { + "keys": "Super + Ctrl + L", + "desc": "Move Column Right", + "_verb": "move-column-right", + "_mods": "Super+Ctrl", + "_mainKey": "L" + }, + { + "keys": "Super + Home", + "desc": "Focus Column First", + "_verb": "focus-column-first", + "_mods": "Super", + "_mainKey": "Home" + }, + { + "keys": "Super + End", + "desc": "Focus Column Last", + "_verb": "focus-column-last", + "_mods": "Super", + "_mainKey": "End" + }, + { + "keys": "Super + Ctrl + Home", + "desc": "Move Column To First", + "_verb": "move-column-to-first", + "_mods": "Super+Ctrl", + "_mainKey": "Home" + }, + { + "keys": "Super + Ctrl + End", + "desc": "Move Column To Last", + "_verb": "move-column-to-last", + "_mods": "Super+Ctrl", + "_mainKey": "End" + }, + { + "keys": "Super + Shift + Left", + "desc": "Focus Monitor Left", + "_verb": "focus-monitor-left", + "_mods": "Super+Shift", + "_mainKey": "Left" + }, + { + "keys": "Super + Shift + Down", + "desc": "Focus Monitor Down", + "_verb": "focus-monitor-down", + "_mods": "Super+Shift", + "_mainKey": "Down" + }, + { + "keys": "Super + Shift + Up", + "desc": "Focus Monitor Up", + "_verb": "focus-monitor-up", + "_mods": "Super+Shift", + "_mainKey": "Up" + }, + { + "keys": "Super + Shift + Right", + "desc": "Focus Monitor Right", + "_verb": "focus-monitor-right", + "_mods": "Super+Shift", + "_mainKey": "Right" + }, + { + "keys": "Super + Shift + H", + "desc": "Focus Monitor Left", + "_verb": "focus-monitor-left", + "_mods": "Super+Shift", + "_mainKey": "H" + }, + { + "keys": "Super + Shift + J", + "desc": "Focus Monitor Down", + "_verb": "focus-monitor-down", + "_mods": "Super+Shift", + "_mainKey": "J" + }, + { + "keys": "Super + Shift + K", + "desc": "Focus Monitor Up", + "_verb": "focus-monitor-up", + "_mods": "Super+Shift", + "_mainKey": "K" + }, + { + "keys": "Super + Shift + L", + "desc": "Focus Monitor Right", + "_verb": "focus-monitor-right", + "_mods": "Super+Shift", + "_mainKey": "L" + }, + { + "keys": "Super + Shift + Ctrl + Left", + "desc": "Move Column To Monitor Left", + "_verb": "move-column-to-monitor-left", + "_mods": "Super+Shift+Ctrl", + "_mainKey": "Left" + }, + { + "keys": "Super + Shift + Ctrl + Down", + "desc": "Move Column To Monitor Down", + "_verb": "move-column-to-monitor-down", + "_mods": "Super+Shift+Ctrl", + "_mainKey": "Down" + }, + { + "keys": "Super + Shift + Ctrl + Up", + "desc": "Move Column To Monitor Up", + "_verb": "move-column-to-monitor-up", + "_mods": "Super+Shift+Ctrl", + "_mainKey": "Up" + }, + { + "keys": "Super + Shift + Ctrl + Right", + "desc": "Move Column To Monitor Right", + "_verb": "move-column-to-monitor-right", + "_mods": "Super+Shift+Ctrl", + "_mainKey": "Right" + }, + { + "keys": "Super + Shift + Ctrl + H", + "desc": "Move Column To Monitor Left", + "_verb": "move-column-to-monitor-left", + "_mods": "Super+Shift+Ctrl", + "_mainKey": "H" + }, + { + "keys": "Super + Shift + Ctrl + J", + "desc": "Move Column To Monitor Down", + "_verb": "move-column-to-monitor-down", + "_mods": "Super+Shift+Ctrl", + "_mainKey": "J" + }, + { + "keys": "Super + Shift + Ctrl + K", + "desc": "Move Column To Monitor Up", + "_verb": "move-column-to-monitor-up", + "_mods": "Super+Shift+Ctrl", + "_mainKey": "K" + }, + { + "keys": "Super + Shift + Ctrl + L", + "desc": "Move Column To Monitor Right", + "_verb": "move-column-to-monitor-right", + "_mods": "Super+Shift+Ctrl", + "_mainKey": "L" + }, + { + "keys": "Super + Page_Down", + "desc": "Focus Workspace Down", + "_verb": "focus-workspace-down", + "_mods": "Super", + "_mainKey": "Page_Down" + }, + { + "keys": "Super + Page_Up", + "desc": "Focus Workspace Up", + "_verb": "focus-workspace-up", + "_mods": "Super", + "_mainKey": "Page_Up" + }, + { + "keys": "Super + U", + "desc": "Focus Workspace Down", + "_verb": "focus-workspace-down", + "_mods": "Super", + "_mainKey": "U" + }, + { + "keys": "Super + I", + "desc": "Focus Workspace Up", + "_verb": "focus-workspace-up", + "_mods": "Super", + "_mainKey": "I" + }, + { + "keys": "Super + Ctrl + Page_Down", + "desc": "Move Column To Workspace Down", + "_verb": "move-column-to-workspace-down", + "_mods": "Super+Ctrl", + "_mainKey": "Page_Down" + }, + { + "keys": "Super + Ctrl + Page_Up", + "desc": "Move Column To Workspace Up", + "_verb": "move-column-to-workspace-up", + "_mods": "Super+Ctrl", + "_mainKey": "Page_Up" + }, + { + "keys": "Super + Ctrl + U", + "desc": "Move Column To Workspace Down", + "_verb": "move-column-to-workspace-down", + "_mods": "Super+Ctrl", + "_mainKey": "U" + }, + { + "keys": "Super + Ctrl + I", + "desc": "Move Column To Workspace Up", + "_verb": "move-column-to-workspace-up", + "_mods": "Super+Ctrl", + "_mainKey": "I" + }, + { + "keys": "Super + Shift + Page_Down", + "desc": "Move Workspace Down", + "_verb": "move-workspace-down", + "_mods": "Super+Shift", + "_mainKey": "Page_Down" + }, + { + "keys": "Super + Shift + Page_Up", + "desc": "Move Workspace Up", + "_verb": "move-workspace-up", + "_mods": "Super+Shift", + "_mainKey": "Page_Up" + }, + { + "keys": "Super + Shift + U", + "desc": "Move Workspace Down", + "_verb": "move-workspace-down", + "_mods": "Super+Shift", + "_mainKey": "U" + }, + { + "keys": "Super + Shift + I", + "desc": "Move Workspace Up", + "_verb": "move-workspace-up", + "_mods": "Super+Shift", + "_mainKey": "I" + }, + { + "keys": "Super + WheelScrollDown", + "desc": "Focus Workspace Down", + "_verb": "focus-workspace-down", + "_mods": "Super", + "_mainKey": "WheelScrollDown" + }, + { + "keys": "Super + WheelScrollUp", + "desc": "Focus Workspace Up", + "_verb": "focus-workspace-up", + "_mods": "Super", + "_mainKey": "WheelScrollUp" + }, + { + "keys": "Super + Ctrl + WheelScrollDown", + "desc": "Move Column To Workspace Down", + "_verb": "move-column-to-workspace-down", + "_mods": "Super+Ctrl", + "_mainKey": "WheelScrollDown" + }, + { + "keys": "Super + Ctrl + WheelScrollUp", + "desc": "Move Column To Workspace Up", + "_verb": "move-column-to-workspace-up", + "_mods": "Super+Ctrl", + "_mainKey": "WheelScrollUp" + }, + { + "keys": "Super + WheelScrollRight", + "desc": "Focus Column Right", + "_verb": "focus-column-right", + "_mods": "Super", + "_mainKey": "WheelScrollRight" + }, + { + "keys": "Super + WheelScrollLeft", + "desc": "Focus Column Left", + "_verb": "focus-column-left", + "_mods": "Super", + "_mainKey": "WheelScrollLeft" + }, + { + "keys": "Super + Ctrl + WheelScrollRight", + "desc": "Move Column Right", + "_verb": "move-column-right", + "_mods": "Super+Ctrl", + "_mainKey": "WheelScrollRight" + }, + { + "keys": "Super + Ctrl + WheelScrollLeft", + "desc": "Move Column Left", + "_verb": "move-column-left", + "_mods": "Super+Ctrl", + "_mainKey": "WheelScrollLeft" + }, + { + "keys": "Super + Shift + WheelScrollDown", + "desc": "Focus Column Right", + "_verb": "focus-column-right", + "_mods": "Super+Shift", + "_mainKey": "WheelScrollDown" + }, + { + "keys": "Super + Shift + WheelScrollUp", + "desc": "Focus Column Left", + "_verb": "focus-column-left", + "_mods": "Super+Shift", + "_mainKey": "WheelScrollUp" + }, + { + "keys": "Super + Ctrl + Shift + WheelScrollDown", + "desc": "Move Column Right", + "_verb": "move-column-right", + "_mods": "Super+Ctrl+Shift", + "_mainKey": "WheelScrollDown" + }, + { + "keys": "Super + Ctrl + Shift + WheelScrollUp", + "desc": "Move Column Left", + "_verb": "move-column-left", + "_mods": "Super+Ctrl+Shift", + "_mainKey": "WheelScrollUp" + } + ] + }, + { + "title": "best effort", + "binds": [ + { + "keys": "Super + 1-9", + "desc": "Focus Workspace 1-9", + "_verb": "focus-workspace", + "_mods": "Super", + "_mainKey": "1-9", + "_merged": true + }, + { + "keys": "Super + Ctrl + 1-9", + "desc": "Move Column To Workspace 1-9", + "_verb": "move-column-to-workspace", + "_mods": "Super+Ctrl", + "_mainKey": "1-9", + "_merged": true + }, + { + "keys": "Super + BracketLeft", + "desc": "Consume Or Expel Window Left", + "_verb": "consume-or-expel-window-left", + "_mods": "Super", + "_mainKey": "BracketLeft" + }, + { + "keys": "Super + BracketRight", + "desc": "Consume Or Expel Window Right", + "_verb": "consume-or-expel-window-right", + "_mods": "Super", + "_mainKey": "BracketRight" + }, + { + "keys": "Super + Comma", + "desc": "Consume Window Into Column", + "_verb": "consume-window-into-column", + "_mods": "Super", + "_mainKey": "Comma" + }, + { + "keys": "Super + Period", + "desc": "Expel Window From Column", + "_verb": "expel-window-from-column", + "_mods": "Super", + "_mainKey": "Period" + }, + { + "keys": "Super + R", + "desc": "Switch Preset Column Width", + "_verb": "switch-preset-column-width", + "_mods": "Super", + "_mainKey": "R" + }, + { + "keys": "Super + Shift + R", + "desc": "Switch Preset Column Width Back", + "_verb": "switch-preset-column-width-back", + "_mods": "Super+Shift", + "_mainKey": "R" + }, + { + "keys": "Super + Ctrl + Shift + R", + "desc": "Switch Preset Window Height", + "_verb": "switch-preset-window-height", + "_mods": "Super+Ctrl+Shift", + "_mainKey": "R" + }, + { + "keys": "Super + Ctrl + R", + "desc": "Reset Window Height", + "_verb": "reset-window-height", + "_mods": "Super+Ctrl", + "_mainKey": "R" + }, + { + "keys": "Super + F", + "desc": "Maximize Column", + "_verb": "maximize-column", + "_mods": "Super", + "_mainKey": "F" + }, + { + "keys": "Super + Shift + F", + "desc": "Fullscreen Window", + "_verb": "fullscreen-window", + "_mods": "Super+Shift", + "_mainKey": "F" + }, + { + "keys": "Super + M", + "desc": "Maximize Window To Edges", + "_verb": "maximize-window-to-edges", + "_mods": "Super", + "_mainKey": "M" + }, + { + "keys": "Super + Ctrl + F", + "desc": "Expand Column To Available Width", + "_verb": "expand-column-to-available-width", + "_mods": "Super+Ctrl", + "_mainKey": "F" + }, + { + "keys": "Super + Ctrl + C", + "desc": "Center Visible Columns", + "_verb": "center-visible-columns", + "_mods": "Super+Ctrl", + "_mainKey": "C" + }, + { + "keys": "Super + Minus", + "desc": "Set Column Width \" 10%\"", + "_verb": "set-column-width", + "_mods": "Super", + "_mainKey": "Minus" + }, + { + "keys": "Super + Equal", + "desc": "Set Column Width \"+10%\"", + "_verb": "set-column-width", + "_mods": "Super", + "_mainKey": "Equal" + }, + { + "keys": "Super + Shift + Minus", + "desc": "Set Window Height \" 10%\"", + "_verb": "set-window-height", + "_mods": "Super+Shift", + "_mainKey": "Minus" + }, + { + "keys": "Super + Shift + Equal", + "desc": "Set Window Height \"+10%\"", + "_verb": "set-window-height", + "_mods": "Super+Shift", + "_mainKey": "Equal" + }, + { + "keys": "Super + Shift + V", + "desc": "Toggle Window Floating", + "_verb": "toggle-window-floating", + "_mods": "Super+Shift", + "_mainKey": "V" + }, + { + "keys": "Super + W", + "desc": "Toggle Column Tabbed Display", + "_verb": "toggle-column-tabbed-display", + "_mods": "Super", + "_mainKey": "W" + }, + { + "keys": "PrtSc", + "desc": "Screenshot", + "_verb": "screenshot", + "_mods": "", + "_mainKey": "Print" + }, + { + "keys": "Ctrl + PrtSc", + "desc": "Screenshot Screen", + "_verb": "screenshot-screen", + "_mods": "Ctrl", + "_mainKey": "Print" + }, + { + "keys": "Alt + PrtSc", + "desc": "Screenshot Window", + "_verb": "screenshot-window", + "_mods": "Alt", + "_mainKey": "Print" + }, + { + "keys": "Super + Escape", + "desc": "Toggle Keyboard Shortcuts Inhibit", + "_verb": "toggle-keyboard-shortcuts-inhibit", + "_mods": "Super", + "_mainKey": "Escape" + }, + { + "keys": "Super + Shift + E", + "desc": "Quit", + "_verb": "quit", + "_mods": "Super+Shift", + "_mainKey": "E" + }, + { + "keys": "Ctrl + Alt + Delete", + "desc": "Quit", + "_verb": "quit", + "_mods": "Ctrl+Alt", + "_mainKey": "Delete" + }, + { + "keys": "Super + Shift + P", + "desc": "Power Off Monitors", + "_verb": "power-off-monitors", + "_mods": "Super+Shift", + "_mainKey": "P" + } + ] + } + ], + "detectedCompositor": "niri", + "windowWidth": 1400, + "windowHeight": 850, + "autoHeight": true, + "columnCount": 3, + "modKeyVariable": "$mod", + "hyprlandConfigPath": "~/.config/hypr/hyprland.conf", + "hyprlandLuaConfigPath": "~/.config/hypr/hyprland.lua", + "hyprlandParserMode": "auto", + "niriConfigPath": "~/.config/niri/config.kdl", + "mangoConfigPath": "~/.config/mango/config.conf", + "mergeSequentialBinds": true, + "showUndescribedBinds": true, + "splitLargeWorkspaceCategory": true, + "workspaceSplitThreshold": 12, + "keyColorAlt": "#FF6B6B", + "keyColorXF86": "#4ECDC4", + "keyColorPrint": "#95E1D3", + "keyColorNumeric": "#A8DADC", + "keyColorMouse": "#F38181", + "keyColorSuper": "", + "keyColorCtrl": "", + "keyColorShift": "", + "keyColorDefault": "#6C757D", + "keyLabelColor": "#FFFFFF", + "keyTextSuper": "", + "keyTextCtrl": "", + "keyTextShift": "", + "keyTextAlt": "", + "keyTextXF86": "", + "keyTextPrint": "", + "keyTextNumeric": "", + "keyTextMouse": "", + "keyTextDefault": "", + "keyColorMod2": "", + "keyColorMod3": "", + "keyColorMod5": "", + "keyTextMod2": "", + "keyTextMod3": "", + "keyTextMod5": "", + "keyLabelMod2": "", + "keyLabelMod3": "", + "keyLabelMod5": "", + "descriptionTextColor": "", + "bindOverrides": {} +} diff --git a/noctalia/.config/plugins/privacy-indicator/BarWidget.qml b/noctalia/.config/plugins/privacy-indicator/BarWidget.qml new file mode 100644 index 0000000..bdebd41 --- /dev/null +++ b/noctalia/.config/plugins/privacy-indicator/BarWidget.qml @@ -0,0 +1,175 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.UI +import qs.Widgets + +Item { + id: root + + property var pluginApi: null + + property ShellScreen screen + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + // Bar positioning properties + readonly property string screenName: screen ? screen.name : "" + readonly property string barPosition: Settings.getBarPositionForScreen(screenName) + readonly property bool isVertical: barPosition === "left" || barPosition === "right" + readonly property real barHeight: Style.getBarHeightForScreen(screenName) + readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName) + readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName) + + // Access main instance for state + readonly property var mainInstance: pluginApi?.mainInstance + + property bool micActive: mainInstance ? mainInstance.micActive : false + property bool camActive: mainInstance ? mainInstance.camActive : false + property bool scrActive: mainInstance ? mainInstance.scrActive : false + property var micApps: mainInstance ? mainInstance.micApps : [] + property var camApps: mainInstance ? mainInstance.camApps : [] + property var scrApps: mainInstance ? mainInstance.scrApps : [] + + property var cfg: pluginApi?.pluginSettings || ({}) + property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({}) + + property bool hideInactive: cfg.hideInactive ?? defaults.hideInactive ?? false + property bool enableToast: cfg.enableToast ?? defaults.enableToast ?? true + property bool removeMargins: cfg.removeMargins ?? defaults.removeMargins ?? false + property int iconSpacing: cfg.iconSpacing ?? defaults.iconSpacing ?? 4 + property string activeColorKey: cfg.activeColor ?? defaults.activeColor ?? "primary" + property string inactiveColorKey: cfg.inactiveColor ?? defaults.inactiveColor ?? "none" + + readonly property color activeColor: Color.resolveColorKey(activeColorKey) + readonly property color inactiveColor: inactiveColorKey === "none" ? Qt.alpha(Color.mOnSurfaceVariant, 0.3) : Color.resolveColorKey(inactiveColorKey) + readonly property color micColor: micActive ? activeColor : inactiveColor + readonly property color camColor: camActive ? activeColor : inactiveColor + readonly property color scrColor: scrActive ? activeColor : inactiveColor + + readonly property bool isVisible: !hideInactive || micActive || camActive || scrActive + + property real margins: removeMargins ? 0 : Style.marginM * 2 + + readonly property real contentWidth: isVertical ? Style.capsuleHeight : Math.round(layout.implicitWidth + margins) + readonly property real contentHeight: isVertical ? Math.round(layout.implicitHeight + margins) : Style.capsuleHeight + + implicitWidth: contentWidth + implicitHeight: contentHeight + + Layout.alignment: Qt.AlignVCenter + visible: root.isVisible + opacity: root.isVisible ? 1.0 : 0.0 + + function buildTooltip() { + var parts = []; + + if (micActive && micApps.length > 0) { + parts.push("Mic: " + micApps.join(", ")); + } + + if (camActive && camApps.length > 0) { + parts.push("Cam: " + camApps.join(", ")); + } + + if (scrActive && scrApps.length > 0) { + parts.push("Screen sharing: " + scrApps.join(", ")); + } + + return parts.length > 0 ? parts.join("\n") : ""; + } + + Rectangle { + id: visualCapsule + x: Style.pixelAlignCenter(parent.width, width) + y: Style.pixelAlignCenter(parent.height, height) + width: root.contentWidth + height: root.contentHeight + radius: Style.radiusM + color: Style.capsuleColor + border.color: Style.capsuleBorderColor + border.width: Style.capsuleBorderWidth + + Item { + id: layout + + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + + implicitWidth: iconsLayout.implicitWidth + implicitHeight: iconsLayout.implicitHeight + + GridLayout { + id: iconsLayout + + columns: root.isVertical ? 1 : 3 + rows: root.isVertical ? 3 : 1 + + rowSpacing: root.iconSpacing + columnSpacing: root.iconSpacing + + NIcon { + visible: micActive || !root.hideInactive + icon: micActive ? "microphone" : "microphone-off" + color: root.micColor + } + NIcon { + visible: camActive || !root.hideInactive + icon: camActive ? "camera" : "camera-off" + color: root.camColor + } + NIcon { + visible: scrActive || !root.hideInactive + icon: scrActive ? "screen-share" : "screen-share-off" + color: root.scrColor + } + } + } + } + + NPopupContextMenu { + id: contextMenu + + model: [ + { + "label": pluginApi?.tr("menu.settings"), + "action": "settings", + "icon": "settings" + }, + ] + + onTriggered: function (action) { + contextMenu.close(); + PanelService.closeContextMenu(screen); + if (action === "settings") { + BarService.openPluginSettings(root.screen, pluginApi.manifest); + } + } + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.RightButton | Qt.LeftButton + hoverEnabled: true + + onClicked: function (mouse) { + if (mouse.button === Qt.RightButton) { + PanelService.showContextMenu(contextMenu, root, screen); + } else if (mouse.button === Qt.LeftButton) { + if (pluginApi) pluginApi.openPanel(root.screen, root); + } + } + + onEntered: { + var tooltipText = buildTooltip(); + if (tooltipText) { + TooltipService.show(root, tooltipText, BarService.getTooltipDirection()); + } + } + onExited: TooltipService.hide() + } +} diff --git a/noctalia/.config/plugins/privacy-indicator/Main.qml b/noctalia/.config/plugins/privacy-indicator/Main.qml new file mode 100644 index 0000000..29d94a0 --- /dev/null +++ b/noctalia/.config/plugins/privacy-indicator/Main.qml @@ -0,0 +1,316 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Services.Pipewire +import qs.Commons +import qs.Services.UI + +Item { + id: root + property var pluginApi: null + + // --- Logic extracted from BarWidget.qml --- + + property bool micActive: false + property bool camActive: false + property bool scrActive: false + property var micApps: [] + property var camApps: [] + property var scrApps: [] + + property var accessHistory: [] + + // Previous states for history tracking + property var _prevMicApps: [] + property var _prevCamApps: [] + property var _prevScrApps: [] + + // Get active color from settings or default + property var cfg: pluginApi?.pluginSettings || ({}) + property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({}) + property bool enableToast: cfg.enableToast ?? defaults.enableToast ?? true + property string activeColorKey: cfg.activeColor ?? defaults.activeColor ?? "primary" + property string micFilterRegex: cfg.micFilterRegex ?? defaults.micFilterRegex ?? "" + property string camFilterRegex: cfg.camFilterRegex ?? defaults.camFilterRegex ?? "" + + PwObjectTracker { + objects: Pipewire.ready ? Pipewire.nodes.values : [] + } + + Process { + id: cameraDetectionProcess + running: false + command: ["sh", "-c", "for dev in /sys/class/video4linux/video*; do [ -e \"$dev/name\" ] && grep -qv 'Metadata' \"$dev/name\" && dev_name=$(basename \"$dev\") && find /proc/[0-9]*/fd -lname \"/dev/$dev_name\" 2>/dev/null; done | cut -d/ -f3 | xargs -r ps -o comm= -p | sort -u | tr '\\n' ',' | sed 's/,$//'"] + stdout: StdioCollector { + onStreamFinished: { + var appsString = this.text.trim(); + var apps = appsString.length > 0 ? appsString.split(',') : []; + + var filterRegex = null; + if (root.camFilterRegex && root.camFilterRegex.length > 0) { + try { + filterRegex = new RegExp(root.camFilterRegex); + } catch (e) { + Logger.w("PrivacyIndicator: Invalid camFilterRegex:", root.camFilterRegex); + } + } + + var appNames = []; + for (var i = 0; i < apps.length; i++) { + var appName = apps[i]; + if (filterRegex && appName && filterRegex.test(appName)) continue; + if (appName && appNames.indexOf(appName) === -1) appNames.push(appName); + } + + root.camApps = appNames; + root.camActive = appNames.length > 0; + } + } + } + + + Timer { + interval: 1000 + repeat: true + running: true + triggeredOnStart: true + onTriggered: updatePrivacyState() + } + + function hasNodeLinks(node, links) { + for (var i = 0; i < links.length; i++) { + var link = links[i]; + if (link && (link.source === node || link.target === node)) return true; + } + return false; + } + + function getAppName(node) { + return node.properties["application.name"] || node.nickname || node.name || ""; + } + + function updateMicrophoneState(nodes, links) { + var appNames = []; + var isActive = false; + + var filterRegex = null; + if (root.micFilterRegex && root.micFilterRegex.length > 0) { + try { + filterRegex = new RegExp(root.micFilterRegex); + } catch (e) { + Logger.w("PrivacyIndicator: Invalid micFilterRegex:", root.micFilterRegex); + } + } + + for (var i = 0; i < nodes.length; i++) { + var node = nodes[i]; + if (!node || !node.isStream || !node.audio || node.isSink) continue; + if (!hasNodeLinks(node, links) || !node.properties) continue; + var mediaClass = node.properties["media.class"] || ""; + if (mediaClass === "Stream/Input/Audio") { + if (node.properties["stream.capture.sink"] === "true") continue; + + var appName = getAppName(node); + if (filterRegex && appName && filterRegex.test(appName)) continue; + + isActive = true; + if (appName && appNames.indexOf(appName) === -1) appNames.push(appName); + } + } + root.micActive = isActive; + root.micApps = appNames; + } + + function updateCameraState() { + cameraDetectionProcess.running = true; + } + + function isScreenShareNode(node) { + if (!node.properties) return false; + var mediaClass = node.properties["media.class"] || ""; + if (mediaClass.indexOf("Audio") >= 0) return false; + if (mediaClass.indexOf("Video") === -1) return false; + var mediaName = (node.properties["media.name"] || "").toLowerCase(); + if (mediaName.match(/^(xdph-streaming|gsr-default|game capture|screen|desktop|display|cast|webrtc|v4l2)/) || + mediaName === "gsr-default_output" || + mediaName.match(/screen-cast|screen-capture|desktop-capture|monitor-capture|window-capture|game-capture/i)) { + return true; + } + return false; + } + + function updateScreenShareState(nodes, links) { + var appNames = []; + var isActive = false; + for (var i = 0; i < nodes.length; i++) { + var node = nodes[i]; + if (!node || !hasNodeLinks(node, links) || !node.properties) continue; + if (isScreenShareNode(node)) { + isActive = true; + var appName = getAppName(node); + if (appName && appNames.indexOf(appName) === -1) appNames.push(appName); + } + } + root.scrActive = isActive; + root.scrApps = appNames; + } + + function updatePrivacyState() { + if (!Pipewire.ready) return; + var nodes = Pipewire.nodes.values || []; + var links = Pipewire.links.values || []; + updateMicrophoneState(nodes, links); + updateCameraState(); + updateScreenShareState(nodes, links); + } + + // --- History Persistence --- + + property string stateFile: "" + property bool isLoaded: false + + Component.onCompleted: { + // Setup state file path + Qt.callLater(() => { + if (typeof Settings !== 'undefined' && Settings.cacheDir) { + stateFile = Settings.cacheDir + "privacy-history.json"; + historyFileView.path = stateFile; + } + }); + } + + FileView { + id: historyFileView + printErrors: false + watchChanges: false + + adapter: JsonAdapter { + id: adapter + property var history: [] + } + + onLoaded: { + root.isLoaded = true; + if (adapter.history) { + // Restore history + root.accessHistory = adapter.history; + } + } + + onLoadFailed: error => { + // If file doesn't exist (error 2), we are ready to save new data + if (error === 2) { + root.isLoaded = true; + } else { + console.error("PrivacyIndicator: Failed to load history file:", error); + root.isLoaded = true; // Try to continue anyway + } + } + } + + function saveHistory() { + if (!stateFile || !isLoaded) return; + + adapter.history = root.accessHistory; + + // Ensure cache directory exists and save + try { + Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]); + Qt.callLater(() => { + try { + historyFileView.writeAdapter(); + } catch (e) { + console.error("PrivacyIndicator: Failed to save history", e); + } + }); + } catch (e) { + console.error("PrivacyIndicator: Failed to save history", e); + } + } + + function addToHistory(app, type, icon, colorKey, action) { + var time = new Date().toLocaleTimeString(Qt.locale(), Locale.ShortFormat); + var entry = { + "appName": app, + "type": type, + "icon": icon, + "colorKey": colorKey, + "time": time, + "timestamp": Date.now(), + "action": action // "started" or "stopped" + }; + var newHistory = [entry].concat(accessHistory); + if (newHistory.length > 50) newHistory = newHistory.slice(0, 50); // Increased limit as we have more entries now + accessHistory = newHistory; + saveHistory(); + } + + function clearHistory() { + accessHistory = []; + saveHistory(); + } + + function checkAppChanges(newApps, oldApps, type, icon, colorKey) { + if (!newApps && !oldApps) return; + + // Check for new apps (Started) + if (newApps) { + for (var i = 0; i < newApps.length; i++) { + var app = newApps[i]; + if (!oldApps || oldApps.indexOf(app) === -1) { + addToHistory(app, type, icon, colorKey, "started"); + } + } + } + + // Check for removed apps (Stopped) + if (oldApps) { + for (var j = 0; j < oldApps.length; j++) { + var oldApp = oldApps[j]; + if (!newApps || newApps.indexOf(oldApp) === -1) { + addToHistory(oldApp, type, icon, colorKey, "stopped"); + } + } + } + } + + + onMicAppsChanged: { + checkAppChanges(micApps, _prevMicApps, "Microphone", "microphone", activeColorKey); + _prevMicApps = micApps; + } + // Helper to detect activation edge + property bool oldMicActive: false + onMicActiveChanged: { + if (enableToast && micActive && !oldMicActive) { + ToastService.showNotice(pluginApi?.tr("toast.mic-on"), "", "microphone"); + } + oldMicActive = micActive + } + + property bool oldCamActive: false + onCamActiveChanged: { + if (enableToast && camActive && !oldCamActive) { + ToastService.showNotice(pluginApi?.tr("toast.cam-on"), "", "camera"); + } + oldCamActive = camActive + } + onCamAppsChanged: { + checkAppChanges(camApps, _prevCamApps, "Camera", "camera", activeColorKey); + _prevCamApps = camApps; + } + + property bool oldScrActive: false + onScrActiveChanged: { + if (enableToast && scrActive && !oldScrActive) { + ToastService.showNotice(pluginApi?.tr("toast.screen-on"), "", "screen-share"); + } + oldScrActive = scrActive + } + onScrAppsChanged: { + checkAppChanges(scrApps, _prevScrApps, "Screen", "screen-share", activeColorKey); + _prevScrApps = scrApps; + } + + +} diff --git a/noctalia/.config/plugins/privacy-indicator/Panel.qml b/noctalia/.config/plugins/privacy-indicator/Panel.qml new file mode 100644 index 0000000..0cfcc22 --- /dev/null +++ b/noctalia/.config/plugins/privacy-indicator/Panel.qml @@ -0,0 +1,170 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Widgets + +Item { + id: root + + property var pluginApi: null + + // Standard panel properties + readonly property var geometryPlaceholder: panelContainer + property real contentPreferredWidth: 320 * Style.uiScaleRatio + property real contentPreferredHeight: 450 * Style.uiScaleRatio + + readonly property var mainInstance: pluginApi?.mainInstance + readonly property bool allowAttach: true + + Rectangle { + id: panelContainer + anchors.fill: parent + color: "transparent" + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + // Header Box + NBox { + Layout.fillWidth: true + Layout.preferredHeight: headerRow.implicitHeight + Style.marginM * 2 + + RowLayout { + id: headerRow + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginS + + NIcon { + icon: "shield-check" + color: Color.mPrimary + pointSize: Style.fontSizeL + } + + NText { + Layout.fillWidth: true + text: pluginApi?.tr("history.title") + font.weight: Style.fontWeightBold + pointSize: Style.fontSizeL + color: Color.mOnSurface + } + + NIconButton { + icon: "trash" + baseSize: Style.baseWidgetSize * 0.8 + onClicked: { + if (mainInstance) mainInstance.clearHistory(); + } + } + } + } + + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + NScrollView { + id: scrollView + anchors.fill: parent + horizontalPolicy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AsNeeded + + ColumnLayout { + width: scrollView.availableWidth + spacing: Style.marginS + + Repeater { + model: mainInstance ? mainInstance.accessHistory : [] + + delegate: Rectangle { + Layout.fillWidth: true + implicitHeight: 56 * Style.uiScaleRatio + radius: Style.radiusM + color: Color.mSurfaceVariant + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + Rectangle { + width: 32 * Style.uiScaleRatio + height: 32 * Style.uiScaleRatio + radius: width/2 + color: Qt.alpha(iconColor, 0.1) + + readonly property color iconColor: Color.resolveColorKey(modelData.colorKey || "primary") + + NIcon { + anchors.centerIn: parent + icon: modelData.icon + color: parent.iconColor + pointSize: Style.fontSizeM + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + NText { + Layout.fillWidth: true + text: modelData.appName + elide: Text.ElideRight + font.weight: Style.fontWeightBold + pointSize: Style.fontSizeM + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + NText { + text: modelData.time + color: Qt.alpha(Color.mOnSurface, 0.7) + pointSize: Style.fontSizeS + } + + NText { + text: "•" + color: Qt.alpha(Color.mOnSurface, 0.3) + pointSize: Style.fontSizeS + } + + NText { + text: { + const action = modelData.action || "started"; + return pluginApi?.tr("history.action." + action) || action; + } + color: (modelData.action || "started") === "stopped" ? Color.resolveColorKey("error") : Color.resolveColorKey("primary") + font.weight: Style.fontWeightBold + pointSize: Style.fontSizeS + } + } + } + } + } + } + + // Empty state + NText { + Layout.alignment: Qt.AlignHCenter + visible: (!mainInstance || mainInstance.accessHistory.length === 0) + text: pluginApi?.tr("history.empty") + color: Qt.alpha(Color.mOnSurface, 0.5) + pointSize: Style.fontSizeM + Layout.topMargin: Style.marginL + } + + Item { Layout.fillHeight: true } // spacer + } + } + } + } + } +} diff --git a/noctalia/.config/plugins/privacy-indicator/README.md b/noctalia/.config/plugins/privacy-indicator/README.md new file mode 100644 index 0000000..d192040 --- /dev/null +++ b/noctalia/.config/plugins/privacy-indicator/README.md @@ -0,0 +1,47 @@ +# Privacy Indicator Plugin + +A privacy indicator widget that monitors and displays when microphone, camera, or screen sharing is active on your system. + +## Features + +- **Microphone Monitoring**: Detects active microphone usage via Pipewire +- **Camera Monitoring**: Detects active camera usage by checking `/dev/video*` devices +- **Screen Sharing Detection**: Monitors screen sharing sessions via Pipewire +- **Visual Indicators**: Shows icons that change color based on active state + - Active: Primary color + - Inactive: Semi-transparent variant color +- **App Information**: Tooltip displays which applications are using each resource +- **Adaptive Layout**: Automatically adjusts layout for horizontal or vertical bar positions + +## Configuration + +Access the plugin settings in Noctalia to configure the following options: + +- **Hide Inactive States**: If enabled, microphone, camera, and screen icons are hidden whenever they are inactive. Only active states are shown. +- **Remove Margins**: If enabled, removes all outer margins of the widget. +- **Icon Spacing**: Controls the horizontal/vertical spacing between the icons. +- **Active/Inactive Icon Color**: Customize the colors for active and inactive states. +- **Microphone Filter Regex**: Regex pattern to filter out specific microphone applications. Matching apps are completely excluded from detection (they won't trigger the indicator or appear in tooltips). Use `|` to specify multiple patterns, e.g., `effect_input.rnnoise|easyeffects`. + + +## Usage + +The widget displays three icons in the bar: +- **Microphone**: Shows when any app is using the microphone +- **Camera**: Shows when any app is accessing the camera +- **Screen Share**: Shows when screen sharing is active + +Hover over the widget to see a tooltip listing which applications are using each resource. + +## Requirements + +- Noctalia Shell 3.6.0 or higher +- Pipewire (for microphone and screen sharing detection) +- Access to `/dev/video*` devices (for camera detection) + +## Technical Details + +- Updates privacy state every second +- Uses Pipewire API to monitor audio/video streams +- Checks `/proc/[0-9]*/fd/` for camera device access +- Detects screen sharing by analyzing Pipewire node properties and media class diff --git a/noctalia/.config/plugins/privacy-indicator/Settings.qml b/noctalia/.config/plugins/privacy-indicator/Settings.qml new file mode 100644 index 0000000..a6acde9 --- /dev/null +++ b/noctalia/.config/plugins/privacy-indicator/Settings.qml @@ -0,0 +1,142 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + + property var pluginApi: null + + property var cfg: pluginApi?.pluginSettings || ({}) + property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({}) + + property bool hideInactive: cfg.hideInactive ?? defaults.hideInactive ?? false + property bool enableToast: cfg.enableToast ?? defaults.enableToast ?? true + property bool removeMargins: cfg.removeMargins ?? defaults.removeMargins ?? false + property int iconSpacing: cfg.iconSpacing ?? defaults.iconSpacing ?? 4 + property string activeColor: cfg.activeColor ?? defaults.activeColor ?? "primary" + property string inactiveColor: cfg.inactiveColor ?? defaults.inactiveColor ?? "none" + property string micFilterRegex: cfg.micFilterRegex ?? defaults.micFilterRegex + property string camFilterRegex: cfg.camFilterRegex ?? defaults.camFilterRegex + + spacing: Style.marginL + + Component.onCompleted: { + Logger.i("PrivacyIndicator", "Settings UI loaded"); + } + + ColumnLayout { + spacing: Style.marginM + Layout.fillWidth: true + + NToggle { + label: pluginApi?.tr("settings.hideInactive.label") + description: pluginApi?.tr("settings.hideInactive.desc") + + checked: root.hideInactive + onToggled: checked => { + root.hideInactive = checked; + } + } + + NToggle { + label: pluginApi?.tr("settings.enableToast.label") + description: pluginApi?.tr("settings.enableToast.desc") + + checked: root.enableToast + onToggled: checked => { + root.enableToast = checked; + } + } + + NToggle { + label: pluginApi?.tr("settings.removeMargins.label") + description: pluginApi?.tr("settings.removeMargins.desc") + + checked: root.removeMargins + onToggled: checked => { + root.removeMargins = checked; + } + } + + NColorChoice { + label: pluginApi?.tr("settings.activeColor.label") + description: pluginApi?.tr("settings.activeColor.desc") + currentKey: root.activeColor + onSelected: key => root.activeColor = key + } + + NColorChoice { + label: pluginApi?.tr("settings.inactiveColor.label") + description: pluginApi?.tr("settings.inactiveColor.desc") + currentKey: root.inactiveColor + onSelected: key => root.inactiveColor = key + noneColor: Qt.alpha(Color.mOnSurfaceVariant, 0.3) + noneOnColor: Qt.alpha(Color.mOnSurface, 0.7) + } + + NComboBox { + label: pluginApi?.tr("settings.iconSpacing.label") + description: pluginApi?.tr("settings.iconSpacing.desc") + + model: { + const labels = ["XXS", "XS", "S", "M", "L", "XL"]; + const values = [Style.marginXXS, Style.marginXS, Style.marginS, Style.marginM, Style.marginL, Style.marginXL]; + + const result = []; + for (var i = 0; i < labels.length; ++i) { + const v = values[i]; + result.push({ + key: v.toFixed(0), + name: `${labels[i]} (${v}px)` + }); + } + return result; + } + + // INFO: From my understanding, the toFixed(0) shouldn't be needed here and there, but without the + // current key does not show when opening the settings window. + currentKey: root.iconSpacing.toFixed(0) + onSelected: key => root.iconSpacing = key + } + + NTextInput { + Layout.fillWidth: true + label: pluginApi?.tr("settings.micFilterRegex.label") + description: pluginApi?.tr("settings.micFilterRegex.desc") + placeholderText: "effect_input.rnnoise|easyeffects" + text: root.micFilterRegex + onTextChanged: root.micFilterRegex = text + } + + NTextInput { + Layout.fillWidth: true + label: pluginApi?.tr("settings.camFilterRegex.label") + description: pluginApi?.tr("settings.camFilterRegex.desc") + placeholderText: "droidcam" + text: root.camFilterRegex + onTextChanged: root.camFilterRegex = text + } + } + + function saveSettings() { + if (!pluginApi) { + Logger.e("PrivacyIndicator", "Cannot save settings: pluginApi is null"); + return; + } + + pluginApi.pluginSettings.hideInactive = root.hideInactive; + pluginApi.pluginSettings.enableToast = root.enableToast; + pluginApi.pluginSettings.iconSpacing = root.iconSpacing; + pluginApi.pluginSettings.removeMargins = root.removeMargins; + pluginApi.pluginSettings.activeColor = root.activeColor; + pluginApi.pluginSettings.inactiveColor = root.inactiveColor; + pluginApi.pluginSettings.micFilterRegex = root.micFilterRegex; + pluginApi.pluginSettings.camFilterRegex = root.camFilterRegex; + + pluginApi.saveSettings(); + + Logger.i("PrivacyIndicator", "Settings saved successfully"); + } +} diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/de.json b/noctalia/.config/plugins/privacy-indicator/i18n/de.json new file mode 100644 index 0000000..5817042 --- /dev/null +++ b/noctalia/.config/plugins/privacy-indicator/i18n/de.json @@ -0,0 +1,58 @@ +{ + "menu": { + "settings": "Widget Einstellungen" + }, + "settings": { + "activeColor": { + "desc": "Farbe der Symbole, wenn sie aktiv sind.", + "label": "Aktive Farbsymbol" + }, + "hideInactive": { + "desc": "Mikrofon-, Kamera- und Bildschirmsymbole ausblenden, wenn sie inaktiv sind.", + "label": "Inaktive Zustände ausblenden" + }, + "enableToast": { + "desc": "Zeige Toast Mitteilungen an, wenn sich einer der Zustände ändert.", + "label": "Aktiviere Toast Mitteilungen" + }, + "inactiveColor": { + "desc": "Farbe der Symbole, wenn sie inaktiv sind.", + "label": "Inaktive Farbsymbol" + }, + "iconSpacing": { + "desc": "Den Abstand zwischen den Symbolen festlegen.", + "label": "Symbolabstand" + }, + "removeMargins": { + "desc": "Alle äußeren Ränder des Widgets entfernen.", + "label": "Ränder entfernen" + }, + "micFilterRegex": { + "desc": "Regex Muster zum Herausfiltern von Mikrofon-Apps. Entsprechende Apps werden vollständig von der Erkennung ausgeschlossen.", + "label": "Regex für Mikrofonfilter" + }, + "camFilterRegex": { + "desc": "Regex-Muster zum Herausfiltern von Kamera-Apps. Entsprechende Apps werden vollständig von der Erkennung ausgeschlossen.", + "label": "Regex für Kamerafilter" + } + }, + "tooltip": { + "cam-on": "Kamera: {apps}", + "mic-on": "Mikrofon: {apps}", + "screen-on": "Bildschirmfreigabe: {apps}" + }, + "toast": { + "cam-on": "Kamera ist aktiv", + "mic-on": "Mikrofon ist aktiv", + "screen-on": "Bildschirmfreigabe ist aktiv" + }, + "history": { + "title": "Zugriffsverlauf", + "empty": "Kein kürzlicher Zugriff", + "clear": "Leeren", + "action": { + "started": "Gestartet", + "stopped": "Beendet" + } + } +} diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/en.json b/noctalia/.config/plugins/privacy-indicator/i18n/en.json new file mode 100644 index 0000000..fa65418 --- /dev/null +++ b/noctalia/.config/plugins/privacy-indicator/i18n/en.json @@ -0,0 +1,58 @@ +{ + "menu": { + "settings": "Widget settings" + }, + "settings": { + "activeColor": { + "desc": "Color of the icons when active.", + "label": "Active icon color" + }, + "hideInactive": { + "desc": "Hide microphone, camera, and screen icons when there are inactive.", + "label": "Hide inactive states" + }, + "enableToast": { + "desc": "Show toast messages when one of the states changes.", + "label": "Enable toast notifications" + }, + "inactiveColor": { + "desc": "Color of the icons when inactive.", + "label": "Inactive icon color" + }, + "iconSpacing": { + "desc": "Set the spacing between the icons.", + "label": "Icon spacing" + }, + "removeMargins": { + "desc": "Remove all outer margins of the widget.", + "label": "Remove margins" + }, + "micFilterRegex": { + "desc": "Regex pattern to filter out microphone applications. Matching apps are completely excluded from detection.", + "label": "Microphone filter regex" + }, + "camFilterRegex": { + "desc": "Regex pattern to filter out camera applications. Matching apps are completely excluded from detection.", + "label": "Camera filter regex" + } + }, + "tooltip": { + "cam-on": "Camera: {apps}", + "mic-on": "Microphone: {apps}", + "screen-on": "Screen sharing: {apps}" + }, + "toast": { + "cam-on": "Camera is active", + "mic-on": "Microphone is active", + "screen-on": "Screen sharing is active" + }, + "history": { + "title": "Access History", + "empty": "No recent access", + "clear": "Clear", + "action": { + "started": "Started", + "stopped": "Stopped" + } + } +} diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/es.json b/noctalia/.config/plugins/privacy-indicator/i18n/es.json new file mode 100644 index 0000000..93a48a7 --- /dev/null +++ b/noctalia/.config/plugins/privacy-indicator/i18n/es.json @@ -0,0 +1,46 @@ +{ + "settings": { + "hideInactive": { + "desc": "Ocultar los iconos de micrófono, cámara y pantalla cuando estén inactivos.", + "label": "Ocultar estados inactivos" + }, + "iconSpacing": { + "desc": "Configurar el espacio entre los iconos.", + "label": "Espaciado de iconos" + }, + "removeMargins": { + "desc": "Quita todos los márgenes externos del widget.", + "label": "Quitar márgenes" + }, + "activeColor": { + "desc": "Color de los iconos cuando están activos.", + "label": "Color de icono activo" + }, + "inactiveColor": { + "desc": "Color de los iconos cuando están inactivos.", + "label": "Color de icono inactivo" + } + }, + "tooltip": { + "cam-on": "Cámara: {apps}", + "mic-on": "Micrófono: {apps}", + "screen-on": "Compartir pantalla: {apps}" + }, + "toast": { + "cam-on": "La cámara está activa", + "mic-on": "El micrófono está activo", + "screen-on": "Compartir pantalla está activo" + }, + "menu": { + "settings": "Configuración del widget" + }, + "history": { + "title": "Historial de acceso", + "empty": "Sin accesos recientes", + "clear": "Borrar", + "action": { + "started": "Iniciado", + "stopped": "Detenido" + } + } +}
\ No newline at end of file diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/fr.json b/noctalia/.config/plugins/privacy-indicator/i18n/fr.json new file mode 100644 index 0000000..814c07d --- /dev/null +++ b/noctalia/.config/plugins/privacy-indicator/i18n/fr.json @@ -0,0 +1,58 @@ +{ + "menu": { + "settings": "Paramètres du widget" + }, + "settings": { + "activeColor": { + "desc": "Couleur des icônes lorsqu'elles sont actives.", + "label": "Couleur d'icône active" + }, + "hideInactive": { + "desc": "Masquer les icônes du micro, de la caméra et de l’écran lorsqu’ils sont inactifs.", + "label": "Masquer les états inactifs" + }, + "enableToast": { + "desc": "Afficher des messages toast lorsque l'un des états change.", + "label": "Activer les notifications toast" + }, + "inactiveColor": { + "desc": "Couleur des icônes lorsqu'elles sont inactives.", + "label": "Couleur d'icône inactive" + }, + "iconSpacing": { + "desc": "Définir l’espacement entre les icônes.", + "label": "Espacement des icônes" + }, + "removeMargins": { + "desc": "Supprime toutes les marges extérieures du widget.", + "label": "Supprimer les marges" + }, + "micFilterRegex": { + "desc": "Motif Regex pour filtrer les applications de microphone. Les applications correspondantes sont complètement exclues de la détection.", + "label": "Regex de filtrage du microphone" + }, + "camFilterRegex": { + "desc": "Motif Regex pour filtrer les applications de caméra. Les applications correspondantes sont complètement exclues de la détection.", + "label": "Regex de filtrage de la caméra" + } + }, + "tooltip": { + "cam-on": "Caméra: {apps}", + "mic-on": "Microphone: {apps}", + "screen-on": "Partage d'écran: {apps}" + }, + "toast": { + "cam-on": "La caméra est active", + "mic-on": "Le microphone est actif", + "screen-on": "Le partage d'écran est actif" + }, + "history": { + "title": "Historique d'accès", + "empty": "Aucun accès récent", + "clear": "Effacer", + "action": { + "started": "Démarré", + "stopped": "Arrêté" + } + } +} diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/hu.json b/noctalia/.config/plugins/privacy-indicator/i18n/hu.json new file mode 100644 index 0000000..4a7ba8c --- /dev/null +++ b/noctalia/.config/plugins/privacy-indicator/i18n/hu.json @@ -0,0 +1,46 @@ +{ + "settings": { + "hideInactive": { + "desc": "Mikrofon, kamera és képernyő ikonok elrejtése, ha éppen nincsenek használatban.", + "label": "Inaktív állapotok elrejtése" + }, + "iconSpacing": { + "desc": "Állítsa be az ikonok közötti távolságot.", + "label": "Ikon távolság" + }, + "removeMargins": { + "desc": "Távolítsd el a widget összes külső margóját.", + "label": "Margók eltávolítása" + }, + "activeColor": { + "desc": "Az ikonok színe, amikor aktívak.", + "label": "Aktív ikon színe" + }, + "inactiveColor": { + "desc": "Az ikonok színe, amikor inaktívak.", + "label": "Inaktív ikon színe" + } + }, + "tooltip": { + "cam-on": "Kamera: {apps}", + "mic-on": "Mikrofon: {apps}", + "screen-on": "Képernyőmegosztás: {apps}" + }, + "toast": { + "cam-on": "A kamera aktív", + "mic-on": "A mikrofon aktív", + "screen-on": "Képernyőmegosztás aktív" + }, + "menu": { + "settings": "Widget beállítások" + }, + "history": { + "title": "Hozzáférési előzmények", + "empty": "Nincs nemrégiben történt hozzáférés", + "clear": "Törlés", + "action": { + "started": "Elindítva", + "stopped": "Leállítva" + } + } +}
\ No newline at end of file diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/it.json b/noctalia/.config/plugins/privacy-indicator/i18n/it.json new file mode 100644 index 0000000..d0eee79 --- /dev/null +++ b/noctalia/.config/plugins/privacy-indicator/i18n/it.json @@ -0,0 +1,46 @@ +{ + "settings": { + "hideInactive": { + "desc": "Nascondi le icone di microfono, fotocamera e schermo quando sono inattive.", + "label": "Nascondi stati inattivi" + }, + "iconSpacing": { + "desc": "Imposta la distanza tra le icone.", + "label": "Spaziatura delle icone" + }, + "removeMargins": { + "desc": "Rimuove tutti i margini esterni del widget.", + "label": "Rimuovi margini" + }, + "activeColor": { + "desc": "Colore delle icone quando sono attive.", + "label": "Colore icona attiva" + }, + "inactiveColor": { + "desc": "Colore delle icone quando sono inattive.", + "label": "Colore icona inattiva" + } + }, + "tooltip": { + "cam-on": "Kamera: {apps}", + "mic-on": "Mikrofonoa: {apps}", + "screen-on": "Ekran-partaĝado: {apps}" + }, + "toast": { + "cam-on": "La fotocamera è attiva", + "mic-on": "Il microfono è attivo", + "screen-on": "La condivisione dello schermo è attiva" + }, + "menu": { + "settings": "Impostazioni widget" + }, + "history": { + "title": "Cronologia accessi", + "empty": "Nessun accesso recente", + "clear": "Pulisci", + "action": { + "started": "Iniziato", + "stopped": "Terminato" + } + } +}
\ No newline at end of file diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/ja.json b/noctalia/.config/plugins/privacy-indicator/i18n/ja.json new file mode 100644 index 0000000..ba61aeb --- /dev/null +++ b/noctalia/.config/plugins/privacy-indicator/i18n/ja.json @@ -0,0 +1,46 @@ +{ + "settings": { + "hideInactive": { + "desc": "マイク・カメラ・画面共有のアイコンを、非アクティブなときは非表示にします。", + "label": "非アクティブ状態を非表示" + }, + "iconSpacing": { + "desc": "アイコン同士の間隔を設定します。", + "label": "アイコン間隔" + }, + "removeMargins": { + "desc": "ウィジェットの外側の余白をすべて削除します。", + "label": "余白を削除" + }, + "activeColor": { + "desc": "アクティブ時のアイコンの色。", + "label": "アクティブ時の色" + }, + "inactiveColor": { + "desc": "非アクティブ時のアイコンの色。", + "label": "非アクティブ時の色" + } + }, + "tooltip": { + "cam-on": "カメラ: {apps}", + "mic-on": "マイク: {apps}", + "screen-on": "画面共有: {apps}" + }, + "toast": { + "cam-on": "カメラがアクティブです", + "mic-on": "マイクがアクティブです", + "screen-on": "画面共有がアクティブです" + }, + "menu": { + "settings": "ウィジェット設定" + }, + "history": { + "title": "アクセス履歴", + "empty": "最近のアクセスはありません", + "clear": "クリア", + "action": { + "started": "開始", + "stopped": "停止" + } + } +}
\ No newline at end of file diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/ko.json b/noctalia/.config/plugins/privacy-indicator/i18n/ko.json new file mode 100644 index 0000000..e104202 --- /dev/null +++ b/noctalia/.config/plugins/privacy-indicator/i18n/ko.json @@ -0,0 +1,58 @@ +{ + "menu": { + "settings": "위젯 설정" + }, + "settings": { + "activeColor": { + "desc": "활성 상태일 때 아이콘에 적용되는 색상입니다.", + "label": "활성 아이콘 색상" + }, + "hideInactive": { + "desc": "비활성화된 상태의 마이크, 카메라, 화면 아이콘을 숨깁니다.", + "label": "비활성 상태 숨기기" + }, + "enableToast": { + "desc": "상태가 변경될 때 토스트 알림을 표시합니다.", + "label": "토스트 알림 활성화" + }, + "inactiveColor": { + "desc": "비활성 상태일 때 아이콘에 적용되는 색상입니다.", + "label": "비활성 아이콘 색상" + }, + "iconSpacing": { + "desc": "아이콘 사이의 간격을 설정합니다.", + "label": "아이콘 간격" + }, + "removeMargins": { + "desc": "위젯의 모든 외곽 여백을 제거합니다.", + "label": "여백 제거" + }, + "micFilterRegex": { + "desc": "마이크를 사용하는 애플리케이션을 필터링하기 위한 정규식을 지정합니다. 일치하는 애플리케이션은 상태 감지에서 완전히 제외됩니다.", + "label": "마이크 필터 정규식" + }, + "camFilterRegex": { + "desc": "카메라를 사용하는 애플리케이션을 필터링하기 위한 정규식을 지정합니다. 일치하는 애플리케이션은 상태 감지에서 완전히 제외됩니다.", + "label": "카메라 필터 정규식" + } + }, + "tooltip": { + "cam-on": "카메라: {apps}", + "mic-on": "마이크: {apps}", + "screen-on": "화면 공유: {apps}" + }, + "toast": { + "cam-on": "카메라가 활성화되었습니다.", + "mic-on": "마이크가 활성화되었습니다.", + "screen-on": "화면 공유가 활성화되었습니다." + }, + "history": { + "title": "접근 기록", + "empty": "최근에 접근한 기록 없음", + "clear": "비우기", + "action": { + "started": "시작됨", + "stopped": "정지됨" + } + } +} diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/ku.json b/noctalia/.config/plugins/privacy-indicator/i18n/ku.json new file mode 100644 index 0000000..6a58f84 --- /dev/null +++ b/noctalia/.config/plugins/privacy-indicator/i18n/ku.json @@ -0,0 +1,46 @@ +{ + "settings": { + "hideInactive": { + "desc": "Îkonên mîkrofon, kamera û ekranê dema ku neçalak bin veşêre.", + "label": "Rewşa neçalak veşêre" + }, + "iconSpacing": { + "desc": "Cihê di navbera îkonan de diyar bike.", + "label": "Dûrahiya îkonan" + }, + "removeMargins": { + "desc": "Hemû marjînalên derveyî yên widgetê rake.", + "label": "Derdestên derxînin" + }, + "activeColor": { + "desc": "Color of the icons when active.", + "label": "Active icon color" + }, + "inactiveColor": { + "desc": "Color of the icons when inactive.", + "label": "Inactive icon color" + } + }, + "tooltip": { + "cam-on": "Kamera: {apps}", + "mic-on": "Mîkrofon: {apps}", + "screen-on": "Parvekirina ekranê: {apps}" + }, + "toast": { + "cam-on": "Kamera çalak e", + "mic-on": "Mîkrofon çalak e", + "screen-on": "Parvekirina ekranê çalak e" + }, + "menu": { + "settings": "Widget settings" + }, + "history": { + "title": "Access History", + "empty": "No recent access", + "clear": "Clear", + "action": { + "started": "Started", + "stopped": "Stopped" + } + } +}
\ No newline at end of file diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/nl.json b/noctalia/.config/plugins/privacy-indicator/i18n/nl.json new file mode 100644 index 0000000..d0b031d --- /dev/null +++ b/noctalia/.config/plugins/privacy-indicator/i18n/nl.json @@ -0,0 +1,46 @@ +{ + "settings": { + "hideInactive": { + "desc": "Verberg de pictogrammen voor microfoon, camera en scherm wanneer ze inactief zijn.", + "label": "Inactieve status verbergen" + }, + "iconSpacing": { + "desc": "Stel de afstand tussen de pictogrammen in.", + "label": "Pictogramafstand" + }, + "removeMargins": { + "desc": "Verwijdert alle buitenste marges van de widget.", + "label": "Marges verwijderen" + }, + "activeColor": { + "desc": "Kleur van de pictogrammen wanneer ze actief zijn.", + "label": "Actieve pictogramkleur" + }, + "inactiveColor": { + "desc": "Kleur van de pictogrammen wanneer ze inactief zijn.", + "label": "Inactieve pictogramkleur" + } + }, + "tooltip": { + "cam-on": "Camera: {apps}", + "mic-on": "Microfoon: {apps}", + "screen-on": "Schermdeling: {apps}" + }, + "toast": { + "cam-on": "Camera is actief", + "mic-on": "Microfoon is actief", + "screen-on": "Scherm delen is actief" + }, + "menu": { + "settings": "Widget instellingen" + }, + "history": { + "title": "Toegangsgeschiedenis", + "empty": "Geen recente toegang", + "clear": "Wissen", + "action": { + "started": "Gestart", + "stopped": "Gestopt" + } + } +}
\ No newline at end of file diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/pl.json b/noctalia/.config/plugins/privacy-indicator/i18n/pl.json new file mode 100644 index 0000000..9e05d3a --- /dev/null +++ b/noctalia/.config/plugins/privacy-indicator/i18n/pl.json @@ -0,0 +1,46 @@ +{ + "settings": { + "hideInactive": { + "desc": "Ukryj ikony mikrofonu, kamery i ekranu, gdy są nieaktywne.", + "label": "Ukryj nieaktywne stany" + }, + "iconSpacing": { + "desc": "Ustaw odstęp między ikonami.", + "label": "Odstępy ikon" + }, + "removeMargins": { + "desc": "Usuń wszystkie zewnętrzne marginesy widżetu.", + "label": "Usuń marginesy" + }, + "activeColor": { + "desc": "Kolor ikon, gdy są aktywne.", + "label": "Kolor aktywnej ikony" + }, + "inactiveColor": { + "desc": "Kolor ikon, gdy są nieaktywne.", + "label": "Kolor nieaktywnej ikony" + } + }, + "tooltip": { + "cam-on": "Kamera: {apps}", + "mic-on": "Mikrofon: {apps}", + "screen-on": "Udostępnianie ekranu: {apps}" + }, + "toast": { + "cam-on": "Kamera jest aktywna", + "mic-on": "Mikrofon jest aktywny", + "screen-on": "Udostępnianie ekranu jest aktywne" + }, + "menu": { + "settings": "Ustawienia widżetu" + }, + "history": { + "title": "Historia dostępu", + "empty": "Brak ostatnich dostępów", + "clear": "Wyczyść", + "action": { + "started": "Rozpoczęto", + "stopped": "Zatrzymano" + } + } +}
\ No newline at end of file diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/pt.json b/noctalia/.config/plugins/privacy-indicator/i18n/pt.json new file mode 100644 index 0000000..068a528 --- /dev/null +++ b/noctalia/.config/plugins/privacy-indicator/i18n/pt.json @@ -0,0 +1,46 @@ +{ + "settings": { + "hideInactive": { + "desc": "Oculta os ícones de microfone, câmera e tela quando estiverem inativos.", + "label": "Ocultar estados inativos" + }, + "iconSpacing": { + "desc": "Define o espaçamento entre os ícones.", + "label": "Espaçamento dos ícones" + }, + "removeMargins": { + "desc": "Remove todas as margens externas do widget.", + "label": "Remover margens" + }, + "activeColor": { + "desc": "Cor dos ícones quando ativos.", + "label": "Cor do ícone ativo" + }, + "inactiveColor": { + "desc": "Cor dos ícones quando inativos.", + "label": "Cor do ícone inativo" + } + }, + "tooltip": { + "cam-on": "Câmera: {apps}", + "mic-on": "Microfone: {apps}", + "screen-on": "Compartilhamento de tela: {apps}" + }, + "toast": { + "cam-on": "A câmera está ativa", + "mic-on": "O microfone está ativo", + "screen-on": "O compartilhamento de tela está ativo" + }, + "menu": { + "settings": "Configurações do widget" + }, + "history": { + "title": "Histórico de acesso", + "empty": "Sem acessos recentes", + "clear": "Limpar", + "action": { + "started": "Iniciado", + "stopped": "Interrompido" + } + } +}
\ No newline at end of file diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/ru.json b/noctalia/.config/plugins/privacy-indicator/i18n/ru.json new file mode 100644 index 0000000..152e7d8 --- /dev/null +++ b/noctalia/.config/plugins/privacy-indicator/i18n/ru.json @@ -0,0 +1,46 @@ +{ + "settings": { + "hideInactive": { + "desc": "Скрывать значки микрофона, камеры и экрана, когда они неактивны.", + "label": "Скрывать неактивные состояния" + }, + "iconSpacing": { + "desc": "Задать расстояние между значками.", + "label": "Интервал между значками" + }, + "removeMargins": { + "desc": "Удаляет все внешние отступы виджета.", + "label": "Убрать отступы" + }, + "activeColor": { + "desc": "Цвет иконок при активации.", + "label": "Цвет активной иконки" + }, + "inactiveColor": { + "desc": "Цвет иконок в спокойном состоянии.", + "label": "Цвет неактивной иконки" + } + }, + "tooltip": { + "cam-on": "Камера: {apps}", + "mic-on": "Микрофон: {apps}", + "screen-on": "Демонстрация экрана: {apps}" + }, + "toast": { + "cam-on": "Камера активна", + "mic-on": "Микрофон активен", + "screen-on": "Демонстрация экрана активна" + }, + "menu": { + "settings": "Настройки виджета" + }, + "history": { + "title": "История доступа", + "empty": "Нет недавних доступов", + "clear": "Очистить", + "action": { + "started": "Начато", + "stopped": "Остановлено" + } + } +}
\ No newline at end of file diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/tr.json b/noctalia/.config/plugins/privacy-indicator/i18n/tr.json new file mode 100644 index 0000000..016f1be --- /dev/null +++ b/noctalia/.config/plugins/privacy-indicator/i18n/tr.json @@ -0,0 +1,46 @@ +{ + "settings": { + "hideInactive": { + "desc": "Mikrofon, kamera ve ekran simgelerini pasif olduklarında gizle.", + "label": "Pasif durumları gizle" + }, + "iconSpacing": { + "desc": "Simgeler arasındaki boşluğu ayarla.", + "label": "Simge aralığı" + }, + "removeMargins": { + "desc": "Widget’ın tüm dış kenar boşluklarını kaldırır.", + "label": "Kenarlıkları kaldır" + }, + "activeColor": { + "desc": "Aktif olduğunda simgelerin rengi.", + "label": "Aktif simge rengi" + }, + "inactiveColor": { + "desc": "Devre dışı olduğunda simgelerin rengi.", + "label": "Pasif simge rengi" + } + }, + "tooltip": { + "cam-on": "Kamera: {apps}", + "mic-on": "Mikrofon: {apps}", + "screen-on": "Ekran paylaşımı: {apps}" + }, + "toast": { + "cam-on": "Kamera aktif", + "mic-on": "Mikrofon aktif", + "screen-on": "Ekran paylaşımı aktif" + }, + "menu": { + "settings": "Bileşen ayarları" + }, + "history": { + "title": "Erişim Geçmişi", + "empty": "Yakın zamanda erişim yok", + "clear": "Temizle", + "action": { + "started": "Başlatıldı", + "stopped": "Durduruldu" + } + } +}
\ No newline at end of file diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/uk-UA.json b/noctalia/.config/plugins/privacy-indicator/i18n/uk-UA.json new file mode 100644 index 0000000..5b872a7 --- /dev/null +++ b/noctalia/.config/plugins/privacy-indicator/i18n/uk-UA.json @@ -0,0 +1,46 @@ +{ + "settings": { + "hideInactive": { + "desc": "Приховувати значки мікрофона, камери та екрана, коли вони неактивні.", + "label": "Приховувати неактивні стани" + }, + "iconSpacing": { + "desc": "Встановити відстань між значками.", + "label": "Інтервал між значками" + }, + "removeMargins": { + "desc": "Видаляє всі зовнішні відступи віджета.", + "label": "Прибрати відступи" + }, + "activeColor": { + "desc": "Колір іконок при активації.", + "label": "Колір активної іконки" + }, + "inactiveColor": { + "desc": "Колір іконок у спокійному стані.", + "label": "Колір неактивної іконки" + } + }, + "tooltip": { + "cam-on": "Камера: {apps}", + "mic-on": "Мікрофон: {apps}", + "screen-on": "Демонстрація екрана: {apps}" + }, + "toast": { + "cam-on": "Камера активна", + "mic-on": "Мікрофон активний", + "screen-on": "Демонстрація екрана активна" + }, + "menu": { + "settings": "Налаштування віджета" + }, + "history": { + "title": "Історія доступу", + "empty": "Немає недавніх доступів", + "clear": "Очистити", + "action": { + "started": "Розпочато", + "stopped": "Зупинено" + } + } +}
\ No newline at end of file diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/vi.json b/noctalia/.config/plugins/privacy-indicator/i18n/vi.json new file mode 100644 index 0000000..6cd6d76 --- /dev/null +++ b/noctalia/.config/plugins/privacy-indicator/i18n/vi.json @@ -0,0 +1,54 @@ +{ + "menu": { + "settings": "Cài đặt tiện ích" + }, + "settings": { + "activeColor": { + "desc": "Màu của biểu tượng khi đang hoạt động.", + "label": "Màu biểu tượng khi hoạt động" + }, + "hideInactive": { + "desc": "Ẩn biểu tượng micro, camera và màn hình khi không hoạt động.", + "label": "Ẩn trạng thái không hoạt động" + }, + "enableToast": { + "desc": "Hiển thị thông báo khi một trạng thái thay đổi.", + "label": "Bật thông báo" + }, + "inactiveColor": { + "desc": "Màu của biểu tượng khi không hoạt động.", + "label": "Màu biểu tượng khi không hoạt động" + }, + "iconSpacing": { + "desc": "Thiết lập khoảng cách giữa các biểu tượng.", + "label": "Khoảng cách biểu tượng" + }, + "removeMargins": { + "desc": "Loại bỏ toàn bộ lề ngoài của widget.", + "label": "Xóa lề" + }, + "micFilterRegex": { + "desc": "Biểu thức chính quy để lọc các ứng dụng sử dụng micro. Các ứng dụng khớp sẽ bị loại khỏi việc phát hiện.", + "label": "Regex lọc micro" + } + }, + "tooltip": { + "cam-on": "Camera: {apps}", + "mic-on": "Micro: {apps}", + "screen-on": "Chia sẻ màn hình: {apps}" + }, + "toast": { + "cam-on": "Camera đang hoạt động", + "mic-on": "Micro đang hoạt động", + "screen-on": "Chia sẻ màn hình đang hoạt động" + }, + "history": { + "title": "Lịch sử truy cập", + "empty": "Không có truy cập gần đây", + "clear": "Xóa", + "action": { + "started": "Bắt đầu", + "stopped": "Dừng" + } + } +} diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/zh-CN.json b/noctalia/.config/plugins/privacy-indicator/i18n/zh-CN.json new file mode 100644 index 0000000..5665d78 --- /dev/null +++ b/noctalia/.config/plugins/privacy-indicator/i18n/zh-CN.json @@ -0,0 +1,46 @@ +{ + "settings": { + "hideInactive": { + "desc": "在麦克风、摄像头和屏幕图标处于非活动状态时将其隐藏。", + "label": "隐藏非活动状态" + }, + "iconSpacing": { + "desc": "设置图标之间的间距。", + "label": "图标间距" + }, + "removeMargins": { + "desc": "移除小部件所有外部边距。", + "label": "移除边距" + }, + "activeColor": { + "desc": "激活图标的颜色。", + "label": "激活图标颜色" + }, + "inactiveColor": { + "desc": "未激活图标的颜色。", + "label": "未激活图标颜色" + } + }, + "tooltip": { + "cam-on": "摄像头: {apps}", + "mic-on": "麦克风: {apps}", + "screen-on": "屏幕共享: {apps}" + }, + "toast": { + "cam-on": "摄像头已激活", + "mic-on": "麦克风已激活", + "screen-on": "屏幕共享已激活" + }, + "menu": { + "settings": "小部件设置" + }, + "history": { + "title": "访问历史", + "empty": "无最近访问", + "clear": "清除", + "action": { + "started": "已开始", + "stopped": "已停止" + } + } +}
\ No newline at end of file diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/zh-TW.json b/noctalia/.config/plugins/privacy-indicator/i18n/zh-TW.json new file mode 100644 index 0000000..febeadc --- /dev/null +++ b/noctalia/.config/plugins/privacy-indicator/i18n/zh-TW.json @@ -0,0 +1,46 @@ +{ + "settings": { + "hideInactive": { + "desc": "當麥克風, 攝影機及螢幕分享沒有啟動時就直接隱藏", + "label": "隱藏未啟動的狀態" + }, + "iconSpacing": { + "desc": "設定圖示之間的留空", + "label": "圖示間距" + }, + "removeMargins": { + "desc": "移除小工具外面的所有邊距", + "label": "移除邊距" + }, + "activeColor": { + "desc": "圖示啟用時的顏色。", + "label": "啟用圖示顏色" + }, + "inactiveColor": { + "desc": "圖示未啟用時的顏色。", + "label": "未啟用圖示顏色" + } + }, + "tooltip": { + "cam-on": "攝影機: {apps}", + "mic-on": "麥克風: {apps}", + "screen-on": "螢幕分享: {apps}" + }, + "toast": { + "cam-on": "攝影機已啟用", + "mic-on": "麥克風已啟用", + "screen-on": "螢幕分享已啟用" + }, + "menu": { + "settings": "小工具設定" + }, + "history": { + "title": "存取紀錄", + "empty": "無最近存取", + "clear": "清除", + "action": { + "started": "已開始", + "stopped": "已停止" + } + } +}
\ No newline at end of file diff --git a/noctalia/.config/plugins/privacy-indicator/manifest.json b/noctalia/.config/plugins/privacy-indicator/manifest.json new file mode 100644 index 0000000..4618fc4 --- /dev/null +++ b/noctalia/.config/plugins/privacy-indicator/manifest.json @@ -0,0 +1,37 @@ +{ + "id": "privacy-indicator", + "name": "Privacy Indicator", + "version": "1.2.13", + "minNoctaliaVersion": "3.6.0", + "author": "Noctalia Team", + "official": true, + "license": "MIT", + "repository": "https://github.com/noctalia-dev/noctalia-plugins", + "description": "A privacy indicator widget that shows when microphone, camera or screen sharing is active.", + "tags": [ + "Bar", + "Privacy", + "Indicator" + ], + "entryPoints": { + "main": "Main.qml", + "barWidget": "BarWidget.qml", + "panel": "Panel.qml", + "settings": "Settings.qml" + }, + "dependencies": { + "plugins": [] + }, + "metadata": { + "defaultSettings": { + "hideInactive": false, + "enableToast": true, + "removeMargins": false, + "iconSpacing": 4, + "activeColor": "primary", + "inactiveColor": "none", + "micFilterRegex": "", + "camFilterRegex": "" + } + } +} diff --git a/noctalia/.config/plugins/privacy-indicator/preview.png b/noctalia/.config/plugins/privacy-indicator/preview.png Binary files differnew file mode 100644 index 0000000..0db7b7c --- /dev/null +++ b/noctalia/.config/plugins/privacy-indicator/preview.png |
