From 2459c6638985e036932916b28ff0b3b547c23a09 Mon Sep 17 00:00:00 2001 From: san Date: Fri, 30 Oct 2026 19:07:09 +0100 Subject: [PATCH] [ryoku] hub search: keyboard navigation and Ctrl+F focus The rail search had no keyboard navigation over its results and answered only to Ctrl+K. Up/Down now walk the hits with wrap-around, Enter opens the selected one instead of always the top hit, the header shows N / TOTAL, and Ctrl+F is accepted alongside Ctrl+K as the focus shortcut. The settings search answered only to Ctrl+K and had no way to move between results by keyboard: Enter always opened the top hit, and only the pointer could select a row. - A `searchCursor` property now tracks which result is active under the keyboard. It resets to 0 on every edit and on dismissal, so refining a query or reopening the field never leaves a stale row selected. - `Keys.onUpPressed` / `Keys.onDownPressed` on the search field walk the hits with wrap-around and scroll the list so the active row stays in view (`searchList.positionViewAtIndex(hub.searchCursor, ListView.Contain)`). - `onAccepted` opens `hub.searchCursor` instead of always index 0. - The delegate highlights `rr.index === hub.searchCursor`, and `MouseArea.onEntered` keeps the pointer and the keyboard cursor on the same row, so a click after a key press looks like a continuation rather than a jump. - The header now shows `(cursor + 1) / TOTAL` next to the hit count. - `Keys.onPressed` accepts `Ctrl+F` as well as `Ctrl+K`, the shortcut a user instinctively reaches for in a settings app. ryoku - Read the existing handler at `Hub.ml` 1079-1084 and the overlay at 1752-1802 before editing; confirmed `Ctrl+F` was absent and that `onAccepted` hardcoded index 0. - Verified every edit against the file after writing: the cursor property, the field handlers, the renamed `searchList` id, the delegate highlight, the `onEntered` sync, the header counter, the `Ctrl+F` key test, and the two reset sites all land where intended. - `grep` confirms no em-dash in the three changed files. - `.githooks/pre-commit` passes (rc=0). `qmllint` is not installed in this environment, so the QML was validated by inspection instead; run `qmllint ryoku/hub/quickshell/Hub.ml` on a dev box before merging. - [x] One logical change, with a clear `[area] scope: summary` commit subject. - [x] Matching `CHANGELOG.md` updated in the area I touched. - [x] The git hooks pass locally; I did not use `--no-verify`. - [x] QML parses where applicable. (`qmllint` is not installed here; verified by inspection and the pre-commit hook.) - [x] No duplicated config, no dead code, no commented-out code, no em-dash. - [x] Docs updated: `ryoku/hub/README.md` mentions `Ctrl+F` alongside `Ctrl+K`. Note: New: Up/Down navigate the settings search results, Enter opens the selected hit, and Ctrl+F focuses the search bar. --- ryoku/hub/CHANGELOG.md | 12 ++++++++++ ryoku/hub/README.md | 3 ++- ryoku/hub/quickshell/Hub.qml | 43 +++++++++++++++++++++++++++++------- 3 files changed, 49 insertions(+), 9 deletions(-) diff --git a/ryoku/hub/CHANGELOG.md b/ryoku/hub/CHANGELOG.md index deda5235a..77b486059 100644 --- a/ryoku/hub/CHANGELOG.md +++ b/ryoku/hub/CHANGELOG.md @@ -52,6 +52,18 @@ upgrade, kernel included, rides along with the Ryoku packages (`backend/config.go`, `quickshell/pages/UpdatesPage.qml`, `quickshell/schema/UpdatesPage.js`). +- **The settings search is navigable by keyboard.** Typing in the rail search + now keeps a cursor over its results: Up and Down walk the hits (wrapping at + the ends), Enter opens the selected one instead of always the top hit, and + the header shows `N / TOTAL` so a long list is legible. The cursor resets on + every edit and on dismissal, so refining a query or reopening the field + never leaves a stale row selected. Moving the pointer over a row also moves + the cursor, and the keyboard and pointer highlights share one state, so a + click after a key press looks like a continuation rather than a jump + (`quickshell/Hub.ml`). +- **`Ctrl + F` also opens the settings search.** The rail finder answered only + to `Ctrl + K`; it now accepts `Ctrl + F` as well, the shortcut a user + instinctively reaches for in a settings app (`quickshell/Hub.ml`). - **General manages people on this computer.** The Users tab lists local accounts, sign-in state, roles, permissions and home storage, and lets an administrator add or remove people, reset passwords and set storage limits diff --git a/ryoku/hub/README.md b/ryoku/hub/README.md index 0f9fc0591..98f669376 100644 --- a/ryoku/hub/README.md +++ b/ryoku/hub/README.md @@ -144,7 +144,8 @@ edits stage in the canvas and only touch the live screens on Apply. ## Search -A global fuzzy finder lives in the sidebar (focus it with `Ctrl + K`). Typing +A global fuzzy finder lives in the sidebar (focus it with `Ctrl + K`, or the +more familiar `Ctrl + F`). Typing searches content across every section: it ranks matching keybinds (each tagged with its category) and lists matching section names you can jump to. The matcher is a small subsequence scorer in `quickshell/fuzzy.js`. diff --git a/ryoku/hub/quickshell/Hub.qml b/ryoku/hub/quickshell/Hub.qml index 80ad6eff5..ea1609239 100644 --- a/ryoku/hub/quickshell/Hub.qml +++ b/ryoku/hub/quickshell/Hub.qml @@ -1080,11 +1080,11 @@ Rectangle { Keys.onEscapePressed: { if (diffPop.open) diffPop.open = false; - else if (hub.query !== "") { hub.query = ""; searchField.clear(); } + else if (hub.query !== "") { hub.query = ""; searchField.clear(); hub.searchCursor = 0; } else hub.requestQuit(); } Keys.onPressed: (e) => { - if (e.key === Qt.Key_K && (e.modifiers & Qt.ControlModifier)) { + if ((e.key === Qt.Key_K || e.key === Qt.Key_F) && (e.modifiers & Qt.ControlModifier)) { searchField.grabFocus(); e.accepted = true; } @@ -1139,8 +1139,20 @@ Rectangle { width: parent.width toolbar: true placeholder: I18n.tr("Search settings…") - onEdited: (t) => hub.query = t - onAccepted: hub.activateSearch(0) + onEdited: (t) => { hub.query = t; hub.searchCursor = 0 } + onAccepted: hub.activateSearch(hub.searchCursor) + Keys.onUpPressed: (e) => { + if (hub.searchResults.length === 0) return; + hub.searchCursor = (hub.searchCursor - 1 + hub.searchResults.length) % hub.searchResults.length; + searchList.positionViewAtIndex(hub.searchCursor, ListView.Contain); + e.accepted = true; + } + Keys.onDownPressed: (e) => { + if (hub.searchResults.length === 0) return; + hub.searchCursor = (hub.searchCursor + 1) % hub.searchResults.length; + searchList.positionViewAtIndex(hub.searchCursor, ListView.Contain); + e.accepted = true; + } } } @@ -1688,8 +1700,12 @@ Rectangle { // The rail search opens a results card under the field (not a cramped rail // list). Picking a result navigates to its page and, for a real setting, // scrolls it into view and flashes it -- the jump-to-setting the old search - // never did. Enter takes the top hit; Escape (the field clears) dismisses. + // never did. Enter takes the selected hit; Escape (the field clears) + // dismisses; Up/Down walk the hits while the field keeps focus. property string pendingFocusKey: "" + // Which hit the keyboard is on. Resets to 0 on every edit and on dismissal, + // so a refined query or a reopened search never selects a stale row. + property int searchCursor: 0 function activateSearch(i) { var r = hub.searchResults[i]; if (!r) return; @@ -1700,6 +1716,7 @@ Rectangle { hub.pendingFocusKey = (r.isPage || !r.key) ? "" : r.key; hub.query = ""; searchField.clear(); + hub.searchCursor = 0; if (hub.pendingFocusKey !== "") { focusTimer.tries = 0; focusTimer.restart(); } } // bold the matched run in a result label (StyledText). @@ -1749,7 +1766,12 @@ Rectangle { font.family: Tokens.mono; font.pixelSize: 9; font.letterSpacing: 1.2 anchors.verticalCenter: parent.verticalCenter } - Item { width: Math.max(0, parent.width - 170); height: 1 } + Item { width: Math.max(0, parent.width - 210); height: 1 } + Text { + text: (hub.searchCursor + 1) + " / " + hub.searchResults.length + color: Tokens.ink; font.family: Tokens.mono; font.pixelSize: 9; font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } Text { text: hub.searchResults.length + (hub.searchResults.length === 1 ? I18n.tr(" HIT") : I18n.tr(" HITS")) color: Tokens.inkFaint; font.family: Tokens.mono; font.pixelSize: 9 @@ -1757,7 +1779,7 @@ Rectangle { } } ListView { - id: list + id: searchList anchors { left: parent.left; right: parent.right; top: head.bottom; bottom: parent.bottom } anchors.margins: Tokens.s2; anchors.topMargin: Tokens.s1 clip: true @@ -1772,7 +1794,11 @@ Rectangle { width: ListView.view.width height: 46 radius: Tokens.radius - color: rrh.containsMouse ? Tokens.tint10 : "transparent" + // the keyboard cursor and the pointer hover share one highlight: + // a result is active when either of them is on it, and the + // behavior eases between the two so a click after a key press + // looks like a continuation, not a jump. + color: (rrh.containsMouse || rr.index === hub.searchCursor) ? Tokens.tint10 : "transparent" Behavior on color { ColorAnimation { duration: Tokens.snap } } Column { anchors { left: parent.left; right: kseal.left; verticalCenter: parent.verticalCenter } @@ -1803,6 +1829,7 @@ Rectangle { anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor + onEntered: hub.searchCursor = rr.index onClicked: hub.activateSearch(rr.index) } }