From 733bd12051ad4a9cf7bae6d503ffe625de03ee93 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Fri, 16 Oct 2026 14:16:53 +0200 Subject: chore(assistant): retire the voice overlay desktop-assistant is a text daemon now (its voice version is the tag voice-v1), and the canvas in desktop/assistant/ is its front end. Co-Authored-By: Claude Opus 5.5 --- assistant/Assistant.qml | 261 ------------------------------------------------ 1 file changed, 261 deletions(-) delete mode 100644 assistant/Assistant.qml (limited to 'assistant/Assistant.qml') 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. -// -// 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 - } - } - } - } - } - } -} -- cgit v1.2.3