Skip to content

Commit 441192b

Browse files
authored
feat: grid based navigation mode (#111)
1 parent 472a390 commit 441192b

21 files changed

Lines changed: 2077 additions & 52 deletions

README.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ This is an intentional design choice to keep the project lean, maintainable, and
6666

6767
- 🎯 **Hint Mode** - Click any UI element using keyboard shortcuts with visual labels
6868
- 🎬 **Action Mode** - Choose specific click actions (left, right, double, middle click)
69+
- 🗺️ **Grid Mode** - Full-screen grid overlay for precise clicking anywhere on screen
70+
- Multi-monitor support - automatically adapts to active screen
71+
- Smart cell sizing - maintains optimal precision on any display size
72+
- Configurable sublayer for pinpoint accuracy
6973
- 📜 **Scroll Mode** - Vim-style scrolling (`j`/`k`, `gg`/`G`, etc.) in any application
7074
- 🌐 **Universal Support** - Works with native macOS apps, Electron apps, and Chrome/Firefox
7175
-**Performance** - Built with native macOS APIs for instant response
@@ -83,6 +87,7 @@ This is an intentional design choice to keep the project lean, maintainable, and
8387
- Maybe make it a full-fledged vim mode? Just like [Vimnav.spoon](https://github.com/y3owk1n/vimnav.spoon)
8488
- Hold and unhold action does not work in Finder.app... sobs
8589
- Implement a text selection mode that focuses on AXStaticText elements
90+
- Grid mode: hide unmatched cells during typing to reduce visual clutter
8691

8792
## 🚀 Installation
8893

@@ -260,6 +265,7 @@ open -a Neru # if you prefer app bundle
260265
neru launch # if you prefer cli only
261266

262267
# Try hint mode: Press Cmd+Shift+Space
268+
# Try grid mode: Press Cmd+Shift+G (configurable)
263269
# Try scroll mode: Press Cmd+Shift+J
264270
```
265271

@@ -308,6 +314,45 @@ Keys:
308314

309315
Press `esc` anytime to quit the hint selection.
310316

317+
### Grid Mode
318+
319+
**Default hotkey:** `Cmd+Shift+G` (or bind to your preference)
320+
321+
Grid mode divides your screen into a uniform grid of clickable cells, allowing you to click anywhere with precision.
322+
323+
**Features:**
324+
- 🖥️ **Multi-monitor support** - Automatically adapts to the screen containing your mouse
325+
- 📏 **Smart sizing** - Cell sizes automatically adjust for optimal precision (40-200px default)
326+
- 🎯 **Sublayer precision** - After selecting a cell, choose from a 3×3 subgrid for pinpoint accuracy
327+
- 🔤 **Adaptive labels** - Uses 2, 3, or 4 character labels based on screen size
328+
-**Live matching** - Real-time visual feedback as you type without screen flashing
329+
330+
**How to use:**
331+
332+
1. Press the hotkey to activate grid mode
333+
2. Type the label of the cell you want to click (e.g., "AS", "ASD", or "ASDF" depending on grid size)
334+
3. A 3×3 subgrid appears in the selected cell for precision
335+
4. Type one more character to select the exact position within that cell
336+
5. The action (click, scroll, etc.) is performed at the selected point
337+
338+
**Supported actions:**
339+
- Left click, right click, double click, triple click
340+
- Mouse down/up, middle click
341+
- Move mouse (no click)
342+
- Scroll mode (with vim-style keys)
343+
- Context menu
344+
345+
**Configuration:**
346+
```toml
347+
[grid]
348+
characters = "asdfghjkl" # Characters for grid labels
349+
min_cell_size = 40 # Minimum cell size (prevents cells too small)
350+
max_cell_size = 200 # Maximum cell size (prevents oversized cells on large monitors)
351+
sublayer_keys = "asdfghjkl" # Keys for 3×3 subgrid (at least 9 chars)
352+
```
353+
354+
Press `esc` or `delete` anytime to exit grid mode or correct your input.
355+
311356
### Scroll Mode
312357

313358
**Default hotkey:** `Cmd+Shift+J`
@@ -371,6 +416,25 @@ See [`configs/default-config.toml`](configs/default-config.toml) for all availab
371416

372417
### Common Configurations
373418

419+
#### Enable or disable modes
420+
421+
You can disable a mode via configuration. When disabled:
422+
- Hotkeys for that mode are not registered
423+
- Menubar submenu for that mode is hidden
424+
- Overlay rendering and input handling are bypassed
425+
- Actions for that mode short-circuit with no effect
426+
427+
Changes take effect on app restart.
428+
429+
```toml
430+
[hints]
431+
enabled = true
432+
433+
[grid]
434+
enabled = true
435+
```
436+
437+
374438
#### Change the default keybindings
375439

376440
```toml
@@ -379,6 +443,9 @@ See [`configs/default-config.toml`](configs/default-config.toml) for all availab
379443
"Ctrl+F" = "hints left_click"
380444
"Ctrl+G" = "hints context_menu"
381445
"Ctrl+S" = "hints scroll"
446+
447+
# Grid mode
448+
"Cmd+Shift+G" = "grid left_click"
382449
```
383450

384451
#### Add more bindings
@@ -667,6 +734,45 @@ highlight_color = "#FF0000"
667734
highlight_width = 2
668735
```
669736

737+
### Grid configuration
738+
739+
```toml
740+
[grid]
741+
# Characters used to build grid labels
742+
characters = "asdfghjkl"
743+
744+
# Cell size constraints (in pixels)
745+
min_cell_size = 40 # Minimum cell size for comfortable clicking
746+
max_cell_size = 200 # Maximum cell size to maintain precision on large/ultra-wide monitors
747+
748+
# Visual styling
749+
font_size = 12
750+
font_family = "SF Mono" # Leave empty for system default
751+
opacity = 0.85
752+
753+
# Colors (hex format)
754+
background_color = "#abe9b3"
755+
text_color = "#ffffff"
756+
matched_text_color = "#ffffff" # Text color when cell matches your input
757+
matched_background_color = "#f8bd96" # Background color for matched cells
758+
matched_border_color = "#f8bd96" # Border color for matched cells
759+
border_color = "#abe9b3"
760+
border_width = 1
761+
762+
# Behavior
763+
live_match_update = true # Update matched cells in real-time without screen flashing
764+
subgrid_enabled = true # Enable 3×3 sublayer for precision
765+
subgrid_rows = 3
766+
subgrid_cols = 3
767+
sublayer_keys = "asdfghjkl" # Keys for subgrid selection (at least 9 characters for 3×3)
768+
```
769+
770+
**Multi-monitor notes:**
771+
- Grid automatically activates on the screen containing your mouse cursor
772+
- Cell sizes adapt to screen resolution (smaller cells on larger displays)
773+
- Works seamlessly on ultra-wide (21:9, 32:9) and vertical monitor setups
774+
- Switching monitors requires re-activating grid mode to adapt to new screen
775+
670776
## 🖥️ CLI Usage
671777

672778
Neru provides comprehensive CLI commands with IPC (Inter-Process Communication) for controlling the daemon.
@@ -707,6 +813,18 @@ neru hints move_mouse # Show hints and move the mouse to the position, no clicki
707813
neru hints scroll # Enter scroll mode
708814
neru hints context_menu # Show hints and trigger context menu action
709815

816+
# Grid mode (click anywhere on screen with precision)
817+
neru grid left_click # Activate grid mode with left click
818+
neru grid right_click # Grid mode with right click
819+
neru grid double_click # Grid mode with double click
820+
neru grid triple_click # Grid mode with triple click
821+
neru grid mouse_down # Grid mode with mouse down (hold)
822+
neru grid mouse_up # Grid mode with mouse up (release)
823+
neru grid middle_click # Grid mode with middle click
824+
neru grid move_mouse # Grid mode to move mouse without clicking
825+
neru grid scroll # Grid mode with scroll
826+
neru grid context_menu # Grid mode with context menu
827+
710828
# Return to idle state
711829
neru idle # Return to idle state
712830
```
@@ -841,6 +959,7 @@ neru/
841959
│ ├── accessibility/ # Accessibility API wrappers
842960
│ ├── appwatcher/ # App watcher with callbacks
843961
│ ├── electron/ # Electron manager (includes Chromium and Firefox)
962+
│ ├── grid/ # Grid mode implementation
844963
│ ├── hints/ # Hint generation and display logic
845964
│ ├── scroll/ # Scroll mode implementation
846965
│ ├── hotkeys/ # Global hotkey management

cmd/neru/hotkeys.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,19 @@ func (a *App) registerHotkeys() error {
2222
if key == "" || action == "" {
2323
continue
2424
}
25+
// Skip registering bindings for disabled modes
26+
mode := action
27+
if parts := strings.Split(action, " "); len(parts) > 0 {
28+
mode = parts[0]
29+
}
30+
if mode == "hints" && !a.config.Hints.Enabled {
31+
continue
32+
}
33+
if mode == "grid" && !a.config.Grid.Enabled {
34+
continue
35+
}
2536

2637
a.logger.Info("Registering hotkey binding", zap.String("key", key), zap.String("action", action))
27-
2838
// Capture values for closure
2939
bindKey := key
3040
bindAction := action

cmd/neru/ipc_handlers.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ func (a *App) handleIPCCommand(cmd ipc.Command) ipc.Response {
2424
return a.handleStop(cmd)
2525
case "hints":
2626
return a.handleHints(cmd)
27+
case "grid":
28+
return a.handleGrid(cmd)
2729
case "idle":
2830
return a.handleIdle(cmd)
2931
case "status":
@@ -58,6 +60,9 @@ func (a *App) handleHints(cmd ipc.Command) ipc.Response {
5860
if !a.enabled {
5961
return ipc.Response{Success: false, Message: "neru is not running"}
6062
}
63+
if !a.config.Hints.Enabled {
64+
return ipc.Response{Success: false, Message: "hints mode is disabled by config"}
65+
}
6166

6267
// Parse params
6368
params := cmd.Args
@@ -91,6 +96,46 @@ func (a *App) handleHints(cmd ipc.Command) ipc.Response {
9196
return ipc.Response{Success: true, Message: "hint mode activated"}
9297
}
9398

99+
func (a *App) handleGrid(cmd ipc.Command) ipc.Response {
100+
if !a.enabled {
101+
return ipc.Response{Success: false, Message: "neru is not running"}
102+
}
103+
if !a.config.Grid.Enabled {
104+
return ipc.Response{Success: false, Message: "grid mode is disabled by config"}
105+
}
106+
107+
// Parse params
108+
params := cmd.Args
109+
for _, param := range params {
110+
switch param {
111+
case "left_click":
112+
a.activateMode(ModeGrid, ActionLeftClick)
113+
case "right_click":
114+
a.activateMode(ModeGrid, ActionRightClick)
115+
case "double_click":
116+
a.activateMode(ModeGrid, ActionDoubleClick)
117+
case "triple_click":
118+
a.activateMode(ModeGrid, ActionTripleClick)
119+
case "mouse_up":
120+
a.activateMode(ModeGrid, ActionMouseUp)
121+
case "mouse_down":
122+
a.activateMode(ModeGrid, ActionMouseDown)
123+
case "middle_click":
124+
a.activateMode(ModeGrid, ActionMiddleClick)
125+
case "move_mouse":
126+
a.activateMode(ModeGrid, ActionMoveMouse)
127+
case "scroll":
128+
a.activateMode(ModeGrid, ActionScroll)
129+
case "context_menu":
130+
a.activateMode(ModeGrid, ActionContextMenu)
131+
default:
132+
return ipc.Response{Success: false, Message: fmt.Sprintf("unknown grid action: %s", param)}
133+
}
134+
}
135+
136+
return ipc.Response{Success: true, Message: "grid mode activated"}
137+
}
138+
94139
func (a *App) handleIdle(cmd ipc.Command) ipc.Response {
95140
if !a.enabled {
96141
return ipc.Response{Success: false, Message: "neru is not running"}

cmd/neru/lifecycle.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,10 @@ func (a *App) handleAppActivation(bundleID string) {
5555
go a.refreshHotkeysForAppOrCurrent(bundleID)
5656
a.logger.Debug("Handled hotkey refresh")
5757

58-
if a.config.Accessibility.AdditionalAXSupport.Enable {
59-
a.handleAdditionalAccessibility(bundleID)
58+
if a.config.Hints.Enabled {
59+
if a.config.Accessibility.AdditionalAXSupport.Enable {
60+
a.handleAdditionalAccessibility(bundleID)
61+
}
6062
}
6163

6264
a.logger.Debug("Done handling app activation")
@@ -90,6 +92,18 @@ func (a *App) printStartupInfo() {
9092
fmt.Println("✓ Neru is running")
9193

9294
for k, v := range a.config.Hotkeys.Bindings {
95+
// Skip showing bindings for disabled modes
96+
mode := v
97+
if parts := strings.Split(v, " "); len(parts) > 0 {
98+
mode = parts[0]
99+
}
100+
if mode == "hints" && !a.config.Hints.Enabled {
101+
continue
102+
}
103+
if mode == "grid" && !a.config.Grid.Enabled {
104+
continue
105+
}
106+
93107
toShow := v
94108
if strings.HasPrefix(v, "exec") {
95109
runes := []rune(v)

0 commit comments

Comments
 (0)