diff --git a/voxtype/README.md b/voxtype/README.md new file mode 100644 index 0000000..7297e62 --- /dev/null +++ b/voxtype/README.md @@ -0,0 +1,81 @@ +# Voxtype + +Voxtype adds a bar indicator and recording controls for Voxtype. It uses +configurable Noctalia glyphs and colors while preserving Voxtype's own status +tooltip. + +## Plugin + +| Field | Value | +| --- | --- | +| ID | `gabedunn/voxtype` | +| Entries | Bar widget: `status` | + +## Requirements + +Install [`voxtype`](https://github.com/peteonrails/voxtype) and make sure the +`voxtype` command is available on `PATH`. Configure Voxtype and its recording +hotkey before using this widget. + +## Usage + +Enable the plugin in Settings → Plugins, then add the `status` widget to your +bar in Settings → Bar. + +The widget follows Voxtype's live status and updates as soon as it changes: + +| State | Default glyph | Default color | Tooltip | +| --- | --- | --- | --- | +| `idle` | `microphone-2-off` | `on_surface` | Voxtype ready - hold hotkey to record | +| `streaming` | `microphone-2` | `error` | Streaming live... | +| `recording` | `microphone-2` | `error` | Recording... | +| `transcribing` | `loader` | `primary` | Transcribing... | +| `stopped` | `microphone-2-off` | `error` | Voxtype not running | + +Hover over the widget to see the tooltip supplied by Voxtype. By default the +bar shows only the glyph; enable **Show status text** to display Voxtype's +`alt` field beside it. Enable **Extended status** to request the model, device, +and backend. Voxtype always adds all three to the expanded tooltip; nested +settings let you choose which fields also appear on the bar. + +Default pointer actions: + +- **Left-click** runs `voxtype record toggle` to start or stop recording. +- **Right-click** runs `voxtype record stop` to stop recording. +- **Middle-click** opens the widget settings using Noctalia's standard action. + +You can override these gesture bindings for each widget instance in the bar +settings. + +## Settings + +| Setting | Type | Default | Description | +| --- | --- | --- | --- | +| `show_alt` | `bool` | `false` | Show the current state (`idle`, `streaming`, `recording`, `transcribing`, or `stopped`) beside the glyph. | +| `show_extended` | `bool` | `false` | Enable extended output. The tooltip includes model, device, and backend. | +| `show_model` | `bool` | `true` | Show the model on the bar when extended status is enabled. | +| `show_device` | `bool` | `true` | Show the audio device on the bar when extended status is enabled. | +| `show_backend` | `bool` | `true` | Show the backend on the bar when extended status is enabled. | +| `idle_glyph` | `glyph` | `microphone-2-off` | Glyph shown in the idle state. | +| `idle_color` | `color` | `on_surface` | Glyph color in the idle state. | +| `streaming_glyph` | `glyph` | `microphone-2` | Glyph shown in the streaming state. | +| `streaming_color` | `color` | `error` | Glyph color in the streaming state. | +| `recording_glyph` | `glyph` | `microphone-2` | Glyph shown in the recording state. | +| `recording_color` | `color` | `error` | Glyph color in the recording state. | +| `transcribing_glyph` | `glyph` | `loader` | Glyph shown while Voxtype is transcribing recorded audio. | +| `transcribing_color` | `color` | `primary` | Glyph color in the transcribing state. | +| `stopped_glyph` | `glyph` | `microphone-2-off` | Glyph shown while Voxtype is not running. | +| `stopped_color` | `color` | `error` | Glyph color in the stopped state. | + +## Notes + +For each bar instance, the widget starts a managed shell loop that runs one +`voxtype status --follow --format json` process at a time, adding `--extended` +when extended status is enabled. If the status process exits, the loop waits two +seconds before restarting it. Noctalia stops the process group when the widget +unloads. Each JSON line replaces the current glyph, color, optional status text, +and tooltip; the emoji in Voxtype's `text` field is not rendered. + +The plugin makes no network requests and does not directly read or write files. +Its click actions invoke Voxtype, which records and processes audio according to +your Voxtype configuration. diff --git a/voxtype/plugin.toml b/voxtype/plugin.toml new file mode 100644 index 0000000..09befcc --- /dev/null +++ b/voxtype/plugin.toml @@ -0,0 +1,126 @@ +id = "gabedunn/voxtype" +name = "Voxtype" +version = "1.0.0" +plugin_api = 14 +author = "Gabe Dunn" +license = "MIT" +icon = "microphone-2" +description = "Shows and controls Voxtype's live dictation status from the bar." +dependencies = ["voxtype"] +tags = ["audio", "bar", "indicator", "recording", "utility"] + +[[widget]] +id = "status" +entry = "widget.luau" + + [widget.actions] + left = "exec voxtype record toggle" + right = "exec voxtype record stop" + + [[widget.setting]] + key = "show_alt" + type = "bool" + label_key = "settings.show_alt.label" + description_key = "settings.show_alt.description" + default = false + + [[widget.setting]] + key = "show_extended" + type = "bool" + label_key = "settings.show_extended.label" + description_key = "settings.show_extended.description" + default = false + + [[widget.setting]] + key = "show_model" + type = "bool" + label_key = "settings.show_model.label" + description_key = "settings.show_model.description" + default = true + visible_when = { key = "show_extended", values = ["true"] } + + [[widget.setting]] + key = "show_device" + type = "bool" + label_key = "settings.show_device.label" + description_key = "settings.show_device.description" + default = true + visible_when = { key = "show_extended", values = ["true"] } + + [[widget.setting]] + key = "show_backend" + type = "bool" + label_key = "settings.show_backend.label" + description_key = "settings.show_backend.description" + default = true + visible_when = { key = "show_extended", values = ["true"] } + + [[widget.setting]] + key = "idle_glyph" + type = "glyph" + label_key = "settings.idle_glyph.label" + description_key = "settings.idle_glyph.description" + default = "microphone-2-off" + + [[widget.setting]] + key = "idle_color" + type = "color" + label_key = "settings.idle_color.label" + description_key = "settings.idle_color.description" + default = "on_surface" + + [[widget.setting]] + key = "streaming_glyph" + type = "glyph" + label_key = "settings.streaming_glyph.label" + description_key = "settings.streaming_glyph.description" + default = "microphone-2" + + [[widget.setting]] + key = "streaming_color" + type = "color" + label_key = "settings.streaming_color.label" + description_key = "settings.streaming_color.description" + default = "error" + + [[widget.setting]] + key = "recording_glyph" + type = "glyph" + label_key = "settings.recording_glyph.label" + description_key = "settings.recording_glyph.description" + default = "microphone-2" + + [[widget.setting]] + key = "recording_color" + type = "color" + label_key = "settings.recording_color.label" + description_key = "settings.recording_color.description" + default = "error" + + [[widget.setting]] + key = "transcribing_glyph" + type = "glyph" + label_key = "settings.transcribing_glyph.label" + description_key = "settings.transcribing_glyph.description" + default = "loader" + + [[widget.setting]] + key = "transcribing_color" + type = "color" + label_key = "settings.transcribing_color.label" + description_key = "settings.transcribing_color.description" + default = "primary" + + [[widget.setting]] + key = "stopped_glyph" + type = "glyph" + label_key = "settings.stopped_glyph.label" + description_key = "settings.stopped_glyph.description" + default = "microphone-2-off" + + [[widget.setting]] + key = "stopped_color" + type = "color" + label_key = "settings.stopped_color.label" + description_key = "settings.stopped_color.description" + default = "error" diff --git a/voxtype/thumbnail.webp b/voxtype/thumbnail.webp new file mode 100644 index 0000000..69ab3a4 Binary files /dev/null and b/voxtype/thumbnail.webp differ diff --git a/voxtype/translations/en.json b/voxtype/translations/en.json new file mode 100644 index 0000000..2da4f34 --- /dev/null +++ b/voxtype/translations/en.json @@ -0,0 +1,72 @@ +{ + "settings": { + "idle_color": { + "description": "Color of the icon while Voxtype is ready and waiting.", + "label": "Idle icon color" + }, + "idle_glyph": { + "description": "Noctalia glyph shown while Voxtype is ready and waiting.", + "label": "Idle icon" + }, + "recording_color": { + "description": "Color of the icon while Voxtype is recording.", + "label": "Recording icon color" + }, + "recording_glyph": { + "description": "Noctalia glyph shown while Voxtype is recording.", + "label": "Recording icon" + }, + "show_alt": { + "description": "Show Voxtype's current state (idle, streaming, recording, transcribing, or stopped) next to the icon.", + "label": "Show status text" + }, + "show_backend": { + "description": "Show the active audio backend on the bar.", + "label": "Show backend on bar" + }, + "show_device": { + "description": "Show the active audio device on the bar.", + "label": "Show device on bar" + }, + "show_extended": { + "description": "Request Voxtype's extended status. The tooltip includes model, device, and backend; the settings below control which fields also appear on the bar.", + "label": "Extended status" + }, + "show_model": { + "description": "Show the active transcription model on the bar.", + "label": "Show model on bar" + }, + "streaming_color": { + "description": "Color of the icon while Voxtype is streaming live audio.", + "label": "Streaming icon color" + }, + "streaming_glyph": { + "description": "Noctalia glyph shown while Voxtype is streaming live audio.", + "label": "Streaming icon" + }, + "stopped_color": { + "description": "Color of the icon while Voxtype is not running.", + "label": "Stopped icon color" + }, + "stopped_glyph": { + "description": "Noctalia glyph shown while Voxtype is not running.", + "label": "Stopped icon" + }, + "transcribing_color": { + "description": "Color of the icon while Voxtype is transcribing recorded audio.", + "label": "Transcribing icon color" + }, + "transcribing_glyph": { + "description": "Noctalia glyph shown while Voxtype is transcribing recorded audio.", + "label": "Transcribing icon" + } + }, + "widget": { + "backend": "Backend: {value}", + "device": "Device: {value}", + "model": "Model: {value}", + "stream_error": "Could not start the Voxtype status stream", + "unavailable": "Voxtype is not installed or is not available on PATH", + "waiting": "Waiting for Voxtype status…" + } +} diff --git a/voxtype/widget.luau b/voxtype/widget.luau new file mode 100644 index 0000000..825c504 --- /dev/null +++ b/voxtype/widget.luau @@ -0,0 +1,113 @@ +--!nonstrict +-- Voxtype bar widget. +-- +-- `voxtype status --follow --format json` emits one JSON object immediately, +-- then another whenever the dictation state changes. The command's emoji +-- `text` value is intentionally ignored so Noctalia can render a themeable +-- glyph instead. Voxtype's `--extended` output always expands the tooltip with +-- all three fields; the nested settings choose which fields also appear on the +-- bar. + +local SHOW_ALT = noctalia.getConfig("show_alt") == true +local SHOW_EXTENDED = noctalia.getConfig("show_extended") == true +local EXTENDED_FIELDS = { + { name = "model", visible = noctalia.getConfig("show_model") ~= false }, + { name = "device", visible = noctalia.getConfig("show_device") ~= false }, + { name = "backend", visible = noctalia.getConfig("show_backend") ~= false }, +} + +local STATES = { + idle = { + glyph = noctalia.getConfig("idle_glyph") or "microphone-2-off", + color = noctalia.getConfig("idle_color") or "on_surface", + }, + streaming = { + glyph = noctalia.getConfig("streaming_glyph") or "microphone-2", + color = noctalia.getConfig("streaming_color") or "error", + }, + recording = { + glyph = noctalia.getConfig("recording_glyph") or "microphone-2", + color = noctalia.getConfig("recording_color") or "error", + }, + transcribing = { + glyph = noctalia.getConfig("transcribing_glyph") or "loader", + color = noctalia.getConfig("transcribing_color") or "primary", + }, + stopped = { + glyph = noctalia.getConfig("stopped_glyph") or "microphone-2-off", + color = noctalia.getConfig("stopped_color") or "error", + }, +} + +local function render(status) + if type(status) ~= "table" then + return + end + + local state = STATES[status.class] + if state == nil then + noctalia.log("voxtype: ignored unknown status class " .. tostring(status.class)) + return + end + + barWidget.setGlyph(state.glyph) + barWidget.setGlyphColor(state.color) + + local text = {} + if SHOW_ALT and type(status.alt) == "string" then + table.insert(text, status.alt) + end + if SHOW_EXTENDED then + for _, field in ipairs(EXTENDED_FIELDS) do + local value = status[field.name] + if field.visible and type(value) == "string" and value ~= "" then + table.insert(text, noctalia.tr("widget." .. field.name, { value = value })) + end + end + end + barWidget.setText(table.concat(text, " · ")) + + if type(status.tooltip) == "string" then + barWidget.setTooltip(status.tooltip) + else + barWidget.clearTooltip() + end +end + +local function onStatusLine(line) + local ok, status = pcall(noctalia.json.decode, line) + if not ok or type(status) ~= "table" then + noctalia.log("voxtype: ignored invalid status JSON") + return + end + render(status) +end + +-- Use the configured idle appearance while waiting for the command's initial +-- status line. +barWidget.setGlyph(STATES.idle.glyph) +barWidget.setGlyphColor(STATES.idle.color) +barWidget.setText("") +barWidget.setTooltip(noctalia.tr("widget.waiting")) + +if not noctalia.commandExists("voxtype") then + barWidget.setGlyph("microphone-off") + barWidget.setGlyphColor("error") + barWidget.setTooltip(noctalia.tr("widget.unavailable")) +else + local command = "voxtype status --follow --format json" + if SHOW_EXTENDED then + command = command .. " --extended" + end + + -- runStream has no exit callback or restart policy. Keep the retry lifecycle + -- in one managed shell process: only one status command runs at a time, a + -- failure waits before restarting, and the loop ends once its host is gone. + local retryLoop = 'P=$PPID; while kill -0 "$P" 2>/dev/null; do ' + .. command .. "; sleep 2; done" + if not noctalia.runStream(retryLoop, onStatusLine) then + barWidget.setGlyph("microphone-off") + barWidget.setGlyphColor("error") + barWidget.setTooltip(noctalia.tr("widget.stream_error")) + end +end