Skip to content

Commit 3509ed3

Browse files
authored
feat(hints): bring the contour strategy to macOS with a capture scope (#1550)
1 parent 5792e25 commit 3509ed3

53 files changed

Lines changed: 987 additions & 381 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

configs/default-config.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ text = "#E8EEFF"
6464
# See https://github.com/y3owk1n/neru/blob/main/docs/CONFIGURATION.md#hints
6565
[hints]
6666
enabled = true
67-
strategy = "axtree" # Element detection: "axtree" (AX API) or "vision" (Vision Framework)
67+
strategy = "axtree" # Element detection: "axtree" (AX API), "vision" (screen recognition) or "contour" (edge detection)
68+
capture_scope = "window" # Region the vision and contour strategies scan: "window" (focused window) or "screen"
6869
hint_characters = "asdfghjkl" # Characters used for hint labels
6970
label_direction = "normal" # Hint label algorithm: "normal" (default) or "reverse"
7071
max_depth = 50 # Max accessibility tree depth (0 = unlimited)

docs/CLI.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,8 @@ nothing.
302302
| `--hide-on-empty-search` | | none | `hints` | Hide all hints when search query is empty (requires --search) |
303303
| `--role` | | value, repeatable | `hints` | Filter by element role (comma-separated: button,link — the hints.clickable_roles vocabulary, see 'neru roles'). Repeat the flag to add more |
304304
| `--text` | | value, repeatable | `hints` | Filter elements by text content (comma-separated, case-insensitive substring match). Repeat the flag to add more |
305-
| `--strategy` | | value | `hints` | Element detection strategy: axtree (the platform accessibility tree), vision (screen recognition: the Vision framework on macOS, tesseract OCR on Linux), or wl-kbptr (contour detection via embedded C) |
305+
| `--strategy` | | value | `hints` | Element detection strategy: axtree (the platform accessibility tree), vision (screen recognition: the Vision framework on macOS, tesseract OCR on Linux), or contour (edge and contour analysis of the window pixels, ported from wl-kbptr) |
306+
| `--capture-scope` | | value | `hints` | Region the vision and contour strategies scan: window (the focused window) or screen (the whole active screen) |
306307
| `--label-direction` | | value | `hints` | Hint label enumeration: normal (default, prefix-avoidance, prefers shorter labels) or reverse (spreads labels across the alphabet) |
307308
| `--split-word` | | none | `hints` | Split detected text into word-level regions (requires vision strategy) |
308309
| `--zoom-to-depth` | | value | `recursive_grid` | Auto-zoom to the given depth (a non-negative integer) in recursive-grid at the current cursor position |

docs/CONFIGURATION.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -765,7 +765,12 @@ Explicit component colors override theme derivation. Omitted colors inherit from
765765

766766
## [hints]
767767

768-
Labels clickable UI elements with short overlay labels. By default uses the platform accessibility tree (`axtree` strategy). Optionally uses on-screen recognition (`vision` strategy) for apps whose accessibility tree is too thin to hint from — detects elements from a screen capture scoped to the focused window, through the Vision framework on macOS (text plus rectangles) and tesseract OCR on Linux (text only). On Linux, the `wl-kbptr` strategy detects buttons, icons, and text by running contour detection directly on the captured window buffer without external library dependencies.
768+
Labels clickable UI elements with short overlay labels. By default uses the platform accessibility tree (`axtree` strategy). Two screen-capture strategies exist for apps whose accessibility tree is too thin to hint from; both scan the focused window by default (`capture_scope` widens that to the whole screen), both add the system surfaces the `include_*` options ask for from the accessibility tree, and neither is available on Windows, which has no capture backend:
769+
770+
- `vision`: on-screen recognition. The Vision framework on macOS (text plus rectangles), tesseract OCR on Linux (text only). Detected text becomes the element's title, so hint search (`--search`) and `--split-word` work. Costs an ML or OCR pass per activation, and on Linux needs tesseract installed.
771+
- `contour`: edge and contour analysis of the window pixels, an algorithm ported from [wl-kbptr](https://github.com/moverest/wl-kbptr). Finds anything with a visible outline (buttons, icons, toolbar items, text runs) in a few milliseconds with no external dependency. Elements carry no text, so search and word splitting do not apply, and `hints.vision.*` is not read.
772+
773+
Pick `vision` when you want to type what you see, or the app is text-heavy. Pick `contour` when latency matters, the targets are icons rather than words, or OCR is not installed. Both are overridable per-app.
769774

770775
Press `/` to text-search elements. `Space` for multi-word queries. `Return` confirms filtered hints (first is auto-selected). `Escape` cancels search.
771776

@@ -776,7 +781,8 @@ Start with search visible: `neru hints --search` (see [CLI.md](CLI.md#neru-hints
776781
| Option | Type | Default | Description |
777782
| ---------------------------------- | ------------ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
778783
| `enabled` | bool | `true` | Enable/disable hints mode |
779-
| `strategy` | string | `"axtree"` | Element detection strategy: `"axtree"` (the platform accessibility tree), `"vision"` (screen recognition — Vision framework on macOS, tesseract OCR on Linux, unavailable on Windows), or `"wl-kbptr"` (Linux only contour detection replicated from wl-kbptr). Vision and wl-kbptr modes detect the frontmost window content from a screen capture while still using the accessibility tree for system elements (menubar, dock, NC). Overridable per-app via `[hints.app_configs]`. |
784+
| `strategy` | string | `"axtree"` | Element detection strategy: `"axtree"` (the platform accessibility tree), `"vision"` (screen recognition — Vision framework on macOS, tesseract OCR on Linux, unavailable on Windows), or `"contour"` (edge and contour analysis ported from wl-kbptr, macOS and Linux, unavailable on Windows). Both capture strategies detect the focused window content from a screen capture; see the section intro for when to pick which. Overridable per-app via `[hints.app_configs]`. |
785+
| `capture_scope` | string | `"window"` | Region the `vision` and `contour` strategies scan: `"window"` (the focused window, or the whole screen when nothing is focused) or `"screen"` (the whole active screen, so notifications, panels and adjacent tiled windows get hints too, at the cost of a bigger capture). Ignored by `axtree`. Overridable per-app via `[hints.app_configs]` and per-activation via `neru hints --capture-scope`. |
780786
| `hint_characters` | string | `"asdfghjkl"` | Characters used for labels |
781787
| `label_direction` | string | `"normal"` | Hint label algorithm: `"normal"` (default, prefix-avoidance greedy) or `"reverse"` (reverse-order tiers). Empty value defaults to `"normal"`. Overridable per-app via `[hints.app_configs]` and per-activation via the `neru hints --label-direction` CLI flag. See [Choosing a label direction](#choosing-a-label-direction) below. |
782788
| `max_depth` | int | `50` | Max accessibility tree depth (0 = unlimited) |
@@ -1023,7 +1029,8 @@ You can also mix directions per-app via `[hints.app_configs]` or per-activation
10231029
| Field | Type | Description |
10241030
| ---------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
10251031
| `bundle_id` | string | App bundle ID |
1026-
| `strategy` | string | Override element detection strategy for this app (`"axtree"`, `"vision"`, or `"wl-kbptr"`). Empty string = use global `hints.strategy`. |
1032+
| `strategy` | string | Override element detection strategy for this app (`"axtree"`, `"vision"`, or `"contour"`). Empty string = use global `hints.strategy`. |
1033+
| `capture_scope` | string | Override the region the `vision` and `contour` strategies scan for this app (`"window"` or `"screen"`). Empty string = use global `hints.capture_scope`. |
10271034
| `label_direction` | string | Override hint label algorithm for this app (`"normal"` or `"reverse"`). Empty string = use global `hints.label_direction`. See [Choosing a label direction](#choosing-a-label-direction). |
10281035
| `additional_clickable_roles` | array | Extra roles to treat as clickable, same vocabulary as [`clickable_roles`](#clickable-roles) |
10291036
| `ignore_clickable_check` | bool | Skip clickability heuristic for this app |

docs/CROSS_PLATFORM.md

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -346,9 +346,10 @@ places those pixels.
346346
⁷ Linux `vision` is **text-only**, and permanently so. macOS runs three Vision
347347
requests — text recognition, rectangle detection and saliency — and an OCR
348348
engine answers the first. `hints.vision.detect_rectangles` and the four
349-
`rectangle_*` options are therefore declared macOS-only rather than met with a
350-
contour-detection library, which would be a heavy new required dependency for a
351-
sub-feature of a non-default strategy
349+
`rectangle_*` options are therefore declared macOS-only: they tune the Vision
350+
framework's rectangle request, which has no OCR equivalent. The `contour`
351+
strategy is a separate, dependency-free detector, not an implementation of
352+
`detect_rectangles`
352353
([ADR 0013](./adr/0013-parity-is-measured-in-words-not-subsystems.md)). The
353354
other fourteen `hints.vision.*` options are read on Linux exactly as they are on
354355
macOS.
@@ -1058,12 +1059,18 @@ green in every cell while an option means nothing, which is exactly how
10581059
| `recursive_grid.app_configs.strategy = vision` | option |||| the vision strategy needs an element-detection engine, which macOS has in the Vision framework and Linux in tesseract; Windows has neither, so it finds nothing there and none of its settings are read; use axtree |
10591060
| `scroll.app_configs.strategy = vision` | option |||| the vision strategy needs an element-detection engine, which macOS has in the Vision framework and Linux in tesseract; Windows has neither, so it finds nothing there and none of its settings are read; use axtree |
10601061
| `app_configs.strategy = vision` | option |||| the vision strategy needs an element-detection engine, which macOS has in the Vision framework and Linux in tesseract; Windows has neither, so it finds nothing there and none of its settings are read; use axtree |
1061-
| `hints.strategy = wl-kbptr` | option |||| the wl-kbptr strategy detects UI elements via contour analysis of screen captures on Linux |
1062-
| `hints.app_configs.strategy = wl-kbptr` | option |||| the wl-kbptr strategy detects UI elements via contour analysis of screen captures on Linux |
1063-
| `grid.app_configs.strategy = wl-kbptr` | option |||| the wl-kbptr strategy detects UI elements via contour analysis of screen captures on Linux |
1064-
| `recursive_grid.app_configs.strategy = wl-kbptr` | option |||| the wl-kbptr strategy detects UI elements via contour analysis of screen captures on Linux |
1065-
| `scroll.app_configs.strategy = wl-kbptr` | option |||| the wl-kbptr strategy detects UI elements via contour analysis of screen captures on Linux |
1066-
| `app_configs.strategy = wl-kbptr` | option |||| the wl-kbptr strategy detects UI elements via contour analysis of screen captures on Linux |
1062+
| `hints.strategy = contour` | option |||| the contour strategy runs edge detection over a screen capture, which macOS and Linux can take and Windows cannot; there it finds nothing, use axtree |
1063+
| `hints.app_configs.strategy = contour` | option |||| the contour strategy runs edge detection over a screen capture, which macOS and Linux can take and Windows cannot; there it finds nothing, use axtree |
1064+
| `grid.app_configs.strategy = contour` | option |||| the contour strategy runs edge detection over a screen capture, which macOS and Linux can take and Windows cannot; there it finds nothing, use axtree |
1065+
| `recursive_grid.app_configs.strategy = contour` | option |||| the contour strategy runs edge detection over a screen capture, which macOS and Linux can take and Windows cannot; there it finds nothing, use axtree |
1066+
| `scroll.app_configs.strategy = contour` | option |||| the contour strategy runs edge detection over a screen capture, which macOS and Linux can take and Windows cannot; there it finds nothing, use axtree |
1067+
| `app_configs.strategy = contour` | option |||| the contour strategy runs edge detection over a screen capture, which macOS and Linux can take and Windows cannot; there it finds nothing, use axtree |
1068+
| `hints.capture_scope` | option |||| capture_scope only shapes the vision and contour strategies, and Windows has no capture backend for either |
1069+
| `hints.app_configs.capture_scope` | option |||| capture_scope only shapes the vision and contour strategies, and Windows has no capture backend for either |
1070+
| `grid.app_configs.capture_scope` | option |||| capture_scope only shapes the vision and contour strategies, and Windows has no capture backend for either |
1071+
| `recursive_grid.app_configs.capture_scope` | option |||| capture_scope only shapes the vision and contour strategies, and Windows has no capture backend for either |
1072+
| `scroll.app_configs.capture_scope` | option |||| capture_scope only shapes the vision and contour strategies, and Windows has no capture backend for either |
1073+
| `app_configs.capture_scope` | option |||| capture_scope only shapes the vision and contour strategies, and Windows has no capture backend for either |
10671074
| `recursive_grid.animation.enabled` | option |||| the Windows overlay backend has no grid transition animation |
10681075
| `recursive_grid.animation.duration_ms` | option |||| the Windows overlay backend has no grid transition animation |
10691076
| `monitor_select.enabled` | option |||| monitor_select needs the optional MonitorSelector overlay extension, which the Windows backend does not implement |
@@ -1098,7 +1105,8 @@ green in every cell while an option means nothing, which is exactly how
10981105
| `smooth_scroll.duration_per_pixel` | option |||| the Windows scroll is injected in one step; macOS and Linux animate it, and on X11 the steps are whole wheel notches because X has no smaller scroll to send |
10991106
| `--split-word` | mode flag |||| splitting detected text into words needs the vision strategy, which Windows has no engine for; there the flag is refused rather than ignored |
11001107
| `--strategy=vision` | mode flag |||| the vision strategy needs an element-detection engine, which macOS has in the Vision framework and Linux in tesseract; Windows has neither, so detection returns nothing and no hints appear; use axtree |
1101-
| `--strategy=wl-kbptr` | mode flag |||| the wl-kbptr strategy detects UI elements via contour analysis of screen captures on Linux |
1108+
| `--strategy=contour` | mode flag |||| the contour strategy runs edge detection over a screen capture, which macOS and Linux can take and Windows cannot; there it finds nothing, use axtree |
1109+
| `--capture-scope` | mode flag |||| capture_scope only shapes the vision and contour strategies, and Windows has no capture backend for either |
11021110
| `hide_cursor` | action |||| a Wayland client may not hide another client's cursor, and the blessed Linux stack is Wayland; Windows has no equivalent either |
11031111
| `show_cursor` | action |||| a Wayland client may not hide another client's cursor, and the blessed Linux stack is Wayland; Windows has no equivalent either |
11041112
| `scroll_left` | action |||| the Windows wheel event carries no horizontal delta, so a sideways scroll injects nothing |

internal/adapter/platform/darwin/vision.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,10 @@ VisionResult *NeruDetectElements(CGRect detectionRect, NeruVisionConfig config);
3737
// Returns a CGImageRef (caller must CFRelease).
3838
CGImageRef NeruCaptureScreen(void);
3939

40+
// Captures the display containing rect (global top-left-origin points) and
41+
// reports that display's bounds in the same coordinates through outBounds.
42+
// Returns a CGImageRef (caller must CFRelease), or NULL when capture fails.
43+
CGImageRef NeruCaptureDisplayContaining(CGRect rect, CGRect *outBounds);
44+
4045
// Frees a VisionResult previously returned by NeruDetectElements.
4146
void NeruFreeVisionResult(VisionResult *result);

0 commit comments

Comments
 (0)