aboutsummaryrefslogtreecommitdiffstats
path: root/assistant/Assistant.qml
diff options
context:
space:
mode:
Diffstat (limited to 'assistant/Assistant.qml')
-rw-r--r--assistant/Assistant.qml261
1 files changed, 0 insertions, 261 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
- }
- }
- }
- }
- }
- }
-}