feat: add noctwhspr plugin
This commit is contained in:
@@ -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.
|
||||||
@@ -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: 25 KiB |
@@ -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."
|
||||||
|
}
|
||||||
@@ -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…")
|
||||||
Reference in New Issue
Block a user