// 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 QtQuick import "../.." import "Git.js" as G // Every global git value, one row each, grouped by section in file order. // Enter on a row saves it, Escape restores it, Remove drops that one value. // Every write goes through gitcfg and is followed by a fresh list, so a row // always shows what git holds. A failed write shows git's reason under the // row it came from. Column { id: page spacing: 6 readonly property string ctl: Qt.resolvedUrl("gitcfg").toString().replace(/^file:\/\//, "") property var entries: [] // Keyed by tag(): name and value, which survive the reload after a failed // write because the value did not change. property var errors: ({}) property bool busy: false // The module, which outlives the page: a write in flight finishes even if // the page is closed. Not named `gm`, the module's id (it would bind to itself). required property QtObject owner function tag(e) { return e.name + "=" + e.value; } // One Process per call, so reads and writes never share one (a running // Process ignores a second start, AGENTS.md). cb gets stdout. function run(cmd, cb) { op.createObject(page.owner, { command: cmd, cb: cb }).running = true; } Component { id: op Process { id: proc property var cb: null stdout: StdioCollector { onStreamFinished: { try { proc.cb(text); } finally { proc.destroy(); } } } } } function load() { page.run([page.ctl, "list"], out => { page.entries = G.parse(out); page.busy = false; }); } // errKey: where a failure is shown. ok: run on success only. function write(args, errKey, ok) { if (page.busy) return; page.busy = true; page.run([page.ctl].concat(args), out => { const e = {}; if (out.trim() !== "") e[errKey] = out.trim(); else if (ok) ok(); page.errors = e; page.load(); }); } Component.onCompleted: page.load() // Detached, so the editor outlives the drawer. ponytail: ~/.gitconfig is // the one global file here; ~/.config/git/config would be missed. Button { text: "Edit in editor" onClicked: Quickshell.execDetached(["sublime_text", Quickshell.env("HOME") + "/.gitconfig"]) } Repeater { model: page.entries delegate: Column { id: row required property var modelData required property int index readonly property string tag: page.tag(modelData) width: page.width spacing: 4 enabled: !page.busy // A section's heading, above its first row. Text { visible: row.index === 0 || page.entries[row.index - 1].section !== row.modelData.section topPadding: 10 bottomPadding: 2 text: row.modelData.section font { family: Theme.fontFamily; pixelSize: Theme.fontSize; bold: true } color: Theme.text } // An Item with anchors, not a Row: a Row sizes to its children // and fixed widths in one overflow the page (AGENTS.md). Item { width: parent.width height: 34 Text { id: keyLabel anchors { left: parent.left; verticalCenter: parent.verticalCenter } width: parent.width * 0.32 elide: Text.ElideRight text: row.modelData.key font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } color: Theme.subtext } Rectangle { anchors { left: keyLabel.right; leftMargin: 8; right: del.left; rightMargin: 8 } height: parent.height radius: 8 color: Theme.surface // ponytail: one line; a value holding a newline (a long // alias) shows joined and is best edited in the editor. TextInput { id: input anchors { fill: parent; leftMargin: 10; rightMargin: 10 } verticalAlignment: TextInput.AlignVCenter clip: true selectByMouse: true color: Theme.text font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } text: row.modelData.value onAccepted: if (text !== row.modelData.value) page.write(["set", row.modelData.name, row.modelData.value, text], row.tag) // An edit in progress is undone first; an unedited // field lets Escape through, to leave the page. Keys.onEscapePressed: event => { if (text !== row.modelData.value) text = row.modelData.value; else event.accepted = false; } } } Button { id: del anchors { right: parent.right; verticalCenter: parent.verticalCenter } text: "Remove" danger: true onClicked: page.write(["unset", row.modelData.name, row.modelData.value], row.tag) } } Text { width: parent.width visible: !!page.errors[row.tag] text: page.errors[row.tag] ?? "" wrapMode: Text.Wrap color: Theme.red font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } } } } // Add: section.key and a value. The fields clear only when git took it. Text { topPadding: 10 bottomPadding: 2 text: "Add" font { family: Theme.fontFamily; pixelSize: Theme.fontSize; bold: true } color: Theme.text } Item { width: parent.width height: 34 enabled: !page.busy Rectangle { id: newKeyBox anchors { left: parent.left } width: parent.width * 0.32 height: parent.height radius: 8 color: Theme.surface TextInput { id: newKey anchors { fill: parent; leftMargin: 10; rightMargin: 10 } verticalAlignment: TextInput.AlignVCenter clip: true selectByMouse: true color: Theme.text font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } Text { anchors.verticalCenter: parent.verticalCenter visible: parent.text === "" text: "section.key" color: Theme.subtext font: parent.font } } } Rectangle { anchors { left: newKeyBox.right; leftMargin: 8; right: addBtn.left; rightMargin: 8 } height: parent.height radius: 8 color: Theme.surface TextInput { id: newValue anchors { fill: parent; leftMargin: 10; rightMargin: 10 } verticalAlignment: TextInput.AlignVCenter clip: true selectByMouse: true color: Theme.text font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } onAccepted: if (addBtn.enabled) addBtn.clicked() Text { anchors.verticalCenter: parent.verticalCenter visible: parent.text === "" text: "value" color: Theme.subtext font: parent.font } } } Button { id: addBtn anchors { right: parent.right; verticalCenter: parent.verticalCenter } text: "Add" enabled: newKey.text.trim() !== "" onClicked: page.write(["add", newKey.text.trim(), newValue.text], "add", () => { newKey.text = ""; newValue.text = ""; }) } } Text { width: parent.width visible: !!page.errors.add text: page.errors.add ?? "" wrapMode: Text.Wrap color: Theme.red font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } } }