aboutsummaryrefslogtreecommitdiffstats
path: root/desktop
diff options
context:
space:
mode:
Diffstat (limited to 'desktop')
-rw-r--r--desktop/Module.qml7
-rw-r--r--desktop/Page.qml60
-rw-r--r--desktop/README.md15
-rw-r--r--desktop/Settings.qml43
-rw-r--r--desktop/modules/autostart/AutostartModule.qml21
-rw-r--r--desktop/modules/autostart/AutostartPage.qml7
-rw-r--r--desktop/modules/git/GitModule.qml18
-rw-r--r--desktop/modules/git/GitPage.qml2
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