detached fa2a336

Merge pull request #172 from bnema/feature/esc-hide-floating-pane

fix: hide visible floating pane on Escape
commit
fa2a336e5e7658686ef9833a48a202f40ed3786b
author
bnema · · 2026-02-17 05:47 UTC
committer
GitHub · · 2026-02-17 05:47 UTC
merge of
cb6aa35 + 9244af4 · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
3 files · +34 -0
M internal/ui/app.go +7 -0
@@ -625,6 +625,9 @@ func (a *App) initKeyboardHandler(ctx context.Context) {
625625 }
626626 return a.kbDispatcher.Dispatch(ctx, action)
627627 })
628+ a.keyboardHandler.SetOnEscape(func(ctx context.Context) bool {
629+ return a.handleGlobalEscape(ctx)
630+ })
628631 a.keyboardHandler.SetOnModeChange(func(from, to input.Mode) {
629632 a.handleModeChange(ctx, from, to)
630633 })
@@ -2730,6 +2733,10 @@ func (a *App) closeActiveFloatingPane(ctx context.Context) bool {
27302733 return true
27312734 }
27322735
2736+func (a *App) handleGlobalEscape(ctx context.Context) bool {
2737+ return a.closeActiveFloatingPane(ctx)
2738+}
2739+
27332740 func (a *App) closeAndReleaseActiveFloatingPane(ctx context.Context) bool {
27342741 key, session, ok := a.activeFloatingSessionEntry()
27352742 if !ok {
@@ -625,6 +625,9 @@ func (a *App) initKeyboardHandler(ctx context.Context) {
625 }625 }
626 return a.kbDispatcher.Dispatch(ctx, action)626 return a.kbDispatcher.Dispatch(ctx, action)
627 })627 })
628+ a.keyboardHandler.SetOnEscape(func(ctx context.Context) bool {
629+ return a.handleGlobalEscape(ctx)
630+ })
628 a.keyboardHandler.SetOnModeChange(func(from, to input.Mode) {631 a.keyboardHandler.SetOnModeChange(func(from, to input.Mode) {
629 a.handleModeChange(ctx, from, to)632 a.handleModeChange(ctx, from, to)
630 })633 })
@@ -2730,6 +2733,10 @@ func (a *App) closeActiveFloatingPane(ctx context.Context) bool {
2730 return true2733 return true
2731 }2734 }
2732 2735
2736+func (a *App) handleGlobalEscape(ctx context.Context) bool {
2737+ return a.closeActiveFloatingPane(ctx)
2738+}
2739+
2733 func (a *App) closeAndReleaseActiveFloatingPane(ctx context.Context) bool {2740 func (a *App) closeAndReleaseActiveFloatingPane(ctx context.Context) bool {
2734 key, session, ok := a.activeFloatingSessionEntry()2741 key, session, ok := a.activeFloatingSessionEntry()
2735 if !ok {2742 if !ok {
M internal/ui/floating_pane_test.go +11 -0
@@ -312,6 +312,17 @@ func TestFloatingPane_CloseActiveFloatingSession(t *testing.T) {
312312 assert.False(t, handled)
313313 }
314314
315+func TestFloatingPane_HandleGlobalEscape_HidesVisibleFloatingPane(t *testing.T) {
316+ app, _, session := newFloatingPaneTestApp(t)
317+
318+ require.NoError(t, app.ToggleFloatingPane(context.Background()))
319+ require.True(t, session.pane.IsVisible())
320+
321+ handled := app.handleGlobalEscape(context.Background())
322+ assert.True(t, handled)
323+ assert.False(t, session.pane.IsVisible())
324+}
325+
315326 func TestFloatingPane_OpenFloatingPaneSession_OptionalURL(t *testing.T) {
316327 app, _, session := newFloatingPaneTestApp(t)
317328
@@ -312,6 +312,17 @@ func TestFloatingPane_CloseActiveFloatingSession(t *testing.T) {
312 assert.False(t, handled)312 assert.False(t, handled)
313 }313 }
314 314
315+func TestFloatingPane_HandleGlobalEscape_HidesVisibleFloatingPane(t *testing.T) {
316+ app, _, session := newFloatingPaneTestApp(t)
317+
318+ require.NoError(t, app.ToggleFloatingPane(context.Background()))
319+ require.True(t, session.pane.IsVisible())
320+
321+ handled := app.handleGlobalEscape(context.Background())
322+ assert.True(t, handled)
323+ assert.False(t, session.pane.IsVisible())
324+}
325+
315 func TestFloatingPane_OpenFloatingPaneSession_OptionalURL(t *testing.T) {326 func TestFloatingPane_OpenFloatingPaneSession_OptionalURL(t *testing.T) {
316 app, _, session := newFloatingPaneTestApp(t)327 app, _, session := newFloatingPaneTestApp(t)
317 328
M internal/ui/input/keyboard.go +16 -0
@@ -44,6 +44,8 @@ type KeyboardHandler struct {
4444 shouldBypass func(modifiers Modifier) bool
4545 // Optional accent handler for dead keys support
4646 accentHandler AccentHandler
47+ // Optional escape hook for app-level overlays
48+ onEscape func(ctx context.Context) bool
4749
4850 // GTK controller (nil until attached)
4951 controller *gtk.EventControllerKey
@@ -112,6 +114,14 @@ func (h *KeyboardHandler) SetAccentHandler(handler AccentHandler) {
112114 h.accentHandler = handler
113115 }
114116
117+// SetOnEscape sets an optional callback invoked for plain Escape in normal mode.
118+// Return true to consume the key and stop further handling.
119+func (h *KeyboardHandler) SetOnEscape(fn func(ctx context.Context) bool) {
120+ h.mu.Lock()
121+ defer h.mu.Unlock()
122+ h.onEscape = fn
123+}
124+
115125 // Mode returns the current input mode.
116126 func (h *KeyboardHandler) Mode() Mode {
117127 return h.modal.Mode()
@@ -176,6 +186,7 @@ func (h *KeyboardHandler) handleKeyPress(keyval, keycode uint, state gdk.Modifie
176186 h.mu.RLock()
177187 shouldBypass := h.shouldBypass
178188 accentHandler := h.accentHandler
189+ onEscape := h.onEscape
179190 h.mu.RUnlock()
180191
181192 // Check if accent picker is visible - it takes priority
@@ -184,6 +195,11 @@ func (h *KeyboardHandler) handleKeyPress(keyval, keycode uint, state gdk.Modifie
184195 }
185196
186197 modifiers := Modifier(state) & modifierMask
198+ if h.modal.Mode() == ModeNormal && keyval == uint(gdk.KEY_Escape) && modifiers == 0 {
199+ if onEscape != nil && onEscape(h.ctx) {
200+ return true
201+ }
202+ }
187203
188204 // Check accent detection first - works in both normal and bypass mode
189205 // This enables accent picker for omnibox (GTK SearchEntry) as well as WebView
@@ -44,6 +44,8 @@ type KeyboardHandler struct {
44 shouldBypass func(modifiers Modifier) bool44 shouldBypass func(modifiers Modifier) bool
45 // Optional accent handler for dead keys support45 // Optional accent handler for dead keys support
46 accentHandler AccentHandler46 accentHandler AccentHandler
47+ // Optional escape hook for app-level overlays
48+ onEscape func(ctx context.Context) bool
47 49
48 // GTK controller (nil until attached)50 // GTK controller (nil until attached)
49 controller *gtk.EventControllerKey51 controller *gtk.EventControllerKey
@@ -112,6 +114,14 @@ func (h *KeyboardHandler) SetAccentHandler(handler AccentHandler) {
112 h.accentHandler = handler114 h.accentHandler = handler
113 }115 }
114 116
117+// SetOnEscape sets an optional callback invoked for plain Escape in normal mode.
118+// Return true to consume the key and stop further handling.
119+func (h *KeyboardHandler) SetOnEscape(fn func(ctx context.Context) bool) {
120+ h.mu.Lock()
121+ defer h.mu.Unlock()
122+ h.onEscape = fn
123+}
124+
115 // Mode returns the current input mode.125 // Mode returns the current input mode.
116 func (h *KeyboardHandler) Mode() Mode {126 func (h *KeyboardHandler) Mode() Mode {
117 return h.modal.Mode()127 return h.modal.Mode()
@@ -176,6 +186,7 @@ func (h *KeyboardHandler) handleKeyPress(keyval, keycode uint, state gdk.Modifie
176 h.mu.RLock()186 h.mu.RLock()
177 shouldBypass := h.shouldBypass187 shouldBypass := h.shouldBypass
178 accentHandler := h.accentHandler188 accentHandler := h.accentHandler
189+ onEscape := h.onEscape
179 h.mu.RUnlock()190 h.mu.RUnlock()
180 191
181 // Check if accent picker is visible - it takes priority192 // Check if accent picker is visible - it takes priority
@@ -184,6 +195,11 @@ func (h *KeyboardHandler) handleKeyPress(keyval, keycode uint, state gdk.Modifie
184 }195 }
185 196
186 modifiers := Modifier(state) & modifierMask197 modifiers := Modifier(state) & modifierMask
198+ if h.modal.Mode() == ModeNormal && keyval == uint(gdk.KEY_Escape) && modifiers == 0 {
199+ if onEscape != nil && onEscape(h.ctx) {
200+ return true
201+ }
202+ }
187 203
188 // Check accent detection first - works in both normal and bypass mode204 // Check accent detection first - works in both normal and bypass mode
189 // This enables accent picker for omnibox (GTK SearchEntry) as well as WebView205 // This enables accent picker for omnibox (GTK SearchEntry) as well as WebView

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 8

Tags 42

↑↓ moveenter openesc close