diff options
Diffstat (limited to 'assistant')
| -rw-r--r-- | assistant/Assistant.qml | 261 | ||||
| -rw-r--r-- | assistant/Place.js | 23 | ||||
| -rw-r--r-- | assistant/README.md | 17 | ||||
| l--------- | assistant/Theme.qml | 1 | ||||
| -rw-r--r-- | assistant/shell.qml | 16 | ||||
| -rw-r--r-- | assistant/test_place.js | 32 |
6 files changed, 0 insertions, 350 deletions
diff --git a/assistant/Assistant.qml b/assistant/Assistant.qml deleted file mode 100644 index 6d1eeb2..0000000 --- a/assistant/Assistant.qml +++ /dev/null @@ -1,261 +0,0 @@ -// Copyright (C) 2026 Danilo M. <danix@danix.xyz> -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License version 2 as -// published by the Free Software Foundation. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -import Quickshell -import Quickshell.Io -import Quickshell.Wayland -import Quickshell.Hyprland -import QtQuick -import "Place.js" as Place - -// The voice assistant's overlay. desktop-assistant broadcasts its state, -// context use and audio levels as JSON lines on a Unix socket; this only -// listens, and keeps reconnecting while the assistant is down. -Scope { - id: root - - // Replaced by the assistant's hello; these render only before it. - property string name: "Assistant" - property string position: "center" - property real hideDelay: 3 - property string userColor: "accent" - property string assistantColor: "green" - - property int ctxUsed: 0 - property int ctxMax: 0 - property string state: "idle" - property string tool: "" - property real level: 0 // latest loudness, 0..1 - property string who: "user" // whose voice that was - property real heardAt: 0 // when it arrived, ms - property bool shown: false - property var shownScreen: Quickshell.screens[0] - - // Faded, not switched: the window stays loaded until the fade has run. - property real fade: shown ? 1 : 0 - Behavior on fade { NumberAnimation { duration: 200 } } - - // Colour roles are palette names from udt, so a theme change recolours - // the waves live. An unknown name falls back to the accent. - function colorOf(role) { return Theme[role] ?? Theme.accent; } - - function handle(line) { - let e; - try { e = JSON.parse(line); } catch (err) { return; } - if (e.type === "hello") { - root.name = e.name; - root.position = e.position; - root.hideDelay = e.hide_delay; - root.userColor = e.user_color; - root.assistantColor = e.assistant_color; - root.ctxMax = e.ctx_max; - root.setState(e.state, e.tool ?? ""); - } else if (e.type === "state") { - root.setState(e.state, e.tool ?? ""); - } else if (e.type === "ctx") { - root.ctxUsed = e.used; - root.ctxMax = e.max; - } else if (e.type === "level") { - root.level = e.value; - root.who = e.who; - root.heardAt = Date.now(); - } - } - - function setState(s, t) { - root.state = s; - root.tool = t; - if (s === "listening" && !root.shown) { - // The monitor in focus when it appears, not wherever it last was. - const focused = Hyprland.focusedMonitor?.name; - root.shownScreen = Quickshell.screens.find(x => x.name === focused) ?? Quickshell.screens[0]; - root.level = 0; - root.shown = true; - } - if (s === "idle") hideTimer.restart(); else hideTimer.stop(); - } - - Timer { - id: hideTimer - interval: root.hideDelay * 1000 - onTriggered: root.shown = false - } - - // The assistant may start after this shell, or restart: keep trying with - // a fresh Socket each time. Quickshell 0.3.1 keeps the QLocalSocket of a - // failed connect, so setting `connected` again on the same Socket never - // retries: an overlay started before the assistant stayed dark for good. - LazyLoader { - id: link - active: true - Socket { - path: `${Quickshell.env("XDG_RUNTIME_DIR") || "/tmp"}/desktop-assistant.sock` - connected: true - onConnectionStateChanged: if (!connected) root.shown = false - parser: SplitParser { onRead: line => root.handle(line) } - } - } - - Timer { - interval: 2000 - repeat: true - running: !(link.item?.connected ?? false) - onTriggered: { link.active = false; link.active = true; } - } - - // A config with no visible window exits, reporting nothing. This 1x1 - // click-through window holds the shell open while the overlay is hidden. - PanelWindow { - anchors { top: true; left: true } - implicitWidth: 1 - implicitHeight: 1 - color: "transparent" - exclusionMode: ExclusionMode.Ignore - mask: Region {} - WlrLayershell.keyboardFocus: WlrKeyboardFocus.None - } - - LazyLoader { - active: root.fade > 0 - - PanelWindow { - screen: root.shownScreen - anchors { top: true; bottom: true; left: true; right: true } - color: "transparent" - exclusionMode: ExclusionMode.Ignore - mask: Region {} - WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.namespace: "quickshell-assistant" - WlrLayershell.keyboardFocus: WlrKeyboardFocus.None - - Item { - id: orb - // Every size below scales from this one value. - readonly property real zoom: 1.3 - readonly property int size: 360 * zoom - readonly property var spot: Place.at(root.position, parent.width, parent.height, size, 24) - x: spot.x - y: spot.y - width: size - height: size - opacity: root.fade - - // The waves: three sine ripples round the circle, drifting at - // different speeds, their height following the loudness and - // their colour whoever is talking. Silence settles them into a - // thin flat ring. - Canvas { - id: waves - anchors.fill: parent - renderTarget: Canvas.FramebufferObject - property real amp: 0 - property real phase: 0 - - // One repaint per display frame (vsync, not a timer: a - // 16 ms Timer drifted against the refresh and stuttered). - // Motion scales by the frame's real duration, so a late - // frame does not jump. The level stream only sets the - // height; a level older than 150 ms means the audio - // stopped, so the height eases back to zero. - FrameAnimation { - running: root.fade > 0 - onTriggered: { - const target = Date.now() - root.heardAt < 150 ? root.level : 0; - waves.amp += (target - waves.amp) * (1 - Math.exp(-frameTime * 15)); - waves.phase += frameTime * 3; - waves.requestPaint(); - } - } - - onPaint: { - const ctx = getContext("2d"); - ctx.reset(); - const c = width / 2, r0 = 86 * orb.zoom, reach = 70 * orb.zoom, n = 128; - ctx.strokeStyle = `${root.colorOf(root.who === "user" ? root.userColor : root.assistantColor)}`; - ctx.lineWidth = 2.5 * orb.zoom; - [[3, 1.3, 0.9], [5, -0.9, 0.55], [7, 2.1, 0.35]].forEach(([k, speed, alpha]) => { - ctx.globalAlpha = alpha; - ctx.beginPath(); - for (let i = 0; i <= n; i++) { - const a = i / n * 2 * Math.PI; - const r = r0 + 4 + amp * reach * (0.55 + 0.45 * Math.sin(k * a + phase * speed)); - const x = c + r * Math.cos(a), y = c + r * Math.sin(a); - if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); - } - ctx.closePath(); - ctx.stroke(); - }); - } - } - - Rectangle { - anchors.centerIn: parent - width: 160 * orb.zoom - height: width - radius: width / 2 - color: Qt.rgba(Theme.base.r, Theme.base.g, Theme.base.b, 0.8) - border.color: root.colorOf(root.assistantColor) - border.width: 2 - - Column { - anchors.centerIn: parent - spacing: 4 - - Text { - anchors.horizontalCenter: parent.horizontalCenter - text: root.name - color: Theme.text - font.family: Theme.fontFamily - font.pixelSize: (Theme.fontSize + 2) * orb.zoom - font.bold: true - } - // Working without sound (transcribing, thinking): - // three dots pulsing in turn, on the waves' clock. - Row { - anchors.horizontalCenter: parent.horizontalCenter - visible: root.state === "thinking" || root.state === "transcribing" - spacing: 6 * orb.zoom - Repeater { - model: 3 - Rectangle { - required property int index - width: 7 * orb.zoom - height: width - radius: width / 2 - color: root.colorOf(root.assistantColor) - opacity: 0.25 + 0.75 * (0.5 + 0.5 * Math.sin(waves.phase * 2 - index * 0.9)) - } - } - } - // nf-fa-wrench, present in Inconsolata Nerd Font. - Text { - anchors.horizontalCenter: parent.horizontalCenter - visible: root.state === "tool" - text: "" - color: Theme.peach - font.family: Theme.iconFamily - font.pixelSize: 22 * orb.zoom - } - Text { - anchors.horizontalCenter: parent.horizontalCenter - text: root.state === "tool" ? root.tool - : root.ctxMax > 0 ? `${root.ctxUsed} / ${root.ctxMax}` - : `${root.ctxUsed}` - color: Theme.subtext - font.family: Theme.fontFamily - font.pixelSize: (Theme.fontSize - 3) * orb.zoom - } - } - } - } - } - } -} diff --git a/assistant/Place.js b/assistant/Place.js deleted file mode 100644 index f6d895c..0000000 --- a/assistant/Place.js +++ /dev/null @@ -1,23 +0,0 @@ -.pragma library -// Copyright (C) 2026 Danilo M. <danix@danix.xyz> -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License version 2 as -// published by the Free Software Foundation. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// Top-left corner of a size x size square on a W x H screen, margin away -// from the edges its position names. Anything unknown is the centre. -function at(position, W, H, size, margin) { - const x = position.endsWith("left") ? margin - : position.endsWith("right") ? W - size - margin - : (W - size) / 2; - const y = position.startsWith("top") ? margin - : position.startsWith("bottom") ? H - size - margin - : (H - size) / 2; - return { x, y }; -} diff --git a/assistant/README.md b/assistant/README.md deleted file mode 100644 index 9c989cf..0000000 --- a/assistant/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# assistant - -Overlay for desktop-assistant, the local voice assistant. It appears when the -mic button is pressed and shows the assistant's name, its model context use, -the tool running, and a ring of sound waves that follows the real audio: the -user's voice in one palette colour, the reply in another. It fades out a few -seconds after the reply. - -The assistant owns a Unix socket, `$XDG_RUNTIME_DIR/desktop-assistant.sock`, -and writes one JSON event per line (`hello`, `state`, `ctx`, `level`); this -component only reads it and reconnects every 2 s while the assistant is down. -Name, position, fade delay and the two colour roles come from the -assistant's `config.toml` `[overlay]` table, in the `hello` event. The -protocol is specified in the desktop-assistant repo, -`docs/superpowers/specs/2026-09-26-overlay-design.md`. - -Run: `qs -p <this dir>`. Test the position maths: `node assistant/test_place.js`. diff --git a/assistant/Theme.qml b/assistant/Theme.qml deleted file mode 120000 index 3d2e40f..0000000 --- a/assistant/Theme.qml +++ /dev/null @@ -1 +0,0 @@ -../shared/Theme.qml
\ No newline at end of file diff --git a/assistant/shell.qml b/assistant/shell.qml deleted file mode 100644 index 06e247a..0000000 --- a/assistant/shell.qml +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (C) 2026 Danilo M. <danix@danix.xyz> -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License version 2 as -// published by the Free Software Foundation. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -import Quickshell - -ShellRoot { - Assistant {} -} diff --git a/assistant/test_place.js b/assistant/test_place.js deleted file mode 100644 index 9f1376e..0000000 --- a/assistant/test_place.js +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (C) 2026 Danilo M. <danix@danix.xyz> -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License version 2 as -// published by the Free Software Foundation. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// Run: node assistant/test_place.js -// Place.js is a QML library, so its .pragma line is stripped before node -// evaluates it. - -const fs = require("fs"); -const assert = require("assert"); - -const src = fs.readFileSync(__dirname + "/Place.js", "utf8") - .replace(/^\.pragma library$/m, ""); -const P = new Function(src + "\nreturn { at };")(); - -assert.deepStrictEqual(P.at("center", 1920, 1080, 360, 24), { x: 780, y: 360 }); -assert.deepStrictEqual(P.at("top", 1920, 1080, 360, 24), { x: 780, y: 24 }); -assert.deepStrictEqual(P.at("bottom", 1920, 1080, 360, 24), { x: 780, y: 696 }); -assert.deepStrictEqual(P.at("top-left", 1920, 1080, 360, 24), { x: 24, y: 24 }); -assert.deepStrictEqual(P.at("top-right", 1920, 1080, 360, 24), { x: 1536, y: 24 }); -assert.deepStrictEqual(P.at("bottom-left", 1920, 1080, 360, 24), { x: 24, y: 696 }); -assert.deepStrictEqual(P.at("bottom-right", 1920, 1080, 360, 24), { x: 1536, y: 696 }); -// An unknown position is the centre, not an error. -assert.deepStrictEqual(P.at("middle", 1920, 1080, 360, 24), { x: 780, y: 360 }); -console.log("ok"); |
