Merge pull request #35 from goodroot/add-noctwhspr

feat: add noctwhspr plugin
This commit is contained in:
Lysec
2026-07-17 15:48:30 +02:00
committed by GitHub
5 changed files with 223 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
# noctwhspr
Noctalia companion for [hyprwhspr](https://github.com/goodroot/hyprwhspr) —
native speech-to-text for Linux. hyprwhspr is fast, accurate, private,
system-wide dictation: press a hotkey, talk, and your words land in whatever
you were typing — transcribed by local in-memory models (Whisper, Parakeet)
that never leave your machine, or by top cloud APIs if you point it there.
Its visuals even theme themselves to match your Noctalia theme.
This plugin puts hyprwhspr on your bar: the widget shows the dictation state
at a glance, left-click starts or stops a recording, and right-click restarts
the hyprwhspr service if it ever gets stuck.
## Plugin
| Field | Value |
| --- | --- |
| ID | `goodroot/noctwhspr` |
| Entries | Bar widget: `status` |
## Requirements
Install [`hyprwhspr`](https://github.com/goodroot/hyprwhspr) and set it up so
its `hyprwhspr.service` systemd user service exists. The widget drives
hyprwhspr's own tray script (`hyprwhspr-tray.sh`), which it looks up under the
install root — `/usr/lib/hyprwhspr` for the Arch/AUR package. Nothing else is
needed; the widget has no network access of its own.
## Usage
Enable the plugin in Settings → Plugins, then add the `status` widget to your
bar in Settings → Bar.
The glyph tracks the dictation state reported by hyprwhspr:
| Glyph | State |
| --- | --- |
| Filled record dot (error color) | Recording — dictation is capturing audio |
| Microphone (primary color) | Ready — service is up, waiting for a hotkey or click |
| Zzz | Model unloaded — first recording will load it |
| Microphone off | Service stopped |
| Warning triangle | Error — the tooltip explains what is wrong |
Interactions:
- **Left-click** — toggle recording (same as the hyprwhspr hotkey). If the
service is stopped, this starts it first.
- **Right-click** — restart the `hyprwhspr.service` systemd user service.
- **Hover** — the tooltip shows the detailed status line, including error
reasons when something is wrong.
## Settings
| Setting | Type | Default | Description |
| --- | --- | --- | --- |
| `root` | `string` | `/usr/lib/hyprwhspr` | Directory hyprwhspr is installed under. Leave at the default unless hyprwhspr lives at a non-standard prefix; `HYPRWHSPR_ROOT` in Noctalia's environment is also honored. |
## Notes
- The widget is a thin shim over hyprwhspr's tray script — the same script
that backs its Waybar module — so state detection stays in one place. It
polls `<root>/config/hyprland/hyprwhspr-tray.sh status` every 2 seconds and
renders the returned JSON.
- Spawned processes: the tray script on every poll and click; the script in
turn queries `systemctl --user` and hyprwhspr's runtime state files.
Right-click runs `systemctl --user restart hyprwhspr.service` through the
script. No network calls and no filesystem writes are made by the plugin
itself.
- If the widget shows "hyprwhspr not found", hyprwhspr is not installed at
the configured root — point the `root` setting at your install prefix.
- Works on any compositor Noctalia runs on; the `hyprland` in the script path
is historical, hyprwhspr itself supports Hyprland, Niri and friends.
+21
View File
@@ -0,0 +1,21 @@
id = "goodroot/noctwhspr"
name = "noctwhspr"
version = "1.0.0"
plugin_api = 3
author = "goodroot"
license = "MIT"
icon = "microphone"
description = "Noctalia companion for hyprwhspr: shows dictation state, click to record, right-click to restart."
dependencies = ["hyprwhspr"]
tags = ["audio", "bar", "recording", "utility"]
[[widget]]
id = "status"
entry = "widget.luau"
[[widget.setting]]
key = "root"
type = "string"
label_key = "settings.root.label"
description_key = "settings.root.description"
default = "/usr/lib/hyprwhspr"
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

+5
View File
@@ -0,0 +1,5 @@
{
"title": "noctwhspr",
"settings.root.label": "hyprwhspr install root",
"settings.root.description": "Directory hyprwhspr is installed under. Leave at the default unless hyprwhspr lives at a non-standard prefix."
}
+125
View File
@@ -0,0 +1,125 @@
--!nonstrict
-- hyprwhspr bar widget for Noctalia.
--
-- Thin shim over the existing tray script (single source of truth for state
-- detection, shared with the Waybar module): polls `hyprwhspr-tray.sh status`
-- and renders its JSON (class + tooltip) as a glyph. Clicks invoke the same
-- script actions the Waybar module binds.
local TRAY_REL = "/config/hyprland/hyprwhspr-tray.sh"
-- state class (from the tray script's JSON) -> glyph + palette color
local STATES = {
recording = { glyph = "player-record-filled", color = "error" },
ready = { glyph = "microphone", color = "primary" },
unloaded = { glyph = "zzz", color = "outline" },
stopped = { glyph = "microphone-off", color = "outline" },
error = { glyph = "alert-triangle", color = "error" },
}
local function applyState(state, tooltip)
barWidget.setGlyph(state.glyph)
barWidget.setGlyphColor(state.color)
if tooltip ~= nil and tooltip ~= "" then
barWidget.setTooltip(tooltip)
end
end
local function applyError(tooltip)
applyState(STATES.error, tooltip)
end
-- Resolve the tray script lazily and cache the hit. Noctalia's process is
-- spawned by the compositor, so HYPRWHSPR_ROOT is usually absent from its
-- environment — the widget "root" setting is the override for non-standard
-- install prefixes.
local trayCached = nil
local function trayPath()
if trayCached ~= nil and noctalia.fileExists(trayCached) then
return trayCached
end
trayCached = nil
local candidates = {}
local cfg = noctalia.getConfig("root")
if type(cfg) == "string" and cfg ~= "" then
table.insert(candidates, cfg)
end
local env = noctalia.getenv("HYPRWHSPR_ROOT")
if env ~= nil and env ~= "" then
table.insert(candidates, env)
end
table.insert(candidates, "/usr/lib/hyprwhspr")
for _, root in ipairs(candidates) do
local path = root .. TRAY_REL
if noctalia.fileExists(path) then
trayCached = path
return path
end
end
return nil
end
local function trayCommand(action)
local path = trayPath()
if path == nil then
return nil
end
-- Single-quote the path: install roots may contain spaces.
return "'" .. path .. "' " .. action
end
local function render(json)
if type(json) ~= "string" or json == "" then
applyError("hyprwhspr: tray script returned no status")
return
end
local class = json:match('"class":"([^"]*)"') or "error"
local tooltip = json:match('"tooltip":"([^"]*)"') or ""
-- Strip the Waybar cache-buster line, unescape \n
tooltip = tooltip:gsub("\\n_ts:%d+", "")
tooltip = tooltip:gsub("\\n", "\n")
applyState(STATES[class] or STATES.error, tooltip)
end
local function refresh()
local cmd = trayCommand("status")
if cmd == nil then
applyError("hyprwhspr not found\nInstall hyprwhspr or set this widget's root setting")
return
end
noctalia.runAsync(cmd, function(result)
if type(result) ~= "table" then
applyError("hyprwhspr: status query failed")
return
end
render(result.stdout)
end)
end
function update()
-- The tray script re-derives state via many subprocesses per call; 2s is
-- responsive enough (the recording overlay gives immediate feedback).
noctalia.setUpdateInterval(2000)
refresh()
end
function onClick()
local cmd = trayCommand("record")
if cmd ~= nil then
noctalia.runAsync(cmd, function() refresh() end)
end
end
function onRightClick()
local cmd = trayCommand("restart")
if cmd ~= nil then
noctalia.runAsync(cmd, function() refresh() end)
end
end
-- Honest placeholder until the first poll lands.
barWidget.setGlyph("microphone")
barWidget.setGlyphColor("outline")
barWidget.setTooltip("hyprwhspr: waiting for status…")