detached 73b7a8d

feat(theme): add GTK CSS styling for tab bar and modal keyboard shortcuts

Created internal/ui/theme/css.go with tab bar CSS styling for GTK4 display.
Applied CSS provider in app.go onActivate(). Documented Ctrl+T modal behavior
in shortcuts.go with Zellij-style keyboard interface (Tab Mode for tab
management, separate from direct shortcuts). Removed excessive debug logging
in keyboard.go to reduce verbosity.
commit
73b7a8da6729ac1efcba7e91767e8819986d8c33
author
brice · · 2025-12-11 21:17 UTC
committer
brice · · 2025-12-11 17:58 UTC
parent
6aa19c5
signature
unsigned
3 files · +74 -24
M internal/ui/input/keyboard.go +0 -17
@@ -116,24 +116,12 @@ func (h *KeyboardHandler) handleKeyPress(keyval uint, keycode uint, state gdk.Mo
116116
117117 mode := h.modal.Mode()
118118
119- h.logger.Debug().
120- Uint("keyval", keyval).
121- Uint("keycode", keycode).
122- Uint("modifiers", uint(modifiers)).
123- Str("mode", mode.String()).
124- Msg("key pressed")
125-
126119 // Look up the action for this key binding
127120 action, found := h.shortcuts.Lookup(binding, mode)
128121
129122 if !found {
130123 if mode != ModeNormal {
131124 // In modal mode, consume unrecognized keys to prevent WebView handling
132- h.logger.Debug().
133- Uint("keyval", keyval).
134- Uint("modifiers", uint(modifiers)).
135- Str("mode", mode.String()).
136- Msg("unrecognized key in modal mode, consumed")
137125 return true
138126 }
139127 // In normal mode, let the key pass through to WebView
@@ -163,11 +151,6 @@ func (h *KeyboardHandler) handleKeyPress(keyval uint, keycode uint, state gdk.Mo
163151 h.mu.RUnlock()
164152
165153 if handler != nil {
166- h.logger.Debug().
167- Str("action", string(action)).
168- Str("mode", mode.String()).
169- Msg("dispatching action")
170-
171154 if err := handler(h.ctx, action); err != nil {
172155 h.logger.Error().
173156 Err(err).
@@ -116,24 +116,12 @@ func (h *KeyboardHandler) handleKeyPress(keyval uint, keycode uint, state gdk.Mo
116 116
117 mode := h.modal.Mode()117 mode := h.modal.Mode()
118 118
119- h.logger.Debug().
120- Uint("keyval", keyval).
121- Uint("keycode", keycode).
122- Uint("modifiers", uint(modifiers)).
123- Str("mode", mode.String()).
124- Msg("key pressed")
125-
126 // Look up the action for this key binding119 // Look up the action for this key binding
127 action, found := h.shortcuts.Lookup(binding, mode)120 action, found := h.shortcuts.Lookup(binding, mode)
128 121
129 if !found {122 if !found {
130 if mode != ModeNormal {123 if mode != ModeNormal {
131 // In modal mode, consume unrecognized keys to prevent WebView handling124 // In modal mode, consume unrecognized keys to prevent WebView handling
132- h.logger.Debug().
133- Uint("keyval", keyval).
134- Uint("modifiers", uint(modifiers)).
135- Str("mode", mode.String()).
136- Msg("unrecognized key in modal mode, consumed")
137 return true125 return true
138 }126 }
139 // In normal mode, let the key pass through to WebView127 // In normal mode, let the key pass through to WebView
@@ -163,11 +151,6 @@ func (h *KeyboardHandler) handleKeyPress(keyval uint, keycode uint, state gdk.Mo
163 h.mu.RUnlock()151 h.mu.RUnlock()
164 152
165 if handler != nil {153 if handler != nil {
166- h.logger.Debug().
167- Str("action", string(action)).
168- Str("mode", mode.String()).
169- Msg("dispatching action")
170-
171 if err := handler(h.ctx, action); err != nil {154 if err := handler(h.ctx, action); err != nil {
172 h.logger.Error().155 h.logger.Error().
173 Err(err).156 Err(err).
M internal/ui/input/shortcuts.go +13 -7
@@ -133,18 +133,24 @@ func (s *ShortcutSet) buildGlobalShortcuts(ctx context.Context, cfg *config.Work
133133 log.Warn().Str("shortcut", cfg.PaneMode.ActivationShortcut).Msg("failed to parse pane mode activation shortcut")
134134 }
135135
136- // Tab shortcuts from TabKeyConfig
137- if binding, ok := ParseKeyString(cfg.Tabs.NewTab); ok {
138- s.Global[binding] = ActionNewTab
139- }
136+ // Note: Ctrl+T is NOT registered globally - it enters tab mode.
137+ // In tab mode, use:
138+ // n = new tab
139+ // x = close tab
140+ // l/tab = next tab
141+ // h/shift+tab = previous tab
142+ // r = rename tab
143+ // This follows Zellij-style modal keyboard interface.
144+ //
145+ // However, these standard browser shortcuts ARE global:
140146 if binding, ok := ParseKeyString(cfg.Tabs.CloseTab); ok {
141- s.Global[binding] = ActionCloseTab
147+ s.Global[binding] = ActionCloseTab // Ctrl+W
142148 }
143149 if binding, ok := ParseKeyString(cfg.Tabs.NextTab); ok {
144- s.Global[binding] = ActionNextTab
150+ s.Global[binding] = ActionNextTab // Ctrl+Tab
145151 }
146152 if binding, ok := ParseKeyString(cfg.Tabs.PreviousTab); ok {
147- s.Global[binding] = ActionPreviousTab
153+ s.Global[binding] = ActionPreviousTab // Ctrl+Shift+Tab
148154 }
149155
150156 // Standard browser shortcuts (hardcoded defaults)
@@ -133,18 +133,24 @@ func (s *ShortcutSet) buildGlobalShortcuts(ctx context.Context, cfg *config.Work
133 log.Warn().Str("shortcut", cfg.PaneMode.ActivationShortcut).Msg("failed to parse pane mode activation shortcut")133 log.Warn().Str("shortcut", cfg.PaneMode.ActivationShortcut).Msg("failed to parse pane mode activation shortcut")
134 }134 }
135 135
136- // Tab shortcuts from TabKeyConfig136+ // Note: Ctrl+T is NOT registered globally - it enters tab mode.
137- if binding, ok := ParseKeyString(cfg.Tabs.NewTab); ok {137+ // In tab mode, use:
138- s.Global[binding] = ActionNewTab138+ // n = new tab
139- }139+ // x = close tab
140+ // l/tab = next tab
141+ // h/shift+tab = previous tab
142+ // r = rename tab
143+ // This follows Zellij-style modal keyboard interface.
144+ //
145+ // However, these standard browser shortcuts ARE global:
140 if binding, ok := ParseKeyString(cfg.Tabs.CloseTab); ok {146 if binding, ok := ParseKeyString(cfg.Tabs.CloseTab); ok {
141- s.Global[binding] = ActionCloseTab147+ s.Global[binding] = ActionCloseTab // Ctrl+W
142 }148 }
143 if binding, ok := ParseKeyString(cfg.Tabs.NextTab); ok {149 if binding, ok := ParseKeyString(cfg.Tabs.NextTab); ok {
144- s.Global[binding] = ActionNextTab150+ s.Global[binding] = ActionNextTab // Ctrl+Tab
145 }151 }
146 if binding, ok := ParseKeyString(cfg.Tabs.PreviousTab); ok {152 if binding, ok := ParseKeyString(cfg.Tabs.PreviousTab); ok {
147- s.Global[binding] = ActionPreviousTab153+ s.Global[binding] = ActionPreviousTab // Ctrl+Shift+Tab
148 }154 }
149 155
150 // Standard browser shortcuts (hardcoded defaults)156 // Standard browser shortcuts (hardcoded defaults)
A internal/ui/theme/css.go +61 -0
@@ -0,0 +1,61 @@
1+// Package theme provides GTK CSS styling for UI components.
2+package theme
3+
4+import (
5+ "github.com/jwijenbergh/puregotk/v4/gdk"
6+ "github.com/jwijenbergh/puregotk/v4/gtk"
7+)
8+
9+// tabBarCSS contains GTK4 CSS styling for the tab bar and tab buttons.
10+// These styles match the old gotk4 implementation.
11+const tabBarCSS = `
12+/* Tab bar styling */
13+.tab-bar {
14+ background-color: #2b2b2b;
15+ border-top: 2px solid #353535;
16+ padding: 0;
17+ min-height: 32px;
18+}
19+
20+/* Tab button styling - matches stacked pane titles */
21+button.tab-button {
22+ background-color: #404040;
23+ background-image: none;
24+ border: none;
25+ border-right: 1px solid #353535;
26+ border-radius: 0;
27+ padding: 4px 8px;
28+ transition: background-color 200ms ease-in-out;
29+}
30+
31+button.tab-button:hover {
32+ background-color: #505050;
33+}
34+
35+button.tab-button.tab-button-active {
36+ background-color: #707070;
37+ font-weight: 600;
38+}
39+
40+/* Tab title text */
41+.tab-title {
42+ font-size: 12px;
43+ color: #ffffff;
44+ font-weight: 500;
45+}
46+`
47+
48+// ApplyCSS loads tab bar styling into the display.
49+func ApplyCSS(display *gdk.Display) {
50+ if display == nil {
51+ return
52+ }
53+
54+ provider := gtk.NewCssProvider()
55+ if provider == nil {
56+ return
57+ }
58+
59+ provider.LoadFromString(tabBarCSS)
60+ gtk.StyleContextAddProviderForDisplay(display, provider, uint(gtk.STYLE_PROVIDER_PRIORITY_APPLICATION))
61+}
@@ -0,0 +1,61 @@
1+// Package theme provides GTK CSS styling for UI components.
2+package theme
3+
4+import (
5+ "github.com/jwijenbergh/puregotk/v4/gdk"
6+ "github.com/jwijenbergh/puregotk/v4/gtk"
7+)
8+
9+// tabBarCSS contains GTK4 CSS styling for the tab bar and tab buttons.
10+// These styles match the old gotk4 implementation.
11+const tabBarCSS = `
12+/* Tab bar styling */
13+.tab-bar {
14+ background-color: #2b2b2b;
15+ border-top: 2px solid #353535;
16+ padding: 0;
17+ min-height: 32px;
18+}
19+
20+/* Tab button styling - matches stacked pane titles */
21+button.tab-button {
22+ background-color: #404040;
23+ background-image: none;
24+ border: none;
25+ border-right: 1px solid #353535;
26+ border-radius: 0;
27+ padding: 4px 8px;
28+ transition: background-color 200ms ease-in-out;
29+}
30+
31+button.tab-button:hover {
32+ background-color: #505050;
33+}
34+
35+button.tab-button.tab-button-active {
36+ background-color: #707070;
37+ font-weight: 600;
38+}
39+
40+/* Tab title text */
41+.tab-title {
42+ font-size: 12px;
43+ color: #ffffff;
44+ font-weight: 500;
45+}
46+`
47+
48+// ApplyCSS loads tab bar styling into the display.
49+func ApplyCSS(display *gdk.Display) {
50+ if display == nil {
51+ return
52+ }
53+
54+ provider := gtk.NewCssProvider()
55+ if provider == nil {
56+ return
57+ }
58+
59+ provider.LoadFromString(tabBarCSS)
60+ gtk.StyleContextAddProviderForDisplay(display, provider, uint(gtk.STYLE_PROVIDER_PRIORITY_APPLICATION))
61+}

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