diff options
Diffstat (limited to 'desktop')
| -rw-r--r-- | desktop/Module.qml | 7 | ||||
| -rw-r--r-- | desktop/Page.qml | 60 | ||||
| -rw-r--r-- | desktop/README.md | 15 | ||||
| -rw-r--r-- | desktop/Settings.qml | 43 | ||||
| -rw-r--r-- | desktop/modules/autostart/AutostartModule.qml | 21 | ||||
| -rw-r--r-- | desktop/modules/autostart/AutostartPage.qml | 7 | ||||
| -rw-r--r-- | desktop/modules/git/GitModule.qml | 18 | ||||
| -rw-r--r-- | desktop/modules/git/GitPage.qml | 2 |
8 files changed, 149 insertions, 24 deletions
diff --git a/desktop/Module.qml b/desktop/Module.qml index 448fa7b..a1817a4 100644 --- a/desktop/Module.qml +++ b/desktop/Module.qml @@ -46,6 +46,13 @@ QtObject { // settings buried inside the page, which is not loaded until opened. property list<string> keywords: [] + // The rows inside a settings page, for the search: [{text, key}]. key is + // the objectName of the row in the page, which the window scrolls to and + // highlights. Kept by the module because the page is not loaded until + // opened; refresh() rereads it, and the window calls it on open. + property var searchItems: [] + function refresh() {} + // Whether the drawer's grid shows a tile. False for a module reached // some other way: the user page opens from the drawer's header row. property bool tiled: !settings diff --git a/desktop/Page.qml b/desktop/Page.qml index 8365c51..357c3a7 100644 --- a/desktop/Page.qml +++ b/desktop/Page.qml @@ -80,7 +80,47 @@ Item { color: Qt.alpha(Theme.text, 0.12) } + // Scrolls to the content row whose objectName is key and flashes it, for + // a settings search hit. Rows usually arrive after the page loads its + // data, so it looks for up to 3s before giving up. + function reveal(key) { + finder.key = key; + finder.tries = 0; + finder.restart(); + } + + function findNamed(item, key) { + if (item.objectName === key) return item; + for (let i = 0; i < item.children.length; i++) { + const f = page.findNamed(item.children[i], key); + if (f) return f; + } + return null; + } + + Timer { + id: finder + property string key: "" + property int tries: 0 + interval: 100 + repeat: true + onTriggered: { + const it = page.findNamed(holder, key); + if (!it) { if (++tries > 30) stop(); return; } + stop(); + const p = it.mapToItem(holder, 0, 0); + const flick = scroll.contentItem; + flick.contentY = Math.max(0, Math.min(p.y - 40, flick.contentHeight - flick.height)); + flash.x = p.x - 4; + flash.y = p.y - 2; + flash.width = it.width + 8; + flash.height = it.height + 4; + flashAnim.restart(); + } + } + ScrollView { + id: scroll anchors { top: rule.bottom; left: parent.left; right: parent.right; bottom: parent.bottom } anchors.topMargin: 12 clip: true @@ -92,4 +132,24 @@ Item { implicitHeight: childrenRect.height } } + + // The reveal highlight, over the row and in holder's coordinates. + // Declared out here, not in the ScrollView, which sizes from a single + // child; parent puts it in holder all the same. + Rectangle { + id: flash + parent: holder + z: 10 + radius: 8 + opacity: 0 + color: Qt.alpha(Theme.accent, 0.28) + border.width: 1 + border.color: Theme.accent + SequentialAnimation { + id: flashAnim + NumberAnimation { target: flash; property: "opacity"; to: 1; duration: 120 } + PauseAnimation { duration: 1400 } + NumberAnimation { target: flash; property: "opacity"; to: 0; duration: 600 } + } + } } diff --git a/desktop/README.md b/desktop/README.md index ed30620..42c2efb 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -39,11 +39,16 @@ gear tile opens it, and so does IPC: a short About. A page's back arrow and Escape return there; Escape on the landing page closes the window. -The search above the list matches each page's label and its module's -`keywords`, the settings buried inside the page; an entry matched by a -keyword shows which one. Enter opens the first match, Escape clears. Pages -load only when opened, so their live contents (a git alias, an autostart -command) are not searched, only the keywords. +The search above the list matches each page's label, its module's +`keywords`, and the rows inside it: every git value, every autostart line. +A row hit opens its page scrolled to that row, flashed in the accent. Enter +opens the first result, Escape clears. + +Pages load only when opened, so a module keeps its own copy of its rows, +`searchItems` (`[{text, key}]`), and the window calls its `refresh()` on +every open. `key` is the `objectName` of the row in the page; `Page.reveal()` +looks for it, waiting up to 3s for the page's data to arrive, scrolls to it +and flashes it, so a page needs no highlight code of its own. ## The modules diff --git a/desktop/Settings.qml b/desktop/Settings.qml index 52ca549..9882193 100644 --- a/desktop/Settings.qml +++ b/desktop/Settings.qml @@ -32,22 +32,40 @@ Scope { } property string query: "" - // The pages the search keeps, as {mod, hit}: hit is the keyword that - // matched when the label did not, shown under the entry. + // What the search keeps, as {mod, hit, key}. A page matched by its label + // or a keyword has key "" and opens at the top; hit is the keyword when + // the label did not match. Then each row inside a page that matches, from + // the module's searchItems, with key naming that row. + readonly property int perPage: 8 readonly property var found: { const q = root.query.trim().toLowerCase(); const out = []; for (const m of root.pages) { - if (q === "" || m.label.toLowerCase().includes(q) || m.name.includes(q)) { - out.push({ mod: m, hit: "" }); - continue; + if (q === "" || m.label.toLowerCase().includes(q) || m.name.includes(q)) + out.push({ mod: m, hit: "", key: "" }); + else { + const k = Array.from(m.keywords).find(w => w.toLowerCase().includes(q)); + if (k !== undefined) out.push({ mod: m, hit: k, key: "" }); } - const k = Array.from(m.keywords).find(w => w.toLowerCase().includes(q)); - if (k !== undefined) out.push({ mod: m, hit: k }); + if (q === "") continue; + // ponytail: the first perPage rows per page; a narrower query finds the rest. + for (const it of m.searchItems.filter(it => it.text.toLowerCase().includes(q)).slice(0, root.perPage)) + out.push({ mod: m, hit: it.text, key: it.key }); } return out; } + // The page is shown first so the Loader holds it, then asked to reveal. + signal reveal(string key) + function go(entry) { + root.page = entry.mod.name; + if (entry.key) root.reveal(entry.key); + } + + // The search index is the modules' own copy of their rows, reread on + // every open so it matches what the pages will show. + onOpenChanged: if (root.open) for (const m of root.pages) m.refresh() + // Null is the landing page. readonly property QtObject current: root.pages.find(m => m.name === root.page) ?? null @@ -96,7 +114,7 @@ Scope { font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } text: root.query onTextEdited: root.query = text - onAccepted: if (root.found.length) root.page = root.found[0].mod.name + onAccepted: if (root.found.length) root.go(root.found[0]) Keys.onEscapePressed: event => { if (root.query !== "") root.query = ""; else event.accepted = false; @@ -121,7 +139,7 @@ Scope { delegate: Rectangle { id: entry required property var modelData - readonly property bool selected: root.current === modelData.mod + readonly property bool selected: root.current === modelData.mod && modelData.key === "" width: side.width height: modelData.hit ? 46 : 36 radius: 8 @@ -159,7 +177,7 @@ Scope { anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor - onClicked: root.page = entry.modelData.mod.name + onClicked: root.go(entry.modelData) } } } @@ -225,6 +243,11 @@ Scope { // The Page's back arrow steps out to the landing page, // dismiss closes the window, and go() moves to another // settings page by name. + Connections { + target: root + function onReveal(key) { if (pageLoader.item?.reveal) pageLoader.item.reveal(key); } + } + onLoaded: { if (item && item.back) item.back.connect(root.back); if (item && item.dismiss) item.dismiss.connect(root.close); diff --git a/desktop/modules/autostart/AutostartModule.qml b/desktop/modules/autostart/AutostartModule.qml index ef4c005..24c69da 100644 --- a/desktop/modules/autostart/AutostartModule.qml +++ b/desktop/modules/autostart/AutostartModule.qml @@ -9,24 +9,41 @@ // 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 "Autostart.js" as A // What Hyprland launches at login, from hypr-theme's autostart.lua. Set once // and left alone, so it lives in the settings window, not on a tile. Module { + id: am name: "autostart" label: "Autostart" // nf-fa-rocket, present in Inconsolata Nerd Font. - icon: "\uf135" + icon: "" settings: true keywords: ["login", "startup", "launch", "exec", "daemon", "session", "workspace", "silent", "scratchpad", "autostart.lua", "hyprland"] + readonly property string file: (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + + "/hypr/sections/autostart.lua" + + // Every entry, as the page lists it; key is the row's objectName there. + function refresh() { fv.reload(); } + property FileView fv: FileView { + path: am.file + printErrors: false + watchChanges: true + onFileChanged: reload() + onLoaded: am.searchItems = A.parse(text()).map(e => ({ text: e.cmd, key: e.cmd })) + } + page: Component { Page { title: "Autostart" - AutostartPage { width: parent.width } + AutostartPage { width: parent.width; file: am.file } } } } diff --git a/desktop/modules/autostart/AutostartPage.qml b/desktop/modules/autostart/AutostartPage.qml index 1289a93..e0fa2c9 100644 --- a/desktop/modules/autostart/AutostartPage.qml +++ b/desktop/modules/autostart/AutostartPage.qml @@ -29,6 +29,8 @@ Column { spacing: 6 readonly property string home: Quickshell.env("HOME") + // autostart.lua, from the module. + required property string file property var entries: [] // Entry index -> the program's resolved path, "" when it is not found. property var found: ({}) @@ -48,8 +50,7 @@ Column { FileView { id: fv - path: (Quickshell.env("XDG_CONFIG_HOME") || page.home + "/.config") - + "/hypr/sections/autostart.lua" + path: page.file // Hyprland reloads its config on every change, so a half-written file // would reach it. atomicWrites: true @@ -253,6 +254,8 @@ Column { readonly property var parts: A.split(modelData.cmd) readonly property var where: A.workspace(parts.rule) readonly property string path: page.found[modelData.index] ?? "?" + // What a search hit scrolls to (AutostartModule's searchItems key). + objectName: modelData.cmd width: page.width height: 44 diff --git a/desktop/modules/git/GitModule.qml b/desktop/modules/git/GitModule.qml index aaa72a9..6aa0bc6 100644 --- a/desktop/modules/git/GitModule.qml +++ b/desktop/modules/git/GitModule.qml @@ -9,8 +9,10 @@ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. +import Quickshell.Io import QtQuick import "../.." +import "Git.js" as G // The global git config, in the settings window: set once and left alone. // The user page's "More git settings" opens it there. @@ -21,11 +23,17 @@ Module { // nf-dev-git, present in Inconsolata Nerd Font. icon: "" settings: true - keywords: ["gitconfig", "user", "name", "email", "signingkey", "signing", "gpg", - "alias", "core", "editor", "pager", "commit", "gpgsign", "init", - "defaultBranch", "branch", "pull", "push", "fetch", "rebase", "merge", - "diff", "log", "color", "credential", "lfs", "rerere", "tag", "status", - "safe", "directory"] + keywords: ["gitconfig"] + + // Every value, as the page lists it; key is the row's tag in GitPage. + function refresh() { proc.running = false; proc.running = true; } + property Process proc: Process { + command: [Qt.resolvedUrl("gitcfg").toString().replace(/^file:\/\//, ""), "list"] + stdout: StdioCollector { + onStreamFinished: gm.searchItems = text.startsWith("error=") ? [] + : G.parse(text).map(e => ({ text: e.name + " = " + e.value, key: e.name + "=" + e.value })) + } + } page: Component { Page { diff --git a/desktop/modules/git/GitPage.qml b/desktop/modules/git/GitPage.qml index 6edf085..c5d04d9 100644 --- a/desktop/modules/git/GitPage.qml +++ b/desktop/modules/git/GitPage.qml @@ -244,6 +244,8 @@ Column { // An Item with anchors, not a Row: a Row sizes to its children // and fixed widths in one overflow the page (AGENTS.md). Item { + // What a search hit scrolls to (GitModule's searchItems key). + objectName: row.tag width: parent.width height: 34 |
