detached 74ee7c4

feat(debug): trace stacked click routing (#446)

* feat(debug): trace stacked click routing

* fix(debug): spell canceled consistently
commit
74ee7c46c08a5806489c603244478514b77a5564
author
bnema · · 2026-09-23 05:50 UTC
committer
GitHub · · 2026-09-23 05:50 UTC
parent
f97971b
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
7 files · +201 -14
M go.mod +1 -1
@@ -7,7 +7,7 @@ require (
77 github.com/andybalholm/brotli v1.2.4
88 github.com/bnema/purego v0.11.0-bnema.4
99 github.com/bnema/purego-cef v0.14.2
10- github.com/bnema/purego-cef2gtk v0.11.1
10+ github.com/bnema/purego-cef2gtk v0.11.1-0.20260922042635-bb6ad3b440c6
1111 github.com/bnema/purego-pipewire v0.1.6
1212 github.com/bnema/purego-sqlite v0.1.5
1313 github.com/bnema/purego-webp v0.2.1
@@ -7,7 +7,7 @@ require (
7 github.com/andybalholm/brotli v1.2.47 github.com/andybalholm/brotli v1.2.4
8 github.com/bnema/purego v0.11.0-bnema.48 github.com/bnema/purego v0.11.0-bnema.4
9 github.com/bnema/purego-cef v0.14.29 github.com/bnema/purego-cef v0.14.2
10- github.com/bnema/purego-cef2gtk v0.11.110+ github.com/bnema/purego-cef2gtk v0.11.1-0.20260922042635-bb6ad3b440c6
11 github.com/bnema/purego-pipewire v0.1.611 github.com/bnema/purego-pipewire v0.1.6
12 github.com/bnema/purego-sqlite v0.1.512 github.com/bnema/purego-sqlite v0.1.5
13 github.com/bnema/purego-webp v0.2.113 github.com/bnema/purego-webp v0.2.1
M go.sum +2 -0 generated file: collapsed
A internal/infrastructure/cef/click_diagnostics.go +97 -0
@@ -0,0 +1,97 @@
1+package cef
2+
3+import (
4+ "fmt"
5+
6+ "github.com/bnema/dumber/internal/logging"
7+ cef2gtk "github.com/bnema/purego-cef2gtk"
8+ "github.com/rs/zerolog"
9+)
10+
11+func clickDiagnosticPhaseName(phase cef2gtk.ClickDiagnosticPhase) string {
12+ switch phase {
13+ case cef2gtk.ClickDiagnosticPressed:
14+ return "pressed"
15+ case cef2gtk.ClickDiagnosticReleased:
16+ return "released"
17+ case cef2gtk.ClickDiagnosticCancelled:
18+ return "canceled"
19+ case cef2gtk.ClickDiagnosticForwarded:
20+ return "forwarded"
21+ case cef2gtk.ClickDiagnosticConsumed:
22+ return "consumed"
23+ case cef2gtk.ClickDiagnosticDropped:
24+ return "dropped"
25+ default:
26+ return fmt.Sprintf("unknown(%d)", phase)
27+ }
28+}
29+
30+func clickDropReasonName(reason cef2gtk.ClickDropReason) string {
31+ switch reason {
32+ case cef2gtk.ClickDropReasonNone:
33+ return "none"
34+ case cef2gtk.ClickDropReasonMissingHost:
35+ return "missing_host"
36+ case cef2gtk.ClickDropReasonDetached:
37+ return "detached"
38+ case cef2gtk.ClickDropReasonMissingInputState:
39+ return "missing_input_state"
40+ default:
41+ return fmt.Sprintf("unknown(%d)", reason)
42+ }
43+}
44+
45+func clickButtonName(button uint) string {
46+ switch button {
47+ case 1:
48+ return "primary"
49+ case 2:
50+ return "middle"
51+ case 3:
52+ return "secondary"
53+ default:
54+ return "other"
55+ }
56+}
57+
58+func (wv *WebView) logClickDiagnostic(event cef2gtk.ClickDiagnosticEvent) {
59+ if wv == nil || wv.ctx == nil {
60+ return
61+ }
62+ wv.mu.RLock()
63+ browserID := int32(0)
64+ if wv.browser != nil {
65+ browserID = wv.browser.GetIdentifier()
66+ }
67+ inputAttached := wv.inputAttached
68+ visibilityKnown := wv.effectiveVisibilityKnown
69+ visible := wv.effectiveVisible
70+ bridge := wv.viewBridge
71+ bridgePresent := bridge != nil
72+ wv.mu.RUnlock()
73+ bridgeFocused := bridgePresent && bridge.HasFocus()
74+
75+ logger := logging.FromContext(wv.ctx)
76+ var logEvent *zerolog.Event
77+ if event.Phase == cef2gtk.ClickDiagnosticDropped || event.Phase == cef2gtk.ClickDiagnosticCancelled {
78+ logEvent = logger.Warn()
79+ } else {
80+ logEvent = logger.Debug()
81+ }
82+ logEvent.
83+ Uint64("webview_id", uint64(wv.id)).
84+ Int32("browser_id", browserID).
85+ Str("phase", clickDiagnosticPhaseName(event.Phase)).
86+ Uint("button", event.Button).
87+ Str("button_type", clickButtonName(event.Button)).
88+ Int("click_count", event.ClickCount).
89+ Str("drop_reason", clickDropReasonName(event.DropReason)).
90+ Bool("input_attached", inputAttached).
91+ Bool("effective_visibility_known", visibilityKnown).
92+ Bool("effective_visible", visible).
93+ Bool("destroyed", wv.destroyed.Load()).
94+ Bool("bridge_present", bridgePresent).
95+ Bool("bridge_focused", bridgeFocused).
96+ Msg("cef: click routing diagnostic")
97+}
@@ -0,0 +1,97 @@
1+package cef
2+
3+import (
4+ "fmt"
5+
6+ "github.com/bnema/dumber/internal/logging"
7+ cef2gtk "github.com/bnema/purego-cef2gtk"
8+ "github.com/rs/zerolog"
9+)
10+
11+func clickDiagnosticPhaseName(phase cef2gtk.ClickDiagnosticPhase) string {
12+ switch phase {
13+ case cef2gtk.ClickDiagnosticPressed:
14+ return "pressed"
15+ case cef2gtk.ClickDiagnosticReleased:
16+ return "released"
17+ case cef2gtk.ClickDiagnosticCancelled:
18+ return "canceled"
19+ case cef2gtk.ClickDiagnosticForwarded:
20+ return "forwarded"
21+ case cef2gtk.ClickDiagnosticConsumed:
22+ return "consumed"
23+ case cef2gtk.ClickDiagnosticDropped:
24+ return "dropped"
25+ default:
26+ return fmt.Sprintf("unknown(%d)", phase)
27+ }
28+}
29+
30+func clickDropReasonName(reason cef2gtk.ClickDropReason) string {
31+ switch reason {
32+ case cef2gtk.ClickDropReasonNone:
33+ return "none"
34+ case cef2gtk.ClickDropReasonMissingHost:
35+ return "missing_host"
36+ case cef2gtk.ClickDropReasonDetached:
37+ return "detached"
38+ case cef2gtk.ClickDropReasonMissingInputState:
39+ return "missing_input_state"
40+ default:
41+ return fmt.Sprintf("unknown(%d)", reason)
42+ }
43+}
44+
45+func clickButtonName(button uint) string {
46+ switch button {
47+ case 1:
48+ return "primary"
49+ case 2:
50+ return "middle"
51+ case 3:
52+ return "secondary"
53+ default:
54+ return "other"
55+ }
56+}
57+
58+func (wv *WebView) logClickDiagnostic(event cef2gtk.ClickDiagnosticEvent) {
59+ if wv == nil || wv.ctx == nil {
60+ return
61+ }
62+ wv.mu.RLock()
63+ browserID := int32(0)
64+ if wv.browser != nil {
65+ browserID = wv.browser.GetIdentifier()
66+ }
67+ inputAttached := wv.inputAttached
68+ visibilityKnown := wv.effectiveVisibilityKnown
69+ visible := wv.effectiveVisible
70+ bridge := wv.viewBridge
71+ bridgePresent := bridge != nil
72+ wv.mu.RUnlock()
73+ bridgeFocused := bridgePresent && bridge.HasFocus()
74+
75+ logger := logging.FromContext(wv.ctx)
76+ var logEvent *zerolog.Event
77+ if event.Phase == cef2gtk.ClickDiagnosticDropped || event.Phase == cef2gtk.ClickDiagnosticCancelled {
78+ logEvent = logger.Warn()
79+ } else {
80+ logEvent = logger.Debug()
81+ }
82+ logEvent.
83+ Uint64("webview_id", uint64(wv.id)).
84+ Int32("browser_id", browserID).
85+ Str("phase", clickDiagnosticPhaseName(event.Phase)).
86+ Uint("button", event.Button).
87+ Str("button_type", clickButtonName(event.Button)).
88+ Int("click_count", event.ClickCount).
89+ Str("drop_reason", clickDropReasonName(event.DropReason)).
90+ Bool("input_attached", inputAttached).
91+ Bool("effective_visibility_known", visibilityKnown).
92+ Bool("effective_visible", visible).
93+ Bool("destroyed", wv.destroyed.Load()).
94+ Bool("bridge_present", bridgePresent).
95+ Bool("bridge_focused", bridgeFocused).
96+ Msg("cef: click routing diagnostic")
97+}
A internal/infrastructure/cef/click_diagnostics_test.go +49 -0
@@ -0,0 +1,49 @@
1+package cef
2+
3+import (
4+ "testing"
5+
6+ cef2gtk "github.com/bnema/purego-cef2gtk"
7+)
8+
9+func TestClickDiagnosticNamesAreStable(t *testing.T) {
10+ phases := map[cef2gtk.ClickDiagnosticPhase]string{
11+ cef2gtk.ClickDiagnosticPressed: "pressed",
12+ cef2gtk.ClickDiagnosticReleased: "released",
13+ cef2gtk.ClickDiagnosticCancelled: "canceled",
14+ cef2gtk.ClickDiagnosticForwarded: "forwarded",
15+ cef2gtk.ClickDiagnosticConsumed: "consumed",
16+ cef2gtk.ClickDiagnosticDropped: "dropped",
17+ }
18+ for value, want := range phases {
19+ if got := clickDiagnosticPhaseName(value); got != want {
20+ t.Fatalf("phase %d: got %q, want %q", value, got, want)
21+ }
22+ }
23+ if got := clickDiagnosticPhaseName(99); got != "unknown(99)" {
24+ t.Fatalf("unknown phase: got %q", got)
25+ }
26+
27+ reasons := map[cef2gtk.ClickDropReason]string{
28+ cef2gtk.ClickDropReasonNone: "none",
29+ cef2gtk.ClickDropReasonMissingHost: "missing_host",
30+ cef2gtk.ClickDropReasonDetached: "detached",
31+ cef2gtk.ClickDropReasonMissingInputState: "missing_input_state",
32+ }
33+ for value, want := range reasons {
34+ if got := clickDropReasonName(value); got != want {
35+ t.Fatalf("reason %d: got %q, want %q", value, got, want)
36+ }
37+ }
38+ if got := clickDropReasonName(99); got != "unknown(99)" {
39+ t.Fatalf("unknown reason: got %q", got)
40+ }
41+}
42+
43+func TestClickButtonNamesArePrivacySafeAndBounded(t *testing.T) {
44+ for button, want := range map[uint]string{0: "other", 1: "primary", 2: "middle", 3: "secondary", 8: "other"} {
45+ if got := clickButtonName(button); got != want {
46+ t.Fatalf("button %d: got %q, want %q", button, got, want)
47+ }
48+ }
49+}
@@ -0,0 +1,49 @@
1+package cef
2+
3+import (
4+ "testing"
5+
6+ cef2gtk "github.com/bnema/purego-cef2gtk"
7+)
8+
9+func TestClickDiagnosticNamesAreStable(t *testing.T) {
10+ phases := map[cef2gtk.ClickDiagnosticPhase]string{
11+ cef2gtk.ClickDiagnosticPressed: "pressed",
12+ cef2gtk.ClickDiagnosticReleased: "released",
13+ cef2gtk.ClickDiagnosticCancelled: "canceled",
14+ cef2gtk.ClickDiagnosticForwarded: "forwarded",
15+ cef2gtk.ClickDiagnosticConsumed: "consumed",
16+ cef2gtk.ClickDiagnosticDropped: "dropped",
17+ }
18+ for value, want := range phases {
19+ if got := clickDiagnosticPhaseName(value); got != want {
20+ t.Fatalf("phase %d: got %q, want %q", value, got, want)
21+ }
22+ }
23+ if got := clickDiagnosticPhaseName(99); got != "unknown(99)" {
24+ t.Fatalf("unknown phase: got %q", got)
25+ }
26+
27+ reasons := map[cef2gtk.ClickDropReason]string{
28+ cef2gtk.ClickDropReasonNone: "none",
29+ cef2gtk.ClickDropReasonMissingHost: "missing_host",
30+ cef2gtk.ClickDropReasonDetached: "detached",
31+ cef2gtk.ClickDropReasonMissingInputState: "missing_input_state",
32+ }
33+ for value, want := range reasons {
34+ if got := clickDropReasonName(value); got != want {
35+ t.Fatalf("reason %d: got %q, want %q", value, got, want)
36+ }
37+ }
38+ if got := clickDropReasonName(99); got != "unknown(99)" {
39+ t.Fatalf("unknown reason: got %q", got)
40+ }
41+}
42+
43+func TestClickButtonNamesArePrivacySafeAndBounded(t *testing.T) {
44+ for button, want := range map[uint]string{0: "other", 1: "primary", 2: "middle", 3: "secondary", 8: "other"} {
45+ if got := clickButtonName(button); got != want {
46+ t.Fatalf("button %d: got %q, want %q", button, got, want)
47+ }
48+ }
49+}
M internal/infrastructure/cef/webview.go +2 -1
@@ -1737,7 +1737,8 @@ func (wv *WebView) bridgeInputOptions() cef2gtk.InputOptions {
17371737 OnMiddleClick: func(_, _ float64) bool {
17381738 return wv.handleMiddleClickFromBridge()
17391739 },
1740- OnScroll: wv.handleScrollInput,
1740+ OnClickDiagnostic: wv.logClickDiagnostic,
1741+ OnScroll: wv.handleScrollInput,
17411742 NavigationSwipe: cef2gtk.NavigationSwipeOptions{
17421743 // Dumber handles thresholding and progress UI locally from OnScroll so
17431744 // horizontal scrolling continues to reach CEF while history navigation
@@ -1737,7 +1737,8 @@ func (wv *WebView) bridgeInputOptions() cef2gtk.InputOptions {
1737 OnMiddleClick: func(_, _ float64) bool {1737 OnMiddleClick: func(_, _ float64) bool {
1738 return wv.handleMiddleClickFromBridge()1738 return wv.handleMiddleClickFromBridge()
1739 },1739 },
1740- OnScroll: wv.handleScrollInput,1740+ OnClickDiagnostic: wv.logClickDiagnostic,
1741+ OnScroll: wv.handleScrollInput,
1741 NavigationSwipe: cef2gtk.NavigationSwipeOptions{1742 NavigationSwipe: cef2gtk.NavigationSwipeOptions{
1742 // Dumber handles thresholding and progress UI locally from OnScroll so1743 // Dumber handles thresholding and progress UI locally from OnScroll so
1743 // horizontal scrolling continues to reach CEF while history navigation1744 // horizontal scrolling continues to reach CEF while history navigation
M internal/infrastructure/cef/webview_handlers.go +38 -4
@@ -990,21 +990,44 @@ func (h *handlerSet) attachAfterCreatedBrowser(
990990
991991 if bridge != nil {
992992 wv := h.wv
993+ log := logging.FromContext(wv.ctx)
994+ log.Debug().
995+ Uint64("webview_id", uint64(wv.id)).
996+ Int32("browser_id", browserID).
997+ Bool("destroyed", wv.destroyed.Load()).
998+ Bool("bridge_present", true).
999+ Msg("cef: scheduling input bridge attachment")
9931000 wv.runOnGTK(func() {
994- if wv.destroyed.Load() || wv.viewBridge == nil {
1001+ currentBridge := wv.viewBridge
1002+ log.Debug().
1003+ Uint64("webview_id", uint64(wv.id)).
1004+ Int32("browser_id", browserID).
1005+ Bool("destroyed", wv.destroyed.Load()).
1006+ Bool("bridge_present", currentBridge != nil).
1007+ Bool("bridge_stale", currentBridge != bridge).
1008+ Msg("cef: starting input bridge attachment")
1009+ if wv.destroyed.Load() || currentBridge == nil || currentBridge != bridge || bridge.IsDestroyed() {
9951010 wv.handleInputAttachFailure(ErrAdapterDestroyed, host)
9961011 return
9971012 }
998- if err := wv.viewBridge.AttachInputToWidget(host, wv.nativeWidget, wv.bridgeInputOptions()); err != nil {
1013+ if err := bridge.AttachInputToWidget(host, wv.nativeWidget, wv.bridgeInputOptions()); err != nil {
9991014 wv.handleInputAttachFailure(err, host)
10001015 return
10011016 }
1002- if wv.viewBridge != nil && wv.viewBridge.HasFocus() {
1017+ bridgeFocused := bridge.HasFocus()
1018+ if bridgeFocused {
10031019 syncWindowlessBrowserFocus(host)
10041020 } else {
10051021 host.Invalidate(purecef.PaintElementTypePetView)
10061022 }
10071023 wv.markInputAttached()
1024+ log.Debug().
1025+ Uint64("webview_id", uint64(wv.id)).
1026+ Int32("browser_id", browserID).
1027+ Bool("destroyed", wv.destroyed.Load()).
1028+ Bool("bridge_present", wv.viewBridge != nil).
1029+ Bool("bridge_focused", bridgeFocused).
1030+ Msg("cef: input bridge attachment succeeded")
10081031 })
10091032 }
10101033
@@ -1026,7 +1049,18 @@ func (wv *WebView) handleInputAttachFailure(err error, host purecef.BrowserHost)
10261049 return
10271050 }
10281051 if wv.ctx != nil {
1029- logging.FromContext(wv.ctx).Warn().Err(err).Msg("cef: failed to attach input to cef2gtk bridge")
1052+ browserID := int32(0)
1053+ if host != nil {
1054+ if browser := host.GetBrowser(); browser != nil {
1055+ browserID = browser.GetIdentifier()
1056+ }
1057+ }
1058+ logging.FromContext(wv.ctx).Warn().Err(err).
1059+ Uint64("webview_id", uint64(wv.id)).
1060+ Int32("browser_id", browserID).
1061+ Bool("destroyed", wv.destroyed.Load()).
1062+ Bool("bridge_present", wv.viewBridge != nil).
1063+ Msg("cef: input bridge attachment failed")
10301064 }
10311065 if host != nil && !wv.destroyed.Load() {
10321066 host.CloseBrowser(1)
@@ -990,21 +990,44 @@ func (h *handlerSet) attachAfterCreatedBrowser(
990 990
991 if bridge != nil {991 if bridge != nil {
992 wv := h.wv992 wv := h.wv
993+ log := logging.FromContext(wv.ctx)
994+ log.Debug().
995+ Uint64("webview_id", uint64(wv.id)).
996+ Int32("browser_id", browserID).
997+ Bool("destroyed", wv.destroyed.Load()).
998+ Bool("bridge_present", true).
999+ Msg("cef: scheduling input bridge attachment")
993 wv.runOnGTK(func() {1000 wv.runOnGTK(func() {
994- if wv.destroyed.Load() || wv.viewBridge == nil {1001+ currentBridge := wv.viewBridge
1002+ log.Debug().
1003+ Uint64("webview_id", uint64(wv.id)).
1004+ Int32("browser_id", browserID).
1005+ Bool("destroyed", wv.destroyed.Load()).
1006+ Bool("bridge_present", currentBridge != nil).
1007+ Bool("bridge_stale", currentBridge != bridge).
1008+ Msg("cef: starting input bridge attachment")
1009+ if wv.destroyed.Load() || currentBridge == nil || currentBridge != bridge || bridge.IsDestroyed() {
995 wv.handleInputAttachFailure(ErrAdapterDestroyed, host)1010 wv.handleInputAttachFailure(ErrAdapterDestroyed, host)
996 return1011 return
997 }1012 }
998- if err := wv.viewBridge.AttachInputToWidget(host, wv.nativeWidget, wv.bridgeInputOptions()); err != nil {1013+ if err := bridge.AttachInputToWidget(host, wv.nativeWidget, wv.bridgeInputOptions()); err != nil {
999 wv.handleInputAttachFailure(err, host)1014 wv.handleInputAttachFailure(err, host)
1000 return1015 return
1001 }1016 }
1002- if wv.viewBridge != nil && wv.viewBridge.HasFocus() {1017+ bridgeFocused := bridge.HasFocus()
1018+ if bridgeFocused {
1003 syncWindowlessBrowserFocus(host)1019 syncWindowlessBrowserFocus(host)
1004 } else {1020 } else {
1005 host.Invalidate(purecef.PaintElementTypePetView)1021 host.Invalidate(purecef.PaintElementTypePetView)
1006 }1022 }
1007 wv.markInputAttached()1023 wv.markInputAttached()
1024+ log.Debug().
1025+ Uint64("webview_id", uint64(wv.id)).
1026+ Int32("browser_id", browserID).
1027+ Bool("destroyed", wv.destroyed.Load()).
1028+ Bool("bridge_present", wv.viewBridge != nil).
1029+ Bool("bridge_focused", bridgeFocused).
1030+ Msg("cef: input bridge attachment succeeded")
1008 })1031 })
1009 }1032 }
1010 1033
@@ -1026,7 +1049,18 @@ func (wv *WebView) handleInputAttachFailure(err error, host purecef.BrowserHost)
1026 return1049 return
1027 }1050 }
1028 if wv.ctx != nil {1051 if wv.ctx != nil {
1029- logging.FromContext(wv.ctx).Warn().Err(err).Msg("cef: failed to attach input to cef2gtk bridge")1052+ browserID := int32(0)
1053+ if host != nil {
1054+ if browser := host.GetBrowser(); browser != nil {
1055+ browserID = browser.GetIdentifier()
1056+ }
1057+ }
1058+ logging.FromContext(wv.ctx).Warn().Err(err).
1059+ Uint64("webview_id", uint64(wv.id)).
1060+ Int32("browser_id", browserID).
1061+ Bool("destroyed", wv.destroyed.Load()).
1062+ Bool("bridge_present", wv.viewBridge != nil).
1063+ Msg("cef: input bridge attachment failed")
1030 }1064 }
1031 if host != nil && !wv.destroyed.Load() {1065 if host != nil && !wv.destroyed.Load() {
1032 host.CloseBrowser(1)1066 host.CloseBrowser(1)
M internal/ui/layout/stacked.go +12 -8
@@ -131,7 +131,7 @@ func (sv *StackedView) AddPane(ctx context.Context, paneID, title, faviconIconNa
131131 defer sv.mu.Unlock()
132132
133133 log.Debug().
134- Str("title", title).
134+ Str("pane_id", paneID).
135135 Bool("container_nil", container == nil).
136136 Int("current_count", len(sv.panes)).
137137 Msg("StackedView.AddPane called")
@@ -299,7 +299,7 @@ func (sv *StackedView) InsertPaneAfter(
299299 insertIndex := afterIndex + 1
300300
301301 log.Debug().
302- Str("title", title).
302+ Str("pane_id", paneID).
303303 Int("after_index", afterIndex).
304304 Int("insert_index", insertIndex).
305305 Int("current_count", len(sv.panes)).
@@ -478,22 +478,26 @@ func (sv *StackedView) updateVisibilityInternal(ctx context.Context) {
478478 if pane.titleBar != nil {
479479 pane.titleBar.SetVisible(!isActive)
480480 log.Debug().
481+ Str("pane_id", pane.paneID).
481482 Int("pane_index", i).
482- Str("title", pane.title).
483+ Int("active_index", sv.activeIndex).
484+ Int("pane_count", len(sv.panes)).
483485 Bool("is_active", isActive).
484- Bool("titlebar_visible", !isActive).
485- Msg("StackedView: set titlebar visibility")
486+ Bool("requested_visible", !isActive).
487+ Msg("StackedView: titlebar visibility changed")
486488 }
487489
488490 // Container is visible only for active pane
489491 if pane.container != nil {
490492 pane.container.SetVisible(isActive)
491493 log.Debug().
494+ Str("pane_id", pane.paneID).
492495 Int("pane_index", i).
493- Str("title", pane.title).
496+ Int("active_index", sv.activeIndex).
497+ Int("pane_count", len(sv.panes)).
494498 Bool("is_active", isActive).
495- Bool("container_visible", isActive).
496- Msg("StackedView: set container visibility")
499+ Bool("requested_visible", isActive).
500+ Msg("StackedView: container visibility changed")
497501 }
498502
499503 // Update CSS classes
@@ -131,7 +131,7 @@ func (sv *StackedView) AddPane(ctx context.Context, paneID, title, faviconIconNa
131 defer sv.mu.Unlock()131 defer sv.mu.Unlock()
132 132
133 log.Debug().133 log.Debug().
134- Str("title", title).134+ Str("pane_id", paneID).
135 Bool("container_nil", container == nil).135 Bool("container_nil", container == nil).
136 Int("current_count", len(sv.panes)).136 Int("current_count", len(sv.panes)).
137 Msg("StackedView.AddPane called")137 Msg("StackedView.AddPane called")
@@ -299,7 +299,7 @@ func (sv *StackedView) InsertPaneAfter(
299 insertIndex := afterIndex + 1299 insertIndex := afterIndex + 1
300 300
301 log.Debug().301 log.Debug().
302- Str("title", title).302+ Str("pane_id", paneID).
303 Int("after_index", afterIndex).303 Int("after_index", afterIndex).
304 Int("insert_index", insertIndex).304 Int("insert_index", insertIndex).
305 Int("current_count", len(sv.panes)).305 Int("current_count", len(sv.panes)).
@@ -478,22 +478,26 @@ func (sv *StackedView) updateVisibilityInternal(ctx context.Context) {
478 if pane.titleBar != nil {478 if pane.titleBar != nil {
479 pane.titleBar.SetVisible(!isActive)479 pane.titleBar.SetVisible(!isActive)
480 log.Debug().480 log.Debug().
481+ Str("pane_id", pane.paneID).
481 Int("pane_index", i).482 Int("pane_index", i).
482- Str("title", pane.title).483+ Int("active_index", sv.activeIndex).
484+ Int("pane_count", len(sv.panes)).
483 Bool("is_active", isActive).485 Bool("is_active", isActive).
484- Bool("titlebar_visible", !isActive).486+ Bool("requested_visible", !isActive).
485- Msg("StackedView: set titlebar visibility")487+ Msg("StackedView: titlebar visibility changed")
486 }488 }
487 489
488 // Container is visible only for active pane490 // Container is visible only for active pane
489 if pane.container != nil {491 if pane.container != nil {
490 pane.container.SetVisible(isActive)492 pane.container.SetVisible(isActive)
491 log.Debug().493 log.Debug().
494+ Str("pane_id", pane.paneID).
492 Int("pane_index", i).495 Int("pane_index", i).
493- Str("title", pane.title).496+ Int("active_index", sv.activeIndex).
497+ Int("pane_count", len(sv.panes)).
494 Bool("is_active", isActive).498 Bool("is_active", isActive).
495- Bool("container_visible", isActive).499+ Bool("requested_visible", isActive).
496- Msg("StackedView: set container visibility")500+ Msg("StackedView: container visibility changed")
497 }501 }
498 502
499 // Update CSS classes503 // Update CSS classes

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