// 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 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() } } }