aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-23 20:19:34 +0200
committerDanilo M. <danix@danix.xyz>2026-10-23 20:19:34 +0200
commit0d021266ec91cf5660cfbe2678edbec8a9c62b4e (patch)
tree319d6832758ddcd87f3f91a2a95a0cf59cef8c3e
parentf90381dbd12cdd862a3848874cbbf6d7ef15ae67 (diff)
downloadquickshell-0d021266ec91cf5660cfbe2678edbec8a9c62b4e.tar.gz
quickshell-0d021266ec91cf5660cfbe2678edbec8a9c62b4e.zip
companion: DJ on track change, groove while bored with music
Reads the sound module's Player, which already filters the MPRIS duplicates. The track key is artist+title while playing, so a pause and resume of the same track does not DJ again; a shell start with music on DJs once, which is harmless. Frame loops are a small table keyed by the frame Mood names, so dj and headbang need no special case in Mood. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--desktop/companion/Companion.qml54
1 files changed, 50 insertions, 4 deletions
diff --git a/desktop/companion/Companion.qml b/desktop/companion/Companion.qml
index f8d50e9..aa0505d 100644
--- a/desktop/companion/Companion.qml
+++ b/desktop/companion/Companion.qml
@@ -16,6 +16,7 @@ import Quickshell.Bluetooth
import Quickshell.Hyprland
import QtQuick
import ".."
+import "../modules/sound"
import "../NotifyConfig.js" as NC
import "Mood.js" as Mood
import "Lines.js" as Lines
@@ -107,6 +108,8 @@ PanelWindow {
property double wakeAt: 0
property double admireAt: 0
property double singAt: 0
+ property double djAt: 0
+ property string groove: ""
property double lastLineAt: 0
property double lastMonologueAt: 0
property bool monologue: false
@@ -146,7 +149,9 @@ PanelWindow {
root.wakeAt = t;
root.say("wake");
} else if (root.idle === 1 && root.wasIdle === 0) {
- if (Mood.monologue("bored", root.lastMonologueAt, t, Math.random())) root.startMonologue();
+ root.pickGroove();
+ if (Player.playing) root.say("groove");
+ else if (Mood.monologue("bored", root.lastMonologueAt, t, Math.random())) root.startMonologue();
else root.say("bored");
}
root.wasIdle = root.idle;
@@ -230,6 +235,28 @@ PanelWindow {
root.say("headphones");
}
+ // What is playing, from the sound module's player (it already drops the
+ // playerctld and browser-integration duplicates). A new artist+title
+ // while playing is a track start or change: she DJs for Mood.T.djHold
+ // and says the artist's line if she knows them. Paused reads "", and
+ // resuming the same track matches lastTrack, so it does not DJ again.
+ readonly property string track: Player.playing ? Player.artist + "\n" + Player.title : ""
+ property string lastTrack: ""
+ onTrackChanged: {
+ if (root.track === "" || root.track === root.lastTrack) return;
+ root.lastTrack = root.track;
+ root.djAt = Date.now();
+ root.now = root.djAt;
+ root.say("artist:" + Player.artist);
+ }
+ // Music starting while she is already bored needs a groove too.
+ readonly property bool music: Player.playing
+ onMusicChanged: if (root.music && root.idle === 1) root.pickGroove()
+ function pickGroove() {
+ const g = ["whistle", "headbang", "airguitar"];
+ root.groove = g[Math.floor(Math.random() * g.length)];
+ }
+
// A listed wallpaper on an empty workspace: she turns to it and blushes.
// Checked after the events above (switching to an empty workspace,
// closing its last window) and whenever she stops being away: shown,
@@ -285,7 +312,8 @@ PanelWindow {
shovedAt: root.shovedAt,
recording: root.asst.recording, busy: root.asst.busy, repliedAt: root.repliedAt,
clicks: root.clicks, admireAt: root.admireAt, mockAt: root.mockAt, typingAt: root.typingAt, wakeAt: root.wakeAt,
- singAt: root.singAt, monologue: root.monologue, idle: root.idle,
+ singAt: root.singAt, djAt: root.djAt, music: root.music, groove: root.groove,
+ monologue: root.monologue, idle: root.idle,
})
readonly property string outfit: Mood.outfit(root.destructAt, root.now, Prefs.companionSkirtless * 1000)
// Anything above the monologue cuts it short.
@@ -302,7 +330,7 @@ PanelWindow {
function say(kind, force) {
const t = Date.now();
if (root.hidden || (!force && !Mood.canSpeak(root.lastLineAt, t))) return false;
- const line = Lines.pick(kind, Math.random(), Prefs.companionApps);
+ const line = Lines.pick(kind, Math.random(), Prefs.companionApps, Prefs.companionArtists);
if (line === "") return false;
root.lastLineAt = t;
bubble.show(line, 3000);
@@ -409,6 +437,24 @@ PanelWindow {
onTriggered: root.waveIndex = (root.waveIndex + 1) % root.waveCycle.length
}
+ // Frames Mood names that are really a loop: the frame names its cycle
+ // and how fast it turns. Everything else shows as named.
+ readonly property var cycles: ({
+ dj: { frames: ["dj", "dj-scratch"], ms: 450 },
+ headbang: { frames: ["headbang-up", "headbang-down"], ms: 220 },
+ })
+ property int cycleIndex: 0
+ readonly property var cycle: root.cycles[root.mood.frame] ?? null
+ onCycleChanged: root.cycleIndex = 0
+ Timer {
+ interval: root.cycle?.ms ?? 1000
+ repeat: true
+ running: root.cycle !== null && !root.away
+ onTriggered: root.cycleIndex = (root.cycleIndex + 1) % root.cycle.frames.length
+ }
+ readonly property string frame: root.leaving ? root.waveCycle[root.waveIndex]
+ : root.cycle ? root.cycle.frames[root.cycleIndex % root.cycle.frames.length] : root.mood.frame
+
// qs -p <desktop> ipc call companion toggle -> SUPER+\
IpcHandler {
target: "companion"
@@ -444,7 +490,7 @@ PanelWindow {
id: sprite
x: seat.x
y: seat.y
- source: `file://${root.art}/${root.outfit}/${root.leaving ? root.waveCycle[root.waveIndex] : root.mood.frame}.png`
+ source: `file://${root.art}/${root.outfit}/${root.frame}.png`
fallback: `file://${root.art}/${root.outfit}/base.png`
out: root.away
trembling: root.mood.state === "admire" && root.mood.frame === "blush3"