aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/companion/PhrasesPage.qml
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/modules/companion/PhrasesPage.qml')
-rw-r--r--desktop/modules/companion/PhrasesPage.qml195
1 files changed, 195 insertions, 0 deletions
diff --git a/desktop/modules/companion/PhrasesPage.qml b/desktop/modules/companion/PhrasesPage.qml
new file mode 100644
index 0000000..1971171
--- /dev/null
+++ b/desktop/modules/companion/PhrasesPage.qml
@@ -0,0 +1,195 @@
+// 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 QtQuick
+import "../.."
+
+// One card per recognized program: its window class and the lines she says
+// when focus moves to it. Every change edits the module's draft; the
+// header's Save hands the set to Prefs. Reset fills the draft with the
+// built-in set from Lines.js.
+Column {
+ id: page
+ spacing: 10
+
+ // The module, holding the draft. Not named pm, its id (it would bind to
+ // itself, AGENTS.md).
+ required property QtObject owner
+ // What the cards are keyed by; the defaults are the app set's, so
+ // PhrasesModule passes nothing.
+ property string intro: "When focus moves to a program she recognizes, she says one of its lines at random, a third "
+ + "of the time and under her 30 s line cooldown. The class is Hyprland's window class, "
+ + "lowercased (hyprctl activewindow shows it). Save applies at once."
+ property string keyHint: "window class, e.g. firefox"
+ property string addText: "Add app"
+ // The saved set, for Reset's enabled state; null means the defaults.
+ property var saved: Prefs.companionApps
+ readonly property var list: owner.shown
+ // The app card awaiting a second click on Remove.
+ property int confirming: -1
+
+ // Saves a copy of the list with app i replaced by fn(app), or removed
+ // when fn returns null.
+ function edit(i, fn) {
+ const out = [];
+ for (let j = 0; j < page.list.length; j++) {
+ const a = page.list[j];
+ const c = { cls: a.cls, lines: a.lines.slice() };
+ if (j !== i) { out.push(c); continue; }
+ const r = fn(c);
+ if (r) out.push(r);
+ }
+ page.confirming = -1;
+ page.owner.edit(out);
+ }
+
+ function addApp() { page.owner.edit(page.list.concat([{ cls: "", lines: [""] }])); }
+ function removeApp(i) { page.edit(i, () => null); }
+ function setCls(i, t) { page.edit(i, a => Object.assign(a, { cls: t })); }
+ function addLine(i) { page.edit(i, a => Object.assign(a, { lines: a.lines.concat([""]) })); }
+ function setLine(i, j, t) {
+ page.edit(i, a => {
+ const ls = a.lines.slice();
+ ls[j] = t;
+ return Object.assign(a, { lines: ls });
+ });
+ }
+ function removeLine(i, j) { page.edit(i, a => Object.assign(a, { lines: a.lines.filter((_, k) => k !== j) })); }
+
+ component Small: Text {
+ color: Theme.subtext
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
+ }
+
+ // Reports every keystroke into the draft. Not bound to value, which
+ // would reset the cursor; it follows the value only when the draft moves
+ // under it (Discard, Reset). Empty is outlined, since Save refuses it.
+ component Field: Rectangle {
+ id: field
+ property string value: ""
+ property string placeholder: ""
+ signal edited(string text)
+ height: 32
+ radius: 8
+ color: Qt.alpha(Theme.base, 0.6)
+ border.width: ti.text.trim() === "" ? 1 : 0
+ border.color: Theme.red
+ onValueChanged: if (ti.text !== field.value) ti.text = field.value
+ TextInput {
+ activeFocusOnTab: true
+ id: ti
+ anchors { fill: parent; leftMargin: 10; rightMargin: 10 }
+ verticalAlignment: TextInput.AlignVCenter
+ clip: true
+ selectByMouse: true
+ color: Theme.text
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 }
+ Component.onCompleted: text = field.value
+ onTextEdited: field.edited(text)
+ Text {
+ anchors.verticalCenter: parent.verticalCenter
+ visible: parent.text === ""
+ text: field.placeholder
+ color: Theme.overlay
+ font: parent.font
+ }
+ }
+ }
+
+ Small {
+ width: page.width
+ wrapMode: Text.Wrap
+ text: page.intro
+ }
+
+ Repeater {
+ // A count, not the list: a new list on every keystroke would rebuild
+ // every card and drop the focus of the field being typed in.
+ model: page.list.length
+
+ delegate: Rectangle {
+ id: card
+ required property int index
+ readonly property var modelData: page.list[index] ?? { cls: "", lines: [] }
+ objectName: "app:" + index
+ width: page.width
+ height: body.implicitHeight + 20
+ radius: 10
+ color: Qt.alpha(Theme.surface, 0.45)
+
+ Column {
+ id: body
+ anchors { left: parent.left; right: parent.right; top: parent.top; margins: 10 }
+ spacing: 8
+
+ Item {
+ width: parent.width
+ height: 32
+ Field {
+ anchors { left: parent.left; right: del.left; rightMargin: 8; top: parent.top; bottom: parent.bottom }
+ value: card.modelData.cls
+ placeholder: page.keyHint
+ onEdited: t => page.setCls(card.index, t)
+ }
+ Button {
+ id: del
+ anchors { right: parent.right; verticalCenter: parent.verticalCenter }
+ danger: true
+ text: page.confirming === card.index ? "Confirm" : "Remove"
+ onClicked: {
+ if (page.confirming === card.index) page.removeApp(card.index);
+ else page.confirming = card.index;
+ }
+ }
+ }
+
+ Repeater {
+ model: card.modelData.lines.length
+ delegate: Row {
+ id: phraseRow
+ required property int index
+ width: body.width
+ spacing: 6
+ Field {
+ width: phraseRow.width - removeBtn.width - phraseRow.spacing
+ value: card.modelData.lines[phraseRow.index] ?? ""
+ placeholder: "a line she says"
+ onEdited: t => page.setLine(card.index, phraseRow.index, t)
+ }
+ Button {
+ id: removeBtn
+ // nf-fa-xmark, present in Inconsolata Nerd Font.
+ icon: "\uf00d"
+ danger: true
+ onClicked: page.removeLine(card.index, phraseRow.index)
+ }
+ }
+ }
+
+ Button {
+ text: "Add phrase"
+ onClicked: page.addLine(card.index)
+ }
+ }
+ }
+ }
+
+ Row {
+ spacing: 8
+ Button { text: page.addText; onClicked: page.addApp() }
+ Button {
+ text: "Reset to defaults"
+ danger: true
+ enabled: !page.owner.resetting && (page.saved !== null || page.owner.dirty)
+ onClicked: page.owner.reset()
+ }
+ }
+}