detached a471fa4

docs(config): document the experimental hint mode

commit
a471fa4ea003565d77d111251b8070d97906f3ee
author
brice · · 2026-10-19 16:03 UTC
committer
brice · · 2026-10-19 17:36 UTC
parent
d7c4662
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
checks
1/1
refs
feat/hints
3 files · +58 -0
M README.md +1 -0
@@ -281,6 +281,7 @@ bind.cmd+shift+f = toggle-fullscreen
281281 bind.cmd+s = toggle-stash-visible
282282 bind.cmd+shift+s = toggle-window-stash
283283 bind.cmd+o = toggle-overview
284+# bind.cmd+g = hint
284285 bind.cmd+tab = switch-column-next
285286 bind.cmd+shift+tab = switch-column-prev
286287
@@ -281,6 +281,7 @@ bind.cmd+shift+f = toggle-fullscreen
281 bind.cmd+s = toggle-stash-visible281 bind.cmd+s = toggle-stash-visible
282 bind.cmd+shift+s = toggle-window-stash282 bind.cmd+shift+s = toggle-window-stash
283 bind.cmd+o = toggle-overview283 bind.cmd+o = toggle-overview
284+# bind.cmd+g = hint
284 bind.cmd+tab = switch-column-next285 bind.cmd+tab = switch-column-next
285 bind.cmd+shift+tab = switch-column-prev286 bind.cmd+shift+tab = switch-column-prev
286 287
M docs/config.md +50 -0
@@ -55,6 +55,12 @@ See [examples/config](../examples/config) for every key with its default.
5555 | `focus.flash-opacity` | `0.6` | Starting opacity of the flash, `0.1` to `1` |
5656 | `focus.flash-duration` | `200ms` | Fade duration, `50ms` to `1s`; not scaled by `animations.speed` |
5757 | `focus.flash-color` | `border.active` | Flash color `#rrggbb`; empty uses `border.active` |
58+| `hint.enable` | `off` | Turn on the experimental [hint mode](#hint-mode) (restart required) |
59+| `hint.targets` | `clickable` | What [hint mode](#hint-mode) labels: `clickable` (links, URLs and buttons) or `links` (links and URLs only) |
60+| `hint.text-click` | `ctrl+shift` | Modifiers held for clicks on links found by OCR (terminals open links on a modified click); `none` sends a plain click |
61+| `hint.size` | `auto` | Label text size in logical pixels (6 to 72), or `auto` for screen pixel density |
62+| `hint.background` | empty | Label background (`#rrggbb`); empty uses `border.active` |
63+| `hint.foreground` | empty | Label text (`#rrggbb`); empty chooses a contrasting colour |
5864 | `workspace.<name>.monitor` | focused output | Home monitor of a named workspace |
5965 | `workspace.<name>.size` | `inherit` | [Smaller viewport](#workspace-size), e.g. `1920x1080` |
6066 | `workspace.<name>.max-columns`, `.overflow` | output values | Per-workspace layout |
@@ -191,6 +197,7 @@ bind.XF86MonBrightnessDown = spawn brightnessctl set 5%-
191197 | `toggle-window-stash` | Move the focused window to the [stash](#stash), or back to its column |
192198 | `toggle-stash-visible` | Show or hide the stash |
193199 | `toggle-overview` | Open or close the [overview](#overview) |
200+| `hint` | Label the links and buttons on the focused screen, then click the one you type ([hint mode](#hint-mode)) |
194201 | `switch-column-next` / `-prev` | Switch to the previously used column. Hold `cmd` and press again to go further back |
195202 | `maximize-column` | Toggle full width for the focused column |
196203 | `cycle-column-width` | Grow the focused column by one [cell](#column-widths), then back to equal widths |
@@ -295,3 +302,46 @@ A workspace's stash shows as a pile of cards. Selecting the pile unfolds it into
295302 With `layout.overflow = cascade`, workspaces sit side by side, each band of columns is a lane, and the stash sits under the lanes. Down (or fingers down on the touchpad, with natural scroll off) moves to the lane below; past the last lane it unfolds the stash. Left/right walk the cards; where the carousel does not wrap round, they go to the neighbor workspace past either end. Up leaves the stash for the last lane, at the column you came from.
296303
297304 Otherwise, left from the first column unfolds the stash. Up/down walk the cards, the focus-workspace binds change workspace, and right goes back to the columns.
305+
306+## Hint mode
307+
308+Hint mode is experimental and off by default: set `hint.enable = on` and restart NeferWL.
309+While it is off, nothing of hint mode runs or uses memory, and the `hint` bind
310+logs a notice once and does nothing. A reload does not change it.
311+
312+`hint` labels the links and buttons on the focused output. Type a label to
313+click its target; BackSpace erases a letter, Escape cancels. No bind is set
314+by default; for example `bind.cmd+g = hint`.
315+
316+Labels use the `border.active` colour with a contrasting text colour, and
317+their size follows the screen's pixel density. Each label sits just outside
318+the top-left corner of its target. Links found by OCR (terminal text) are
319+clicked with the `hint.text-click` modifiers held, which is how terminals open
320+a link; accessibility targets get a plain click.
321+
322+### Where targets come from
323+
324+| Source | Apps | Accuracy | Speed |
325+|---|---|---|---|
326+| Accessibility tree (AT-SPI) | Firefox, Chromium, GTK, Qt, Electron | Exact: the app reports its own links and buttons | A few milliseconds |
327+| Text recognition (OCR) | Everything else: terminals, apps without accessibility | Best effort: URLs, domains and underlined text written on screen | 150 ms or more, more on large screens |
328+
329+NeferWL asks the accessibility tree first. Windows it does not cover are read
330+with OCR, which finds visible URLs, domains, email addresses and underlined
331+text. Underlined words get a plain click, and the words of one continuous
332+underline form one target; URLs keep the `hint.text-click` modifiers. OCR
333+cannot see a link whose address is hidden behind plain text, and it can misread
334+small or low-contrast text.
335+
336+### Requirements
337+
338+- Accessibility: NeferWL turns it on at startup. Some apps need a flag:
339+ - Chromium: `--force-renderer-accessibility`
340+ - Electron: `ACCESSIBILITY_ENABLED=1`
341+ - Qt: `QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1`
342+- OCR: `tesseract` and `tesseract-data-eng`. Without tesseract, OCR is off
343+ and hint mode labels only apps that expose the accessibility tree.
344+
345+### Debugging hint mode
346+
347+With `hint.enable = on` and `NEFERWL_HINT_DEBUG=1` set when NeferWL starts (for example `NEFERWL_HINT_DEBUG=1 make tty`, or the variable in the launch environment), every successful scan writes three files to `$XDG_STATE_HOME/neferwl/hints/` (`~/.local/state/neferwl/hints/` when unset): `<time>-<output>.png`, the frame as scanned; `<time>-<output>.annotated.png`, the same with each target outlined (OCR links green, accessibility targets magenta, OCR window crops grey); and `<time>-<output>.json`, the targets (with their `source`, `atspi` or `ocr`) and timings. NeferWL keeps the 50 most recent scans and logs a warning at startup. The files contain the screen in clear: delete them after use.
@@ -55,6 +55,12 @@ See [examples/config](../examples/config) for every key with its default.
55 | `focus.flash-opacity` | `0.6` | Starting opacity of the flash, `0.1` to `1` |55 | `focus.flash-opacity` | `0.6` | Starting opacity of the flash, `0.1` to `1` |
56 | `focus.flash-duration` | `200ms` | Fade duration, `50ms` to `1s`; not scaled by `animations.speed` |56 | `focus.flash-duration` | `200ms` | Fade duration, `50ms` to `1s`; not scaled by `animations.speed` |
57 | `focus.flash-color` | `border.active` | Flash color `#rrggbb`; empty uses `border.active` |57 | `focus.flash-color` | `border.active` | Flash color `#rrggbb`; empty uses `border.active` |
58+| `hint.enable` | `off` | Turn on the experimental [hint mode](#hint-mode) (restart required) |
59+| `hint.targets` | `clickable` | What [hint mode](#hint-mode) labels: `clickable` (links, URLs and buttons) or `links` (links and URLs only) |
60+| `hint.text-click` | `ctrl+shift` | Modifiers held for clicks on links found by OCR (terminals open links on a modified click); `none` sends a plain click |
61+| `hint.size` | `auto` | Label text size in logical pixels (6 to 72), or `auto` for screen pixel density |
62+| `hint.background` | empty | Label background (`#rrggbb`); empty uses `border.active` |
63+| `hint.foreground` | empty | Label text (`#rrggbb`); empty chooses a contrasting colour |
58 | `workspace.<name>.monitor` | focused output | Home monitor of a named workspace |64 | `workspace.<name>.monitor` | focused output | Home monitor of a named workspace |
59 | `workspace.<name>.size` | `inherit` | [Smaller viewport](#workspace-size), e.g. `1920x1080` |65 | `workspace.<name>.size` | `inherit` | [Smaller viewport](#workspace-size), e.g. `1920x1080` |
60 | `workspace.<name>.max-columns`, `.overflow` | output values | Per-workspace layout |66 | `workspace.<name>.max-columns`, `.overflow` | output values | Per-workspace layout |
@@ -191,6 +197,7 @@ bind.XF86MonBrightnessDown = spawn brightnessctl set 5%-
191 | `toggle-window-stash` | Move the focused window to the [stash](#stash), or back to its column |197 | `toggle-window-stash` | Move the focused window to the [stash](#stash), or back to its column |
192 | `toggle-stash-visible` | Show or hide the stash |198 | `toggle-stash-visible` | Show or hide the stash |
193 | `toggle-overview` | Open or close the [overview](#overview) |199 | `toggle-overview` | Open or close the [overview](#overview) |
200+| `hint` | Label the links and buttons on the focused screen, then click the one you type ([hint mode](#hint-mode)) |
194 | `switch-column-next` / `-prev` | Switch to the previously used column. Hold `cmd` and press again to go further back |201 | `switch-column-next` / `-prev` | Switch to the previously used column. Hold `cmd` and press again to go further back |
195 | `maximize-column` | Toggle full width for the focused column |202 | `maximize-column` | Toggle full width for the focused column |
196 | `cycle-column-width` | Grow the focused column by one [cell](#column-widths), then back to equal widths |203 | `cycle-column-width` | Grow the focused column by one [cell](#column-widths), then back to equal widths |
@@ -295,3 +302,46 @@ A workspace's stash shows as a pile of cards. Selecting the pile unfolds it into
295 With `layout.overflow = cascade`, workspaces sit side by side, each band of columns is a lane, and the stash sits under the lanes. Down (or fingers down on the touchpad, with natural scroll off) moves to the lane below; past the last lane it unfolds the stash. Left/right walk the cards; where the carousel does not wrap round, they go to the neighbor workspace past either end. Up leaves the stash for the last lane, at the column you came from.302 With `layout.overflow = cascade`, workspaces sit side by side, each band of columns is a lane, and the stash sits under the lanes. Down (or fingers down on the touchpad, with natural scroll off) moves to the lane below; past the last lane it unfolds the stash. Left/right walk the cards; where the carousel does not wrap round, they go to the neighbor workspace past either end. Up leaves the stash for the last lane, at the column you came from.
296 303
297 Otherwise, left from the first column unfolds the stash. Up/down walk the cards, the focus-workspace binds change workspace, and right goes back to the columns.304 Otherwise, left from the first column unfolds the stash. Up/down walk the cards, the focus-workspace binds change workspace, and right goes back to the columns.
305+
306+## Hint mode
307+
308+Hint mode is experimental and off by default: set `hint.enable = on` and restart NeferWL.
309+While it is off, nothing of hint mode runs or uses memory, and the `hint` bind
310+logs a notice once and does nothing. A reload does not change it.
311+
312+`hint` labels the links and buttons on the focused output. Type a label to
313+click its target; BackSpace erases a letter, Escape cancels. No bind is set
314+by default; for example `bind.cmd+g = hint`.
315+
316+Labels use the `border.active` colour with a contrasting text colour, and
317+their size follows the screen's pixel density. Each label sits just outside
318+the top-left corner of its target. Links found by OCR (terminal text) are
319+clicked with the `hint.text-click` modifiers held, which is how terminals open
320+a link; accessibility targets get a plain click.
321+
322+### Where targets come from
323+
324+| Source | Apps | Accuracy | Speed |
325+|---|---|---|---|
326+| Accessibility tree (AT-SPI) | Firefox, Chromium, GTK, Qt, Electron | Exact: the app reports its own links and buttons | A few milliseconds |
327+| Text recognition (OCR) | Everything else: terminals, apps without accessibility | Best effort: URLs, domains and underlined text written on screen | 150 ms or more, more on large screens |
328+
329+NeferWL asks the accessibility tree first. Windows it does not cover are read
330+with OCR, which finds visible URLs, domains, email addresses and underlined
331+text. Underlined words get a plain click, and the words of one continuous
332+underline form one target; URLs keep the `hint.text-click` modifiers. OCR
333+cannot see a link whose address is hidden behind plain text, and it can misread
334+small or low-contrast text.
335+
336+### Requirements
337+
338+- Accessibility: NeferWL turns it on at startup. Some apps need a flag:
339+ - Chromium: `--force-renderer-accessibility`
340+ - Electron: `ACCESSIBILITY_ENABLED=1`
341+ - Qt: `QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1`
342+- OCR: `tesseract` and `tesseract-data-eng`. Without tesseract, OCR is off
343+ and hint mode labels only apps that expose the accessibility tree.
344+
345+### Debugging hint mode
346+
347+With `hint.enable = on` and `NEFERWL_HINT_DEBUG=1` set when NeferWL starts (for example `NEFERWL_HINT_DEBUG=1 make tty`, or the variable in the launch environment), every successful scan writes three files to `$XDG_STATE_HOME/neferwl/hints/` (`~/.local/state/neferwl/hints/` when unset): `<time>-<output>.png`, the frame as scanned; `<time>-<output>.annotated.png`, the same with each target outlined (OCR links green, accessibility targets magenta, OCR window crops grey); and `<time>-<output>.json`, the targets (with their `source`, `atspi` or `ocr`) and timings. NeferWL keeps the 50 most recent scans and logs a warning at startup. The files contain the screen in clear: delete them after use.
M examples/config +7 -0
@@ -94,6 +94,11 @@ focus.flash-width = 2
9494 focus.flash-opacity = 0.6
9595 focus.flash-duration = 200ms
9696 focus.flash-color =
97+# Hint mode is experimental and off: turn it on here (restart required).
98+# hint.enable = off
99+# Hint mode labels the links, URLs and buttons on the focused screen:
100+# clickable (links, URLs and buttons) or links (links and URLs only).
101+# hint.targets = clickable
97102
98103 # Named workspaces are outside the numbered list and require a bind. The bind
99104 # shows one; pressing it again returns to the previous workspace.
@@ -195,6 +200,8 @@ bind.cmd+shift+f = toggle-fullscreen
195200 bind.cmd+s = toggle-stash-visible
196201 bind.cmd+shift+s = toggle-window-stash
197202 bind.cmd+o = toggle-overview
203+# Label the links and buttons on screen and click one by typing its label.
204+# bind.cmd+g = hint
198205 bind.cmd+q = close-window
199206 bind.ctrl+alt+backspace = quit
200207 bind.ctrl+cmd+space = spawn fuzzel
@@ -94,6 +94,11 @@ focus.flash-width = 2
94 focus.flash-opacity = 0.694 focus.flash-opacity = 0.6
95 focus.flash-duration = 200ms95 focus.flash-duration = 200ms
96 focus.flash-color =96 focus.flash-color =
97+# Hint mode is experimental and off: turn it on here (restart required).
98+# hint.enable = off
99+# Hint mode labels the links, URLs and buttons on the focused screen:
100+# clickable (links, URLs and buttons) or links (links and URLs only).
101+# hint.targets = clickable
97 102
98 # Named workspaces are outside the numbered list and require a bind. The bind103 # Named workspaces are outside the numbered list and require a bind. The bind
99 # shows one; pressing it again returns to the previous workspace.104 # shows one; pressing it again returns to the previous workspace.
@@ -195,6 +200,8 @@ bind.cmd+shift+f = toggle-fullscreen
195 bind.cmd+s = toggle-stash-visible200 bind.cmd+s = toggle-stash-visible
196 bind.cmd+shift+s = toggle-window-stash201 bind.cmd+shift+s = toggle-window-stash
197 bind.cmd+o = toggle-overview202 bind.cmd+o = toggle-overview
203+# Label the links and buttons on screen and click one by typing its label.
204+# bind.cmd+g = hint
198 bind.cmd+q = close-window205 bind.cmd+q = close-window
199 bind.ctrl+alt+backspace = quit206 bind.ctrl+alt+backspace = quit
200 bind.ctrl+cmd+space = spawn fuzzel207 bind.ctrl+cmd+space = spawn fuzzel

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close