Skip to content

Commit e35c486

Browse files
authored
fix: ensure grid and hint dont sleep when refresh overlays (#124)
1 parent 856f0b6 commit e35c486

7 files changed

Lines changed: 192 additions & 30 deletions

File tree

cmd/neru/lifecycle.go

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -60,30 +60,52 @@ func (a *App) handleScreenParametersChange() {
6060
defer func() { a.screenChangeProcessing = false }()
6161

6262
a.logger.Info("Screen parameters changed; adjusting overlays")
63-
// Only act if grid is enabled
63+
64+
// Handle grid overlay
6465
if a.config.Grid.Enabled && a.gridCtx != nil && a.gridCtx.gridOverlay != nil {
6566
// If grid mode is not active, mark for refresh on next activation
6667
if a.currentMode != ModeGrid {
6768
a.gridOverlayNeedsRefresh = true
68-
return
69-
}
69+
} else {
70+
// Grid mode is active - resize the existing overlay window to match new screen bounds
71+
gridOverlay := *a.gridCtx.gridOverlay
7072

71-
// Grid mode is active - resize the existing overlay window to match new screen bounds
72-
gridOverlay := *a.gridCtx.gridOverlay
73+
// Resize overlay window to current active screen (where mouse is)
74+
gridOverlay.ResizeToActiveScreenSync()
7375

74-
// Resize overlay window to current active screen (where mouse is)
75-
gridOverlay.ResizeToActiveScreen()
76-
77-
// Give the UI thread a moment to complete the resize
78-
time.Sleep(150 * time.Millisecond)
76+
// Regenerate the grid cells with updated screen bounds
77+
if err := a.setupGrid(a.gridCtx.currentAction); err != nil {
78+
a.logger.Error("Failed to refresh grid after screen change", zap.Error(err))
79+
return
80+
}
7981

80-
// Regenerate the grid cells with updated screen bounds
81-
if err := a.setupGrid(a.gridCtx.currentAction); err != nil {
82-
a.logger.Error("Failed to refresh grid after screen change", zap.Error(err))
83-
return
82+
a.logger.Info("Grid overlay resized and regenerated for new screen bounds")
8483
}
84+
}
8585

86-
a.logger.Info("Grid overlay resized and regenerated for new screen bounds")
86+
// Handle hint overlay
87+
if a.config.Hints.Enabled && a.hintOverlay != nil {
88+
// If hints mode is not active, mark for refresh on next activation
89+
if a.currentMode != ModeHints {
90+
a.hintOverlayNeedsRefresh = true
91+
} else {
92+
// Hints mode is active - resize the overlay and regenerate hints
93+
a.hintOverlay.ResizeToActiveScreenSync()
94+
95+
// Regenerate hints for current action
96+
a.updateRolesForCurrentApp()
97+
elements := a.collectElementsForAction(a.currentAction)
98+
if len(elements) > 0 {
99+
if err := a.setupHints(elements, a.currentAction); err != nil {
100+
a.logger.Error("Failed to refresh hints after screen change", zap.Error(err))
101+
return
102+
}
103+
a.logger.Info("Hint overlay resized and regenerated for new screen bounds")
104+
} else {
105+
a.logger.Warn("No elements found after screen change")
106+
a.exitMode()
107+
}
108+
}
87109
}
88110
}
89111

cmd/neru/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ type App struct {
7373
hotkeysRegistered bool
7474
screenChangeProcessing bool
7575
gridOverlayNeedsRefresh bool
76+
hintOverlayNeedsRefresh bool
7677
hotkeyRefreshPending bool
7778
}
7879

cmd/neru/modes.go

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"fmt"
55
"image"
66
"strings"
7-
"time"
87

98
"github.com/y3owk1n/neru/internal/accessibility"
109
"github.com/y3owk1n/neru/internal/bridge"
@@ -75,11 +74,12 @@ func (a *App) activateHintMode(action Action) {
7574
return
7675
}
7776

78-
// Resize overlay to the active screen before collecting elements
77+
// Always resize overlay to the active screen (where mouse is) before collecting elements
78+
// This ensures proper positioning when switching between multiple displays
7979
if a.hintOverlay != nil {
80-
a.hintOverlay.ResizeToActiveScreen()
81-
// Wait for async resize to complete on main thread
82-
time.Sleep(100 * time.Millisecond)
80+
// Use synchronous resize with callback - no artificial delay needed
81+
a.hintOverlay.ResizeToActiveScreenSync()
82+
a.hintOverlayNeedsRefresh = false
8383
}
8484

8585
// Update roles for the current focused app
@@ -167,17 +167,13 @@ func (a *App) activateGridMode(action Action) {
167167

168168
a.exitMode() // Exit current mode first
169169

170-
if a.gridOverlayNeedsRefresh {
171-
if a.gridCtx != nil && a.gridCtx.gridOverlay != nil {
172-
gridOverlay := *a.gridCtx.gridOverlay
173-
174-
// Resize overlay window to current active screen (where mouse is)
175-
gridOverlay.ResizeToActiveScreen()
176-
177-
// Give the UI thread a moment to complete the resize
178-
time.Sleep(150 * time.Millisecond)
179-
}
170+
// Always resize overlay to the active screen (where mouse is) before drawing grid
171+
// This ensures proper positioning when switching between multiple displays
172+
if a.gridCtx != nil && a.gridCtx.gridOverlay != nil {
173+
gridOverlay := *a.gridCtx.gridOverlay
180174

175+
// Use synchronous resize with callback - no artificial delay needed
176+
gridOverlay.ResizeToActiveScreenSync()
181177
a.gridOverlayNeedsRefresh = false
182178
}
183179

internal/bridge/overlay.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ typedef struct {
5353
int isSubgrid; // 1 if cell is part of subgrid, 0 otherwise
5454
} GridCell;
5555

56+
// Callback type for async operations
57+
typedef void (*ResizeCompletionCallback)(void* context);
58+
5659
// Function declarations
5760
OverlayWindow createOverlayWindow();
5861
void destroyOverlayWindow(OverlayWindow window);
@@ -66,6 +69,7 @@ void drawTargetDot(OverlayWindow window, CGPoint center, double radius, const ch
6669
void replaceOverlayWindow(OverlayWindow *pwindow);
6770
void resizeOverlayToMainScreen(OverlayWindow window);
6871
void resizeOverlayToActiveScreen(OverlayWindow window);
72+
void resizeOverlayToActiveScreenWithCallback(OverlayWindow window, ResizeCompletionCallback callback, void* context);
6973

7074
// Grid-specific drawing functions
7175
void drawGridCells(OverlayWindow window, GridCell* cells, int count, GridCellStyle style);

internal/bridge/overlay.m

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,51 @@ void resizeOverlayToActiveScreen(OverlayWindow window) {
724724
});
725725
}
726726

727+
void resizeOverlayToActiveScreenWithCallback(OverlayWindow window, ResizeCompletionCallback callback, void* context) {
728+
if (!window) {
729+
if (callback) callback(context);
730+
return;
731+
}
732+
733+
OverlayWindowController *controller = (OverlayWindowController*)window;
734+
dispatch_async(dispatch_get_main_queue(), ^{
735+
// Get current mouse location
736+
NSPoint mouseLoc = [NSEvent mouseLocation];
737+
738+
// Find the screen containing the mouse cursor
739+
NSScreen *activeScreen = nil;
740+
for (NSScreen *screen in [NSScreen screens]) {
741+
if (NSPointInRect(mouseLoc, screen.frame)) {
742+
activeScreen = screen;
743+
break;
744+
}
745+
}
746+
747+
// Fall back to main screen if mouse is somehow not on any screen
748+
if (!activeScreen) {
749+
activeScreen = [NSScreen mainScreen];
750+
}
751+
752+
if (!activeScreen) {
753+
if (callback) callback(context);
754+
return;
755+
}
756+
757+
NSRect screenFrame = [activeScreen frame];
758+
[controller.window setFrame:screenFrame display:YES];
759+
760+
// View frame should be in window's coordinate space (origin at 0,0)
761+
NSRect viewFrame = NSMakeRect(0, 0, screenFrame.size.width, screenFrame.size.height);
762+
[controller.overlayView setFrame:viewFrame];
763+
[controller.overlayView setNeedsDisplay:YES];
764+
765+
// Call completion callback
766+
if (callback) {
767+
callback(context);
768+
}
769+
});
770+
}
771+
727772
void drawHints(OverlayWindow window, HintData* hints, int count, HintStyle style) {
728773
if (!window || !hints) return;
729774

internal/grid/grid_overlay.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,40 @@ package grid
44
#cgo CFLAGS: -x objective-c
55
#include "../bridge/overlay.h"
66
#include <stdlib.h>
7+
8+
// Callback function that Go can reference
9+
extern void gridResizeCompletionCallback(void* context);
710
*/
811
import "C"
912

1013
import (
1114
"strings"
15+
"sync"
16+
"sync/atomic"
1217
"unsafe"
1318

1419
"github.com/y3owk1n/neru/internal/config"
1520
)
1621

22+
var (
23+
gridCallbackID uint64
24+
gridCallbackMap = make(map[uint64]chan struct{})
25+
gridCallbackLock sync.Mutex
26+
)
27+
28+
//export gridResizeCompletionCallback
29+
func gridResizeCompletionCallback(context unsafe.Pointer) {
30+
// Convert context to callback ID
31+
id := uint64(uintptr(context))
32+
33+
gridCallbackLock.Lock()
34+
if done, ok := gridCallbackMap[id]; ok {
35+
close(done)
36+
delete(gridCallbackMap, id)
37+
}
38+
gridCallbackLock.Unlock()
39+
}
40+
1741
// GridOverlay manages grid-specific overlay rendering
1842
type GridOverlay struct {
1943
window C.OverlayWindow
@@ -82,6 +106,29 @@ func (o *GridOverlay) ResizeToActiveScreen() {
82106
C.resizeOverlayToActiveScreen(o.window)
83107
}
84108

109+
// ResizeToActiveScreenSync resizes the overlay window synchronously with callback notification
110+
func (o *GridOverlay) ResizeToActiveScreenSync() {
111+
done := make(chan struct{})
112+
113+
// Generate unique ID for this callback
114+
id := atomic.AddUint64(&gridCallbackID, 1)
115+
116+
// Store channel in map
117+
gridCallbackLock.Lock()
118+
gridCallbackMap[id] = done
119+
gridCallbackLock.Unlock()
120+
121+
// Pass ID as context (safe - no Go pointers)
122+
// Note: uintptr conversion must happen in same expression to satisfy go vet
123+
C.resizeOverlayToActiveScreenWithCallback(
124+
o.window,
125+
(C.ResizeCompletionCallback)(unsafe.Pointer(C.gridResizeCompletionCallback)),
126+
*(*unsafe.Pointer)(unsafe.Pointer(&id)),
127+
)
128+
129+
<-done
130+
}
131+
85132
// Draw renders the flat grid with all 3-char cells visible
86133
func (o *GridOverlay) Draw(grid *Grid, currentInput string) error {
87134
// Clear existing content

internal/hints/overlay.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,41 @@ package hints
44
#cgo CFLAGS: -x objective-c
55
#include "../bridge/overlay.h"
66
#include <stdlib.h>
7+
8+
// Callback function that Go can reference
9+
extern void resizeCompletionCallback(void* context);
710
*/
811
import "C"
912

1013
import (
1114
"fmt"
1215
"strings"
16+
"sync"
17+
"sync/atomic"
1318
"unsafe"
1419

1520
"github.com/y3owk1n/neru/internal/config"
1621
)
1722

23+
var (
24+
hintCallbackID uint64
25+
hintCallbackMap = make(map[uint64]chan struct{})
26+
hintCallbackLock sync.Mutex
27+
)
28+
29+
//export resizeCompletionCallback
30+
func resizeCompletionCallback(context unsafe.Pointer) {
31+
// Convert context to callback ID
32+
id := uint64(uintptr(context))
33+
34+
hintCallbackLock.Lock()
35+
if done, ok := hintCallbackMap[id]; ok {
36+
close(done)
37+
delete(hintCallbackMap, id)
38+
}
39+
hintCallbackLock.Unlock()
40+
}
41+
1842
// Overlay manages the hint overlay window
1943
type Overlay struct {
2044
window C.OverlayWindow
@@ -67,6 +91,29 @@ func (o *Overlay) ResizeToActiveScreen() {
6791
C.resizeOverlayToActiveScreen(o.window)
6892
}
6993

94+
// ResizeToActiveScreenSync resizes the overlay window synchronously with callback notification
95+
func (o *Overlay) ResizeToActiveScreenSync() {
96+
done := make(chan struct{})
97+
98+
// Generate unique ID for this callback
99+
id := atomic.AddUint64(&hintCallbackID, 1)
100+
101+
// Store channel in map
102+
hintCallbackLock.Lock()
103+
hintCallbackMap[id] = done
104+
hintCallbackLock.Unlock()
105+
106+
// Pass ID as context (safe - no Go pointers)
107+
// Note: uintptr conversion must happen in same expression to satisfy go vet
108+
C.resizeOverlayToActiveScreenWithCallback(
109+
o.window,
110+
(C.ResizeCompletionCallback)(unsafe.Pointer(C.resizeCompletionCallback)),
111+
*(*unsafe.Pointer)(unsafe.Pointer(&id)),
112+
)
113+
114+
<-done
115+
}
116+
70117
// DrawHintsWithoutArrow draws hints without arrows
71118
func (o *Overlay) DrawHintsWithoutArrow(hints []*Hint, style StyleMode) error {
72119
return o.drawHintsInternal(hints, style, false)

0 commit comments

Comments
 (0)