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 | |||
| 116 | 116 | ||
| 117 | 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 | 119 | // Look up the action for this key binding | |
| 127 | 120 | action, found := h.shortcuts.Lookup(binding, mode) | |
| 128 | 121 | ||
| 129 | 122 | if !found { | |
| 130 | 123 | if mode != ModeNormal { | |
| 131 | 124 | // 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 | 125 | return true | |
| 138 | 126 | } | |
| 139 | 127 | // 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 | 151 | h.mu.RUnlock() | |
| 164 | 152 | ||
| 165 | 153 | if handler != nil { | |
| 166 | - | h.logger.Debug(). | |
| 167 | - | Str("action", string(action)). | |
| 168 | - | Str("mode", mode.String()). | |
| 169 | - | Msg("dispatching action") | |
| 170 | - | ||
| 171 | 154 | if err := handler(h.ctx, action); err != nil { | |
| 172 | 155 | h.logger.Error(). | |
| 173 | 156 | 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 binding | 119 | // 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 handling | 124 | // 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 true | 125 | return true | ||
| 138 | } | 126 | } | ||
| 139 | // In normal mode, let the key pass through to WebView | 127 | // 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 | |||
| 133 | 133 | log.Warn().Str("shortcut", cfg.PaneMode.ActivationShortcut).Msg("failed to parse pane mode activation shortcut") | |
| 134 | 134 | } | |
| 135 | 135 | ||
| 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: | |
| 140 | 146 | if binding, ok := ParseKeyString(cfg.Tabs.CloseTab); ok { | |
| 141 | - | s.Global[binding] = ActionCloseTab | |
| 147 | + | s.Global[binding] = ActionCloseTab // Ctrl+W | |
| 142 | 148 | } | |
| 143 | 149 | if binding, ok := ParseKeyString(cfg.Tabs.NextTab); ok { | |
| 144 | - | s.Global[binding] = ActionNextTab | |
| 150 | + | s.Global[binding] = ActionNextTab // Ctrl+Tab | |
| 145 | 151 | } | |
| 146 | 152 | if binding, ok := ParseKeyString(cfg.Tabs.PreviousTab); ok { | |
| 147 | - | s.Global[binding] = ActionPreviousTab | |
| 153 | + | s.Global[binding] = ActionPreviousTab // Ctrl+Shift+Tab | |
| 148 | 154 | } | |
| 149 | 155 | ||
| 150 | 156 | // 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 TabKeyConfig | 136 | + | // 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] = ActionNewTab | 138 | + | // 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] = ActionCloseTab | 147 | + | 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] = ActionNextTab | 150 | + | 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] = ActionPreviousTab | 153 | + | 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 | + | } | |||