detached ff584d1

fix(ui): address PR review follow-ups

commit
ff584d18e50f0359a9793ca653cc5ffd575410d7
author
brice · · 2026-02-10 05:45 UTC
parent
941c1f6
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
7 files · +61 -2
M internal/ui/app.go +1 -0
@@ -2454,6 +2454,7 @@ func (a *App) releaseFloatingSession(ctx context.Context, key floatingSessionKey
24542454 session.overlay = nil
24552455 session.webView = nil
24562456 session.pane = nil
2457+ // Session is fully released/zeroed; caller must drop this reference and not reuse it.
24572458 }
24582459
24592460 func (a *App) updateFloatingSessionURI(paneID entity.PaneID, url string) {
@@ -2454,6 +2454,7 @@ func (a *App) releaseFloatingSession(ctx context.Context, key floatingSessionKey
2454 session.overlay = nil2454 session.overlay = nil
2455 session.webView = nil2455 session.webView = nil
2456 session.pane = nil2456 session.pane = nil
2457+ // Session is fully released/zeroed; caller must drop this reference and not reuse it.
2457 }2458 }
2458 2459
2459 func (a *App) updateFloatingSessionURI(paneID entity.PaneID, url string) {2460 func (a *App) updateFloatingSessionURI(paneID entity.PaneID, url string) {
M internal/ui/component/floating_pane.go +6 -1
@@ -125,7 +125,12 @@ func (fp *FloatingPane) SessionStarted() bool {
125125 func (fp *FloatingPane) ShowURL(ctx context.Context, url string) error {
126126 url = strings.TrimSpace(url)
127127 if url == "" {
128- return fp.ShowToggle(ctx)
128+ fp.mu.Lock()
129+ fp.visible = true
130+ fp.omniboxVisible = true
131+ fp.stateVersion++
132+ fp.mu.Unlock()
133+ return nil
129134 }
130135
131136 fp.mu.Lock()
@@ -125,7 +125,12 @@ func (fp *FloatingPane) SessionStarted() bool {
125 func (fp *FloatingPane) ShowURL(ctx context.Context, url string) error {125 func (fp *FloatingPane) ShowURL(ctx context.Context, url string) error {
126 url = strings.TrimSpace(url)126 url = strings.TrimSpace(url)
127 if url == "" {127 if url == "" {
128- return fp.ShowToggle(ctx)128+ fp.mu.Lock()
129+ fp.visible = true
130+ fp.omniboxVisible = true
131+ fp.stateVersion++
132+ fp.mu.Unlock()
133+ return nil
129 }134 }
130 135
131 fp.mu.Lock()136 fp.mu.Lock()
M internal/ui/component/workspace_view.go +6 -0
@@ -557,6 +557,12 @@ func (wv *WorkspaceView) AddWorkspaceOverlayWidget(widget layout.Widget) {
557557 if parent == wv.overlay {
558558 return
559559 }
560+
561+ parentGTK := parent.GtkWidget()
562+ overlayGTK := wv.overlay.GtkWidget()
563+ if parentGTK != nil && overlayGTK != nil && parentGTK.GoPointer() == overlayGTK.GoPointer() {
564+ return
565+ }
560566 widget.Unparent()
561567 }
562568
@@ -557,6 +557,12 @@ func (wv *WorkspaceView) AddWorkspaceOverlayWidget(widget layout.Widget) {
557 if parent == wv.overlay {557 if parent == wv.overlay {
558 return558 return
559 }559 }
560+
561+ parentGTK := parent.GtkWidget()
562+ overlayGTK := wv.overlay.GtkWidget()
563+ if parentGTK != nil && overlayGTK != nil && parentGTK.GoPointer() == overlayGTK.GoPointer() {
564+ return
565+ }
560 widget.Unparent()566 widget.Unparent()
561 }567 }
562 568
M internal/ui/component/workspace_view_test.go +32 -0
@@ -4,6 +4,7 @@ import (
44 "context"
55 "testing"
66
7+ "github.com/jwijenbergh/puregotk/v4/gtk"
78 "github.com/stretchr/testify/assert"
89 "github.com/stretchr/testify/mock"
910 "github.com/stretchr/testify/require"
@@ -316,6 +317,8 @@ func TestAddWorkspaceOverlayWidget_UnparentsBeforeAdding(t *testing.T) {
316317 widget := mocks.NewMockWidget(t)
317318 otherParent := mocks.NewMockWidget(t)
318319 widget.EXPECT().GetParent().Return(otherParent).Once()
320+ otherParent.EXPECT().GtkWidget().Return(nil).Once()
321+ mockOverlay.EXPECT().GtkWidget().Return(nil).Once()
319322 widget.EXPECT().Unparent().Once()
320323 mockOverlay.EXPECT().AddOverlay(widget).Once()
321324 mockOverlay.EXPECT().SetClipOverlay(widget, false).Once()
@@ -324,6 +327,35 @@ func TestAddWorkspaceOverlayWidget_UnparentsBeforeAdding(t *testing.T) {
324327 wv.AddWorkspaceOverlayWidget(widget)
325328 }
326329
330+func TestAddWorkspaceOverlayWidget_DoesNotReaddWhenParentGTKMatchesOverlay(t *testing.T) {
331+ mockFactory := mocks.NewMockWidgetFactory(t)
332+ ctx := context.Background()
333+
334+ mockContainer := mocks.NewMockBoxWidget(t)
335+ mockOverlay := mocks.NewMockOverlayWidget(t)
336+ mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()
337+ mockContainer.EXPECT().SetHexpand(true).Once()
338+ mockContainer.EXPECT().SetVexpand(true).Once()
339+ mockContainer.EXPECT().SetVisible(true).Once()
340+ mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
341+ mockOverlay.EXPECT().SetHexpand(true).Once()
342+ mockOverlay.EXPECT().SetVexpand(true).Once()
343+ mockOverlay.EXPECT().SetChild(mockContainer).Once()
344+ mockOverlay.EXPECT().SetVisible(true).Once()
345+
346+ wv := component.NewWorkspaceView(ctx, mockFactory)
347+
348+ widget := mocks.NewMockWidget(t)
349+ otherParent := mocks.NewMockWidget(t)
350+ gtkWidget := &gtk.Widget{}
351+
352+ widget.EXPECT().GetParent().Return(otherParent).Once()
353+ otherParent.EXPECT().GtkWidget().Return(gtkWidget).Once()
354+ mockOverlay.EXPECT().GtkWidget().Return(gtkWidget).Once()
355+
356+ wv.AddWorkspaceOverlayWidget(widget)
357+}
358+
327359 func TestSetActivePaneID_UpdatesStyling(t *testing.T) {
328360 // Arrange
329361 mockFactory := mocks.NewMockWidgetFactory(t)
@@ -4,6 +4,7 @@ import (
4 "context"4 "context"
5 "testing"5 "testing"
6 6
7+ "github.com/jwijenbergh/puregotk/v4/gtk"
7 "github.com/stretchr/testify/assert"8 "github.com/stretchr/testify/assert"
8 "github.com/stretchr/testify/mock"9 "github.com/stretchr/testify/mock"
9 "github.com/stretchr/testify/require"10 "github.com/stretchr/testify/require"
@@ -316,6 +317,8 @@ func TestAddWorkspaceOverlayWidget_UnparentsBeforeAdding(t *testing.T) {
316 widget := mocks.NewMockWidget(t)317 widget := mocks.NewMockWidget(t)
317 otherParent := mocks.NewMockWidget(t)318 otherParent := mocks.NewMockWidget(t)
318 widget.EXPECT().GetParent().Return(otherParent).Once()319 widget.EXPECT().GetParent().Return(otherParent).Once()
320+ otherParent.EXPECT().GtkWidget().Return(nil).Once()
321+ mockOverlay.EXPECT().GtkWidget().Return(nil).Once()
319 widget.EXPECT().Unparent().Once()322 widget.EXPECT().Unparent().Once()
320 mockOverlay.EXPECT().AddOverlay(widget).Once()323 mockOverlay.EXPECT().AddOverlay(widget).Once()
321 mockOverlay.EXPECT().SetClipOverlay(widget, false).Once()324 mockOverlay.EXPECT().SetClipOverlay(widget, false).Once()
@@ -324,6 +327,35 @@ func TestAddWorkspaceOverlayWidget_UnparentsBeforeAdding(t *testing.T) {
324 wv.AddWorkspaceOverlayWidget(widget)327 wv.AddWorkspaceOverlayWidget(widget)
325 }328 }
326 329
330+func TestAddWorkspaceOverlayWidget_DoesNotReaddWhenParentGTKMatchesOverlay(t *testing.T) {
331+ mockFactory := mocks.NewMockWidgetFactory(t)
332+ ctx := context.Background()
333+
334+ mockContainer := mocks.NewMockBoxWidget(t)
335+ mockOverlay := mocks.NewMockOverlayWidget(t)
336+ mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()
337+ mockContainer.EXPECT().SetHexpand(true).Once()
338+ mockContainer.EXPECT().SetVexpand(true).Once()
339+ mockContainer.EXPECT().SetVisible(true).Once()
340+ mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
341+ mockOverlay.EXPECT().SetHexpand(true).Once()
342+ mockOverlay.EXPECT().SetVexpand(true).Once()
343+ mockOverlay.EXPECT().SetChild(mockContainer).Once()
344+ mockOverlay.EXPECT().SetVisible(true).Once()
345+
346+ wv := component.NewWorkspaceView(ctx, mockFactory)
347+
348+ widget := mocks.NewMockWidget(t)
349+ otherParent := mocks.NewMockWidget(t)
350+ gtkWidget := &gtk.Widget{}
351+
352+ widget.EXPECT().GetParent().Return(otherParent).Once()
353+ otherParent.EXPECT().GtkWidget().Return(gtkWidget).Once()
354+ mockOverlay.EXPECT().GtkWidget().Return(gtkWidget).Once()
355+
356+ wv.AddWorkspaceOverlayWidget(widget)
357+}
358+
327 func TestSetActivePaneID_UpdatesStyling(t *testing.T) {359 func TestSetActivePaneID_UpdatesStyling(t *testing.T) {
328 // Arrange360 // Arrange
329 mockFactory := mocks.NewMockWidgetFactory(t)361 mockFactory := mocks.NewMockWidgetFactory(t)
M internal/ui/coordinator/content_active_override_test.go +6 -0
@@ -12,6 +12,8 @@ import (
1212 )
1313
1414 func TestContentCoordinator_ActiveWebView_UsesOverride(t *testing.T) {
15+ t.Parallel()
16+
1517 mainWV := &webkit.WebView{}
1618 floatingWV := &webkit.WebView{}
1719
@@ -29,6 +31,8 @@ func TestContentCoordinator_ActiveWebView_UsesOverride(t *testing.T) {
2931 }
3032
3133 func TestContentCoordinator_ActiveWebView_ClearOverrideFallsBack(t *testing.T) {
34+ t.Parallel()
35+
3236 mainWV := &webkit.WebView{}
3337
3438 c := &ContentCoordinator{
@@ -45,6 +49,8 @@ func TestContentCoordinator_ActiveWebView_ClearOverrideFallsBack(t *testing.T) {
4549 }
4650
4751 func TestContentCoordinator_ActiveWebView_ClearOverrideFallsBackToWorkspace(t *testing.T) {
52+ t.Parallel()
53+
4854 ctx := context.Background()
4955 mainWV := &webkit.WebView{}
5056 mainPane := entity.NewPane(entity.PaneID("main-pane"))
@@ -12,6 +12,8 @@ import (
12 )12 )
13 13
14 func TestContentCoordinator_ActiveWebView_UsesOverride(t *testing.T) {14 func TestContentCoordinator_ActiveWebView_UsesOverride(t *testing.T) {
15+ t.Parallel()
16+
15 mainWV := &webkit.WebView{}17 mainWV := &webkit.WebView{}
16 floatingWV := &webkit.WebView{}18 floatingWV := &webkit.WebView{}
17 19
@@ -29,6 +31,8 @@ func TestContentCoordinator_ActiveWebView_UsesOverride(t *testing.T) {
29 }31 }
30 32
31 func TestContentCoordinator_ActiveWebView_ClearOverrideFallsBack(t *testing.T) {33 func TestContentCoordinator_ActiveWebView_ClearOverrideFallsBack(t *testing.T) {
34+ t.Parallel()
35+
32 mainWV := &webkit.WebView{}36 mainWV := &webkit.WebView{}
33 37
34 c := &ContentCoordinator{38 c := &ContentCoordinator{
@@ -45,6 +49,8 @@ func TestContentCoordinator_ActiveWebView_ClearOverrideFallsBack(t *testing.T) {
45 }49 }
46 50
47 func TestContentCoordinator_ActiveWebView_ClearOverrideFallsBackToWorkspace(t *testing.T) {51 func TestContentCoordinator_ActiveWebView_ClearOverrideFallsBackToWorkspace(t *testing.T) {
52+ t.Parallel()
53+
48 ctx := context.Background()54 ctx := context.Background()
49 mainWV := &webkit.WebView{}55 mainWV := &webkit.WebView{}
50 mainPane := entity.NewPane(entity.PaneID("main-pane"))56 mainPane := entity.NewPane(entity.PaneID("main-pane"))
M internal/ui/input/floating_shortcuts_test.go +9 -0
@@ -192,3 +192,12 @@ func TestShortcutSet_FloatingProfiles_SupportModifierCombos(t *testing.T) {
192192 assert.Equal(t, "search", searchTarget.SessionID)
193193 assert.Equal(t, "https://search.example.com", searchTarget.URL)
194194 }
195+
196+func TestParseFloatingProfileTarget_AllowsPipeInURL(t *testing.T) {
197+ action := NewFloatingProfileAction("pipe-test", "https://example.com/a|b")
198+
199+ target, ok := ParseFloatingProfileTarget(action)
200+ require.True(t, ok)
201+ assert.Equal(t, "pipe-test", target.SessionID)
202+ assert.Equal(t, "https://example.com/a|b", target.URL)
203+}
@@ -192,3 +192,12 @@ func TestShortcutSet_FloatingProfiles_SupportModifierCombos(t *testing.T) {
192 assert.Equal(t, "search", searchTarget.SessionID)192 assert.Equal(t, "search", searchTarget.SessionID)
193 assert.Equal(t, "https://search.example.com", searchTarget.URL)193 assert.Equal(t, "https://search.example.com", searchTarget.URL)
194 }194 }
195+
196+func TestParseFloatingProfileTarget_AllowsPipeInURL(t *testing.T) {
197+ action := NewFloatingProfileAction("pipe-test", "https://example.com/a|b")
198+
199+ target, ok := ParseFloatingProfileTarget(action)
200+ require.True(t, ok)
201+ assert.Equal(t, "pipe-test", target.SessionID)
202+ assert.Equal(t, "https://example.com/a|b", target.URL)
203+}
M internal/ui/input/shortcuts.go +1 -1
@@ -199,7 +199,7 @@ const (
199199 )
200200
201201 const floatingProfileActionPrefix = "open_floating_profile:"
202-const floatingProfileActionSeparator = "|"
202+const floatingProfileActionSeparator = "\u241F"
203203
204204 // ShortcutTable maps KeyBinding to Action.
205205 type ShortcutTable map[KeyBinding]Action
@@ -199,7 +199,7 @@ const (
199 )199 )
200 200
201 const floatingProfileActionPrefix = "open_floating_profile:"201 const floatingProfileActionPrefix = "open_floating_profile:"
202-const floatingProfileActionSeparator = "|"202+const floatingProfileActionSeparator = "\u241F"
203 203
204 // ShortcutTable maps KeyBinding to Action.204 // ShortcutTable maps KeyBinding to Action.
205 type ShortcutTable map[KeyBinding]Action205 type ShortcutTable map[KeyBinding]Action

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