Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions keyviz/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Key Visualizer

A minimal, real-time floating on-screen keystroke visualizer HUD for Noctalia. Displays typed keys and modifier combinations as translucent, blurred glass keycaps directly on your screen.

## Plugin

| Field | Value |
| --- | --- |
| ID | `h-jangra/keyviz` |
| Entries | Bar widget: `widget`; panel: `overlay`; shortcut: `toggle`; service: `keylistener` |

## Requirements

- `python3` on `PATH` (used by the background event listener service).
- Linux user in the `input` group to read `/dev/input/event*` devices:
```sh
sudo usermod -a -G input $USER
```
*(Note: Log out and log back in for group membership to take effect).*

## Usage

### Panel

Toggle the key visualizer overlay HUD directly via IPC:

```sh
noctalia msg panel-toggle h-jangra/keyviz:overlay
```

### Bar Widget & Shortcut

- **Bar Widget (`widget`)**: Add `h-jangra/keyviz:widget` to your bar items in Noctalia settings to get a quick visual status icon and toggle button.
- **Control Center Shortcut (`toggle`)**: Add `h-jangra/keyviz:toggle` to your control center shortcuts to pause or resume key visualization.

### IPC Commands

Control the listener service dynamically:

```sh
# Toggle key visualizer on/off
noctalia msg plugin h-jangra/keyviz:keylistener all toggle

# Clear currently displayed keys
noctalia msg plugin h-jangra/keyviz:keylistener all clear
```

## Settings

Configure Keyviz in **Noctalia Settings → Plugins → Key Visualizer**:

| Setting | Type | Default | Description |
| --- | --- | --- | --- |
| `enabled_by_default` | `bool` | `true` | Start visualizer automatically when Noctalia starts. |
| `padding` | `int` | `6` | Internal padding spacing inside overlay around keycaps (`0` – `30`). |
| `margin` | `int` | `6` | Spacing gap between visualized key combinations (`0` – `30`). |
| `timeout_ms` | `int` | `500` | Inactivity duration (ms) before keys disappear (`200` – `5000`). |
| `max_keys` | `int` | `4` | Maximum number of key combinations to display (`1` – `8`). |
| `font_size` | `select` | `medium` | Text size of keycaps (`small`, `medium`, `large`). |
| `badge_style` | `select` | `glass` | Visual style (`glass`, `solid`, `accent`). |
| `show_modifiers_only` | `bool` | `false` | Only visualize combinations with Ctrl, Alt, Shift, or Super. |

## License

MIT
122 changes: 122 additions & 0 deletions keyviz/overlay.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
--!nonstrict

local symbols = {
Up = "↑",
Down = "↓",
Left = "←",
Right = "→",
Enter = "↵",
Backspace = "⌫",
Tab = "⇥",
Space = "␣",
Esc = "Esc",
Super = "⌘",
Ctrl = "Ctrl",
Alt = "Alt",
Shift = "⇧",
}

local function getFontSize()
local size = noctalia.getConfig("font_size") or "medium"

if size == "small" then
return 11
elseif size == "large" then
return 15
end

return 13
end

local function getStyle()
local style = noctalia.getConfig("badge_style") or "glass"

if style == "solid" then
return "surface_container_high", "outline", 1
elseif style == "accent" then
return "primary/0.16", "primary/0.75", 1
end

return "surface_container_high/0.92", "outline/0.55", 1
end

local function render()
local keys = noctalia.state.get("active_keys") or {}

if #keys == 0 then
panel.render(ui.row({}))
return
end

local fontSize = getFontSize()
local fill, border, borderWidth = getStyle()
local gap = noctalia.getConfig("margin") or 6
local padding = noctalia.getConfig("padding") or 6
local items = {}

for _, combo in ipairs(keys) do
local children = {}
local parts = string.split(combo, " + ")

for i, part in ipairs(parts) do
table.insert(children, ui.label({
text = symbols[part] or part,
fontSize = fontSize,
fontWeight = "bold",
color = "on_surface",
}))

if i < #parts then
table.insert(children, ui.label({
text = "+",
fontSize = math.max(9, fontSize - 3),
fontWeight = "bold",
color = "on_surface_variant",
}))
end
end

local isSingleKey = #parts == 1 and #combo <= 3
local padH = isSingleKey and 7 or 10
local itemHeight = 30

table.insert(items, ui.row({
height = itemHeight,
paddingH = padH,
gap = 6,
fill = fill,
border = border,
borderWidth = borderWidth,
radius = 7,
align = "center",
justify = "center",
}, children))
end

panel.render(ui.row({
flexGrow = 1,
align = "center",
justify = "center",
}, {
ui.row({
gap = gap,
padding = padding,
align = "center",
justify = "center",
}, items),
}))
end

noctalia.state.watch("active_keys", render)

function onConfigChanged()
render()
end

function onOpen()
render()
end

render()


110 changes: 110 additions & 0 deletions keyviz/plugin.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
id = "h-jangra/keyviz"
name = "Key Visualizer"
version = "0.1.0"
plugin_api = 13
author = "h-jangra"
license = "MIT"
dependencies = ["python3"]
tags = ["utility", "bar", "service", "shortcut", "panel"]
icon = "keyboard"
description = "Real-time floating on-screen keystroke visualizer for Noctalia."

[[setting]]
key = "enabled_by_default"
type = "bool"
label_key = "settings.enabled_by_default.label"
description_key = "settings.enabled_by_default.description"
default = true

[[setting]]
key = "padding"
type = "int"
label_key = "settings.padding.label"
description_key = "settings.padding.description"
default = 6
min = 0
max = 30
step = 1

[[setting]]
key = "margin"
type = "int"
label_key = "settings.margin.label"
description_key = "settings.margin.description"
default = 6
min = 0
max = 30
step = 1

[[setting]]
key = "timeout_ms"
type = "int"
label_key = "settings.timeout_ms.label"
description_key = "settings.timeout_ms.description"
default = 500
min = 200
max = 5000
step = 100

[[setting]]
key = "max_keys"
type = "int"
label_key = "settings.max_keys.label"
description_key = "settings.max_keys.description"
default = 4
min = 1
max = 8

[[setting]]
key = "font_size"
type = "select"
label_key = "settings.font_size.label"
description_key = "settings.font_size.description"
options = [
{ value = "small", label_key = "settings.font_size.options.small" },
{ value = "medium", label_key = "settings.font_size.options.medium" },
{ value = "large", label_key = "settings.font_size.options.large" },
]
default = "medium"

[[setting]]
key = "badge_style"
type = "select"
label_key = "settings.badge_style.label"
description_key = "settings.badge_style.description"
options = [
{ value = "glass", label_key = "settings.badge_style.options.glass" },
{ value = "solid", label_key = "settings.badge_style.options.solid" },
{ value = "accent", label_key = "settings.badge_style.options.accent" },
]
default = "glass"

[[setting]]
key = "show_modifiers_only"
type = "bool"
label_key = "settings.show_modifiers_only.label"
description_key = "settings.show_modifiers_only.description"
default = false

[[service]]
id = "keylistener"
entry = "service.luau"

[[panel]]
id = "overlay"
entry = "overlay.luau"
width = 340
height = 48
placement = "floating"
position = "bottom_center"
persistent = true
dismiss_on_outside_click = false
keyboard_focus = "none"

[[widget]]
id = "widget"
entry = "widget.luau"

[[shortcut]]
id = "toggle"
entry = "shortcut.luau"
Loading
Loading