fix(tabs): ctrl+tab navigation and international keyboard support
- Add Ctrl+Tab/Ctrl+Shift+Tab for tab switching via EventControllerKey - Fix active tab styling with distinct gray shades and GTK CSS compliance - Add hardware keycode support for Alt+number on AZERTY/QWERTZ layouts - Fix keyboard layout detection prioritizing actual layout over LANG
- commit
- 6b46846c2cc2b0e8392c43f6e2fc66e0d62696d2
- author
- bnema · · 2025-11-17 20:26 UTC
- parent
- 7bbc91a
- signature
- unsigned
6 files · +171 -42
M CHANGELOG.md +6 -1
| @@ -5,7 +5,8 @@ All notable changes to this project will be documented in this file. | |||
| 5 | 5 | ## [Unreleased] | |
| 6 | 6 | ||
| 7 | 7 | ### Added | |
| 8 | - | - **Zellij-style tab system**: Each tab contains an independent workspace with panes, splits, and stacks. `Ctrl+T` enters tab mode with modal keyboard control (`n`=new, `x`=close, `l/h`=navigate). `Alt+1-9/0` for direct tab switching. Tab bar auto-hides with single tab. Orange border indicator (distinct from blue pane mode). | |
| 8 | + | - **Zellij-style tab system**: Each tab contains an independent workspace with panes, splits, and stacks. `Ctrl+T` enters tab mode with modal keyboard control (`n`=new, `x`=close, `l/h`=navigate). `Alt+1-9/0` for direct tab switching. `Ctrl+Tab`/`Ctrl+Shift+Tab` for next/previous tab. Tab bar auto-hides with single tab. Orange border indicator (distinct from blue pane mode). | |
| 9 | + | - **International keyboard layout support**: Hardware keycode-based shortcuts for `Alt+number` keys work across all keyboard layouts (QWERTY, AZERTY, QWERTZ, etc.). Physical key position used instead of character mapping. | |
| 9 | 10 | - **Click-to-activate stacked panes**: Click on any collapsed title bar to instantly activate that pane. Uses GTK4 gesture clicks for native interaction feel. | |
| 10 | 11 | - **Title bar visual separators**: Subtle 1px borders between stacked title bars. Theme-aware colors automatically adjust for light/dark mode. | |
| 11 | 12 | - **UI scaling for title bars**: New `workspace.styling.ui_scale` config option (default: 1.0). Scales font size, padding, height, and favicon size. Range: 0.5 to 3.0 (e.g., 1.2 = 120% size). | |
| @@ -24,6 +25,10 @@ All notable changes to this project will be documented in this file. | |||
| 24 | 25 | - **Smart Escape key**: First press clears input text, second press closes omnibox. Empty input closes immediately. | |
| 25 | 26 | ||
| 26 | 27 | ### Fixed | |
| 28 | + | - **Ctrl+Tab shortcuts**: Fixed focus cycling behavior, now properly switches between tabs | |
| 29 | + | - **Active tab visibility**: Fixed identical styling - active tab now clearly visible with distinct gray shade | |
| 30 | + | - **AZERTY keyboard support**: Alt+number shortcuts now work on all keyboard layouts using hardware keycodes | |
| 31 | + | - **Keyboard layout detection**: Fixed always detecting "en" - now correctly detects fr/de/etc layouts | |
| 27 | 32 | - **Zoom visual glitch**: Fixed zoom level for next page being applied before navigation completed, causing visible zoom change on current page. Zoom now applies on load-committed event instead of URI change. | |
| 28 | 33 | - **Toast notifications**: Fixed toast notifications not appearing for copy URL (Ctrl+Shift+C) and clipboard operations. Event name was `dumber:showToast` but ToastContainer listens for `dumber:toast`. | |
| 29 | 34 | - **Page reload shortcuts**: Restored `Ctrl+R`, `Ctrl+Shift+R`, `F5`, and `Ctrl+F5` shortcuts. They were registered but not actually wired to the window-level handler. | |
| @@ -5,7 +5,8 @@ All notable changes to this project will be documented in this file. | |||||
| 5 | ## [Unreleased] | 5 | ## [Unreleased] | ||
| 6 | 6 | ||||
| 7 | ### Added | 7 | ### Added | ||
| 8 | - | - **Zellij-style tab system**: Each tab contains an independent workspace with panes, splits, and stacks. `Ctrl+T` enters tab mode with modal keyboard control (`n`=new, `x`=close, `l/h`=navigate). `Alt+1-9/0` for direct tab switching. Tab bar auto-hides with single tab. Orange border indicator (distinct from blue pane mode). | 8 | + | - **Zellij-style tab system**: Each tab contains an independent workspace with panes, splits, and stacks. `Ctrl+T` enters tab mode with modal keyboard control (`n`=new, `x`=close, `l/h`=navigate). `Alt+1-9/0` for direct tab switching. `Ctrl+Tab`/`Ctrl+Shift+Tab` for next/previous tab. Tab bar auto-hides with single tab. Orange border indicator (distinct from blue pane mode). |
| 9 | + | - **International keyboard layout support**: Hardware keycode-based shortcuts for `Alt+number` keys work across all keyboard layouts (QWERTY, AZERTY, QWERTZ, etc.). Physical key position used instead of character mapping. | |||
| 9 | - **Click-to-activate stacked panes**: Click on any collapsed title bar to instantly activate that pane. Uses GTK4 gesture clicks for native interaction feel. | 10 | - **Click-to-activate stacked panes**: Click on any collapsed title bar to instantly activate that pane. Uses GTK4 gesture clicks for native interaction feel. | ||
| 10 | - **Title bar visual separators**: Subtle 1px borders between stacked title bars. Theme-aware colors automatically adjust for light/dark mode. | 11 | - **Title bar visual separators**: Subtle 1px borders between stacked title bars. Theme-aware colors automatically adjust for light/dark mode. | ||
| 11 | - **UI scaling for title bars**: New `workspace.styling.ui_scale` config option (default: 1.0). Scales font size, padding, height, and favicon size. Range: 0.5 to 3.0 (e.g., 1.2 = 120% size). | 12 | - **UI scaling for title bars**: New `workspace.styling.ui_scale` config option (default: 1.0). Scales font size, padding, height, and favicon size. Range: 0.5 to 3.0 (e.g., 1.2 = 120% size). | ||
| @@ -24,6 +25,10 @@ All notable changes to this project will be documented in this file. | |||||
| 24 | - **Smart Escape key**: First press clears input text, second press closes omnibox. Empty input closes immediately. | 25 | - **Smart Escape key**: First press clears input text, second press closes omnibox. Empty input closes immediately. | ||
| 25 | 26 | ||||
| 26 | ### Fixed | 27 | ### Fixed | ||
| 28 | + | - **Ctrl+Tab shortcuts**: Fixed focus cycling behavior, now properly switches between tabs | |||
| 29 | + | - **Active tab visibility**: Fixed identical styling - active tab now clearly visible with distinct gray shade | |||
| 30 | + | - **AZERTY keyboard support**: Alt+number shortcuts now work on all keyboard layouts using hardware keycodes | |||
| 31 | + | - **Keyboard layout detection**: Fixed always detecting "en" - now correctly detects fr/de/etc layouts | |||
| 27 | - **Zoom visual glitch**: Fixed zoom level for next page being applied before navigation completed, causing visible zoom change on current page. Zoom now applies on load-committed event instead of URI change. | 32 | - **Zoom visual glitch**: Fixed zoom level for next page being applied before navigation completed, causing visible zoom change on current page. Zoom now applies on load-committed event instead of URI change. | ||
| 28 | - **Toast notifications**: Fixed toast notifications not appearing for copy URL (Ctrl+Shift+C) and clipboard operations. Event name was `dumber:showToast` but ToastContainer listens for `dumber:toast`. | 33 | - **Toast notifications**: Fixed toast notifications not appearing for copy URL (Ctrl+Shift+C) and clipboard operations. Event name was `dumber:showToast` but ToastContainer listens for `dumber:toast`. | ||
| 29 | - **Page reload shortcuts**: Restored `Ctrl+R`, `Ctrl+Shift+R`, `F5`, and `Ctrl+F5` shortcuts. They were registered but not actually wired to the window-level handler. | 34 | - **Page reload shortcuts**: Restored `Ctrl+R`, `Ctrl+Shift+R`, `F5`, and `Ctrl+F5` shortcuts. They were registered but not actually wired to the window-level handler. | ||
M internal/app/browser/tab_bar.go +3 -0
| @@ -193,14 +193,17 @@ func (tm *TabManager) removeTabFromBar(tab *Tab) { | |||
| 193 | 193 | // setTabActiveStyle applies or removes the active CSS class on a tab button. | |
| 194 | 194 | func (tm *TabManager) setTabActiveStyle(tab *Tab, active bool) { | |
| 195 | 195 | if tab.titleButton == nil { | |
| 196 | + | logging.Warn(fmt.Sprintf("[tabs] Cannot set active style: button is nil for tab %s", tab.id)) | |
| 196 | 197 | return | |
| 197 | 198 | } | |
| 198 | 199 | ||
| 199 | 200 | webkit.RunOnMainThread(func() { | |
| 200 | 201 | if active { | |
| 201 | 202 | webkit.WidgetAddCSSClass(tab.titleButton, "tab-button-active") | |
| 203 | + | logging.Debug(fmt.Sprintf("[tabs] Added active class to tab %s", tab.id)) | |
| 202 | 204 | } else { | |
| 203 | 205 | webkit.WidgetRemoveCSSClass(tab.titleButton, "tab-button-active") | |
| 206 | + | logging.Debug(fmt.Sprintf("[tabs] Removed active class from tab %s", tab.id)) | |
| 204 | 207 | } | |
| 205 | 208 | }) | |
| 206 | 209 | } | |
| @@ -193,14 +193,17 @@ func (tm *TabManager) removeTabFromBar(tab *Tab) { | |||||
| 193 | // setTabActiveStyle applies or removes the active CSS class on a tab button. | 193 | // setTabActiveStyle applies or removes the active CSS class on a tab button. | ||
| 194 | func (tm *TabManager) setTabActiveStyle(tab *Tab, active bool) { | 194 | func (tm *TabManager) setTabActiveStyle(tab *Tab, active bool) { | ||
| 195 | if tab.titleButton == nil { | 195 | if tab.titleButton == nil { | ||
| 196 | + | logging.Warn(fmt.Sprintf("[tabs] Cannot set active style: button is nil for tab %s", tab.id)) | |||
| 196 | return | 197 | return | ||
| 197 | } | 198 | } | ||
| 198 | 199 | ||||
| 199 | webkit.RunOnMainThread(func() { | 200 | webkit.RunOnMainThread(func() { | ||
| 200 | if active { | 201 | if active { | ||
| 201 | webkit.WidgetAddCSSClass(tab.titleButton, "tab-button-active") | 202 | webkit.WidgetAddCSSClass(tab.titleButton, "tab-button-active") | ||
| 203 | + | logging.Debug(fmt.Sprintf("[tabs] Added active class to tab %s", tab.id)) | |||
| 202 | } else { | 204 | } else { | ||
| 203 | webkit.WidgetRemoveCSSClass(tab.titleButton, "tab-button-active") | 205 | webkit.WidgetRemoveCSSClass(tab.titleButton, "tab-button-active") | ||
| 206 | + | logging.Debug(fmt.Sprintf("[tabs] Removed active class from tab %s", tab.id)) | |||
| 204 | } | 207 | } | ||
| 205 | }) | 208 | }) | ||
| 206 | } | 209 | } | ||
M internal/app/browser/window_shortcuts.go +107 -11
| @@ -8,6 +8,8 @@ import ( | |||
| 8 | 8 | ||
| 9 | 9 | "github.com/bnema/dumber/internal/app/messaging" | |
| 10 | 10 | "github.com/bnema/dumber/pkg/webkit" | |
| 11 | + | "github.com/diamondburned/gotk4/pkg/gdk/v4" | |
| 12 | + | "github.com/diamondburned/gotk4/pkg/gtk/v4" | |
| 11 | 13 | ) | |
| 12 | 14 | ||
| 13 | 15 | // WindowShortcutHandler manages global shortcuts at the window level | |
| @@ -47,6 +49,11 @@ func (h *WindowShortcutHandler) initialize() error { | |||
| 47 | 49 | return ErrFailedToInitialize | |
| 48 | 50 | } | |
| 49 | 51 | ||
| 52 | + | // Register hardware keycode-based shortcuts (for AZERTY/international keyboards) | |
| 53 | + | if err := h.registerKeycodeShortcuts(); err != nil { | |
| 54 | + | log.Printf("[window-shortcuts] Failed to register keycode shortcuts: %v", err) | |
| 55 | + | } | |
| 56 | + | ||
| 50 | 57 | return h.registerGlobalShortcuts() | |
| 51 | 58 | } | |
| 52 | 59 | ||
| @@ -65,6 +72,8 @@ func (h *WindowShortcutHandler) registerGlobalShortcuts() error { | |||
| 65 | 72 | {"ctrl+p", h.handlePaneMode, "Enter pane mode"}, | |
| 66 | 73 | // Tab management | |
| 67 | 74 | {"ctrl+t", h.handleTabMode, "Enter tab mode"}, | |
| 75 | + | // Global tab navigation (Ctrl+Tab) is handled via EventControllerKey in registerKeycodeShortcuts | |
| 76 | + | // This prevents GTK's default focus cycling behavior | |
| 68 | 77 | // Tab mode actions (only active when tab mode is enabled) | |
| 69 | 78 | {"n", func() { h.handleTabModeAction("new-tab") }, "New tab (tab mode)"}, | |
| 70 | 79 | {"c", func() { h.handleTabModeAction("new-tab") }, "New tab (tab mode)"}, | |
| @@ -76,17 +85,8 @@ func (h *WindowShortcutHandler) registerGlobalShortcuts() error { | |||
| 76 | 85 | {"shift+Tab", func() { h.handleTabModeAction("previous-tab") }, "Previous tab (tab mode)"}, | |
| 77 | 86 | {"Return", func() { h.handleTabModeAction("confirm") }, "Confirm (tab mode)"}, | |
| 78 | 87 | {"Escape", func() { h.handleTabModeAction("cancel") }, "Cancel (tab mode)"}, | |
| 79 | - | // Direct tab switching | |
| 80 | - | {"alt+1", func() { h.handleDirectTabSwitch(0) }, "Switch to tab 1"}, | |
| 81 | - | {"alt+2", func() { h.handleDirectTabSwitch(1) }, "Switch to tab 2"}, | |
| 82 | - | {"alt+3", func() { h.handleDirectTabSwitch(2) }, "Switch to tab 3"}, | |
| 83 | - | {"alt+4", func() { h.handleDirectTabSwitch(3) }, "Switch to tab 4"}, | |
| 84 | - | {"alt+5", func() { h.handleDirectTabSwitch(4) }, "Switch to tab 5"}, | |
| 85 | - | {"alt+6", func() { h.handleDirectTabSwitch(5) }, "Switch to tab 6"}, | |
| 86 | - | {"alt+7", func() { h.handleDirectTabSwitch(6) }, "Switch to tab 7"}, | |
| 87 | - | {"alt+8", func() { h.handleDirectTabSwitch(7) }, "Switch to tab 8"}, | |
| 88 | - | {"alt+9", func() { h.handleDirectTabSwitch(8) }, "Switch to tab 9"}, | |
| 89 | - | {"alt+0", func() { h.handleDirectTabSwitch(9) }, "Switch to tab 10"}, | |
| 88 | + | // Direct tab switching is now handled via hardware keycodes (see registerKeycodeShortcuts) | |
| 89 | + | // This supports all keyboard layouts (QWERTY, AZERTY, etc.) | |
| 90 | 90 | // Page reload shortcuts | |
| 91 | 91 | {"ctrl+r", h.handleReload, "Reload page"}, | |
| 92 | 92 | {"ctrl+shift+r", h.handleHardReload, "Hard reload (bypass cache)"}, | |
| @@ -118,6 +118,76 @@ func (h *WindowShortcutHandler) registerGlobalShortcuts() error { | |||
| 118 | 118 | return nil | |
| 119 | 119 | } | |
| 120 | 120 | ||
| 121 | + | // registerKeycodeShortcuts registers keyboard shortcuts based on hardware keycodes | |
| 122 | + | // This allows Alt+number shortcuts to work on AZERTY and other keyboard layouts | |
| 123 | + | func (h *WindowShortcutHandler) registerKeycodeShortcuts() error { | |
| 124 | + | if h.window == nil { | |
| 125 | + | return fmt.Errorf("window is nil") | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | gtkWindow := h.window.AsWindow() | |
| 129 | + | if gtkWindow == nil { | |
| 130 | + | return fmt.Errorf("gtk window is nil") | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | // Create event controller for keyboard events | |
| 134 | + | keyController := gtk.NewEventControllerKey() | |
| 135 | + | keyController.SetPropagationPhase(gtk.PhaseCapture) | |
| 136 | + | ||
| 137 | + | // GDK key constants | |
| 138 | + | const ( | |
| 139 | + | gdkKeyTab = 0xff09 | |
| 140 | + | ) | |
| 141 | + | ||
| 142 | + | // Map hardware keycodes to tab indices | |
| 143 | + | // These keycodes are the same across all keyboard layouts | |
| 144 | + | keycodeToTab := map[uint]int{ | |
| 145 | + | 10: 0, // 1/& key | |
| 146 | + | 11: 1, // 2/é key | |
| 147 | + | 12: 2, // 3/" key | |
| 148 | + | 13: 3, // 4/' key | |
| 149 | + | 14: 4, // 5/( key | |
| 150 | + | 15: 5, // 6/- key | |
| 151 | + | 16: 6, // 7/è key | |
| 152 | + | 17: 7, // 8/_ key | |
| 153 | + | 18: 8, // 9/ç key | |
| 154 | + | 19: 9, // 0/à key | |
| 155 | + | } | |
| 156 | + | ||
| 157 | + | keyController.ConnectKeyPressed(func(keyval, keycode uint, state gdk.ModifierType) bool { | |
| 158 | + | // Handle Ctrl+Tab for next tab | |
| 159 | + | if keyval == gdkKeyTab && state.Has(gdk.ControlMask) && !state.Has(gdk.ShiftMask) { | |
| 160 | + | log.Printf("[window-shortcuts] Ctrl+Tab -> next tab") | |
| 161 | + | h.handleNextTab() | |
| 162 | + | return true // Consume event to prevent focus cycling | |
| 163 | + | } | |
| 164 | + | ||
| 165 | + | // Handle Ctrl+Shift+Tab for previous tab | |
| 166 | + | if keyval == gdkKeyTab && state.Has(gdk.ControlMask) && state.Has(gdk.ShiftMask) { | |
| 167 | + | log.Printf("[window-shortcuts] Ctrl+Shift+Tab -> previous tab") | |
| 168 | + | h.handlePrevTab() | |
| 169 | + | return true // Consume event to prevent focus cycling | |
| 170 | + | } | |
| 171 | + | ||
| 172 | + | // Check for Alt+number (but not with Ctrl or Shift) | |
| 173 | + | if state.Has(gdk.AltMask) && !state.Has(gdk.ControlMask) && !state.Has(gdk.ShiftMask) { | |
| 174 | + | // Check if this is a number key | |
| 175 | + | if tabIndex, ok := keycodeToTab[keycode]; ok { | |
| 176 | + | log.Printf("[window-shortcuts] Hardware keycode shortcut: Alt+keycode(%d) -> tab %d", keycode, tabIndex) | |
| 177 | + | h.handleDirectTabSwitch(tabIndex) | |
| 178 | + | return true // Handled | |
| 179 | + | } | |
| 180 | + | } | |
| 181 | + | ||
| 182 | + | return false | |
| 183 | + | }) | |
| 184 | + | ||
| 185 | + | gtkWindow.AddController(keyController) | |
| 186 | + | log.Printf("[window-shortcuts] Registered hardware keycode shortcuts for Alt+number (AZERTY support)") | |
| 187 | + | ||
| 188 | + | return nil | |
| 189 | + | } | |
| 190 | + | ||
| 121 | 191 | func (h *WindowShortcutHandler) handleOmniboxToggle() { | |
| 122 | 192 | h.handleUIToggle(&h.lastOmniboxToggle, "omnibox", "omnibox-nav-toggle") | |
| 123 | 193 | } | |
| @@ -455,6 +525,32 @@ func (h *WindowShortcutHandler) handleTabMode() { | |||
| 455 | 525 | h.app.tabManager.EnterTabMode() | |
| 456 | 526 | } | |
| 457 | 527 | ||
| 528 | + | // handleNextTab switches to the next tab. | |
| 529 | + | func (h *WindowShortcutHandler) handleNextTab() { | |
| 530 | + | if h.app == nil || h.app.tabManager == nil { | |
| 531 | + | log.Printf("[window-shortcuts] Cannot switch tab: tab manager not available") | |
| 532 | + | return | |
| 533 | + | } | |
| 534 | + | ||
| 535 | + | log.Printf("[window-shortcuts] Switching to next tab") | |
| 536 | + | if err := h.app.tabManager.NextTab(); err != nil { | |
| 537 | + | log.Printf("[window-shortcuts] Failed to switch to next tab: %v", err) | |
| 538 | + | } | |
| 539 | + | } | |
| 540 | + | ||
| 541 | + | // handlePrevTab switches to the previous tab. | |
| 542 | + | func (h *WindowShortcutHandler) handlePrevTab() { | |
| 543 | + | if h.app == nil || h.app.tabManager == nil { | |
| 544 | + | log.Printf("[window-shortcuts] Cannot switch tab: tab manager not available") | |
| 545 | + | return | |
| 546 | + | } | |
| 547 | + | ||
| 548 | + | log.Printf("[window-shortcuts] Switching to previous tab") | |
| 549 | + | if err := h.app.tabManager.PreviousTab(); err != nil { | |
| 550 | + | log.Printf("[window-shortcuts] Failed to switch to previous tab: %v", err) | |
| 551 | + | } | |
| 552 | + | } | |
| 553 | + | ||
| 458 | 554 | // handleDirectTabSwitch switches to a specific tab by index (0-based). | |
| 459 | 555 | func (h *WindowShortcutHandler) handleDirectTabSwitch(index int) { | |
| 460 | 556 | if h.app == nil || h.app.tabManager == nil { | |
| @@ -8,6 +8,8 @@ import ( | |||||
| 8 | 8 | ||||
| 9 | "github.com/bnema/dumber/internal/app/messaging" | 9 | "github.com/bnema/dumber/internal/app/messaging" | ||
| 10 | "github.com/bnema/dumber/pkg/webkit" | 10 | "github.com/bnema/dumber/pkg/webkit" | ||
| 11 | + | "github.com/diamondburned/gotk4/pkg/gdk/v4" | |||
| 12 | + | "github.com/diamondburned/gotk4/pkg/gtk/v4" | |||
| 11 | ) | 13 | ) | ||
| 12 | 14 | ||||
| 13 | // WindowShortcutHandler manages global shortcuts at the window level | 15 | // WindowShortcutHandler manages global shortcuts at the window level | ||
| @@ -47,6 +49,11 @@ func (h *WindowShortcutHandler) initialize() error { | |||||
| 47 | return ErrFailedToInitialize | 49 | return ErrFailedToInitialize | ||
| 48 | } | 50 | } | ||
| 49 | 51 | ||||
| 52 | + | // Register hardware keycode-based shortcuts (for AZERTY/international keyboards) | |||
| 53 | + | if err := h.registerKeycodeShortcuts(); err != nil { | |||
| 54 | + | log.Printf("[window-shortcuts] Failed to register keycode shortcuts: %v", err) | |||
| 55 | + | } | |||
| 56 | + | ||||
| 50 | return h.registerGlobalShortcuts() | 57 | return h.registerGlobalShortcuts() | ||
| 51 | } | 58 | } | ||
| 52 | 59 | ||||
| @@ -65,6 +72,8 @@ func (h *WindowShortcutHandler) registerGlobalShortcuts() error { | |||||
| 65 | {"ctrl+p", h.handlePaneMode, "Enter pane mode"}, | 72 | {"ctrl+p", h.handlePaneMode, "Enter pane mode"}, | ||
| 66 | // Tab management | 73 | // Tab management | ||
| 67 | {"ctrl+t", h.handleTabMode, "Enter tab mode"}, | 74 | {"ctrl+t", h.handleTabMode, "Enter tab mode"}, | ||
| 75 | + | // Global tab navigation (Ctrl+Tab) is handled via EventControllerKey in registerKeycodeShortcuts | |||
| 76 | + | // This prevents GTK's default focus cycling behavior | |||
| 68 | // Tab mode actions (only active when tab mode is enabled) | 77 | // Tab mode actions (only active when tab mode is enabled) | ||
| 69 | {"n", func() { h.handleTabModeAction("new-tab") }, "New tab (tab mode)"}, | 78 | {"n", func() { h.handleTabModeAction("new-tab") }, "New tab (tab mode)"}, | ||
| 70 | {"c", func() { h.handleTabModeAction("new-tab") }, "New tab (tab mode)"}, | 79 | {"c", func() { h.handleTabModeAction("new-tab") }, "New tab (tab mode)"}, | ||
| @@ -76,17 +85,8 @@ func (h *WindowShortcutHandler) registerGlobalShortcuts() error { | |||||
| 76 | {"shift+Tab", func() { h.handleTabModeAction("previous-tab") }, "Previous tab (tab mode)"}, | 85 | {"shift+Tab", func() { h.handleTabModeAction("previous-tab") }, "Previous tab (tab mode)"}, | ||
| 77 | {"Return", func() { h.handleTabModeAction("confirm") }, "Confirm (tab mode)"}, | 86 | {"Return", func() { h.handleTabModeAction("confirm") }, "Confirm (tab mode)"}, | ||
| 78 | {"Escape", func() { h.handleTabModeAction("cancel") }, "Cancel (tab mode)"}, | 87 | {"Escape", func() { h.handleTabModeAction("cancel") }, "Cancel (tab mode)"}, | ||
| 79 | - | // Direct tab switching | 88 | + | // Direct tab switching is now handled via hardware keycodes (see registerKeycodeShortcuts) |
| 80 | - | {"alt+1", func() { h.handleDirectTabSwitch(0) }, "Switch to tab 1"}, | 89 | + | // This supports all keyboard layouts (QWERTY, AZERTY, etc.) |
| 81 | - | {"alt+2", func() { h.handleDirectTabSwitch(1) }, "Switch to tab 2"}, | |||
| 82 | - | {"alt+3", func() { h.handleDirectTabSwitch(2) }, "Switch to tab 3"}, | |||
| 83 | - | {"alt+4", func() { h.handleDirectTabSwitch(3) }, "Switch to tab 4"}, | |||
| 84 | - | {"alt+5", func() { h.handleDirectTabSwitch(4) }, "Switch to tab 5"}, | |||
| 85 | - | {"alt+6", func() { h.handleDirectTabSwitch(5) }, "Switch to tab 6"}, | |||
| 86 | - | {"alt+7", func() { h.handleDirectTabSwitch(6) }, "Switch to tab 7"}, | |||
| 87 | - | {"alt+8", func() { h.handleDirectTabSwitch(7) }, "Switch to tab 8"}, | |||
| 88 | - | {"alt+9", func() { h.handleDirectTabSwitch(8) }, "Switch to tab 9"}, | |||
| 89 | - | {"alt+0", func() { h.handleDirectTabSwitch(9) }, "Switch to tab 10"}, | |||
| 90 | // Page reload shortcuts | 90 | // Page reload shortcuts | ||
| 91 | {"ctrl+r", h.handleReload, "Reload page"}, | 91 | {"ctrl+r", h.handleReload, "Reload page"}, | ||
| 92 | {"ctrl+shift+r", h.handleHardReload, "Hard reload (bypass cache)"}, | 92 | {"ctrl+shift+r", h.handleHardReload, "Hard reload (bypass cache)"}, | ||
| @@ -118,6 +118,76 @@ func (h *WindowShortcutHandler) registerGlobalShortcuts() error { | |||||
| 118 | return nil | 118 | return nil | ||
| 119 | } | 119 | } | ||
| 120 | 120 | ||||
| 121 | + | // registerKeycodeShortcuts registers keyboard shortcuts based on hardware keycodes | |||
| 122 | + | // This allows Alt+number shortcuts to work on AZERTY and other keyboard layouts | |||
| 123 | + | func (h *WindowShortcutHandler) registerKeycodeShortcuts() error { | |||
| 124 | + | if h.window == nil { | |||
| 125 | + | return fmt.Errorf("window is nil") | |||
| 126 | + | } | |||
| 127 | + | ||||
| 128 | + | gtkWindow := h.window.AsWindow() | |||
| 129 | + | if gtkWindow == nil { | |||
| 130 | + | return fmt.Errorf("gtk window is nil") | |||
| 131 | + | } | |||
| 132 | + | ||||
| 133 | + | // Create event controller for keyboard events | |||
| 134 | + | keyController := gtk.NewEventControllerKey() | |||
| 135 | + | keyController.SetPropagationPhase(gtk.PhaseCapture) | |||
| 136 | + | ||||
| 137 | + | // GDK key constants | |||
| 138 | + | const ( | |||
| 139 | + | gdkKeyTab = 0xff09 | |||
| 140 | + | ) | |||
| 141 | + | ||||
| 142 | + | // Map hardware keycodes to tab indices | |||
| 143 | + | // These keycodes are the same across all keyboard layouts | |||
| 144 | + | keycodeToTab := map[uint]int{ | |||
| 145 | + | 10: 0, // 1/& key | |||
| 146 | + | 11: 1, // 2/é key | |||
| 147 | + | 12: 2, // 3/" key | |||
| 148 | + | 13: 3, // 4/' key | |||
| 149 | + | 14: 4, // 5/( key | |||
| 150 | + | 15: 5, // 6/- key | |||
| 151 | + | 16: 6, // 7/è key | |||
| 152 | + | 17: 7, // 8/_ key | |||
| 153 | + | 18: 8, // 9/ç key | |||
| 154 | + | 19: 9, // 0/à key | |||
| 155 | + | } | |||
| 156 | + | ||||
| 157 | + | keyController.ConnectKeyPressed(func(keyval, keycode uint, state gdk.ModifierType) bool { | |||
| 158 | + | // Handle Ctrl+Tab for next tab | |||
| 159 | + | if keyval == gdkKeyTab && state.Has(gdk.ControlMask) && !state.Has(gdk.ShiftMask) { | |||
| 160 | + | log.Printf("[window-shortcuts] Ctrl+Tab -> next tab") | |||
| 161 | + | h.handleNextTab() | |||
| 162 | + | return true // Consume event to prevent focus cycling | |||
| 163 | + | } | |||
| 164 | + | ||||
| 165 | + | // Handle Ctrl+Shift+Tab for previous tab | |||
| 166 | + | if keyval == gdkKeyTab && state.Has(gdk.ControlMask) && state.Has(gdk.ShiftMask) { | |||
| 167 | + | log.Printf("[window-shortcuts] Ctrl+Shift+Tab -> previous tab") | |||
| 168 | + | h.handlePrevTab() | |||
| 169 | + | return true // Consume event to prevent focus cycling | |||
| 170 | + | } | |||
| 171 | + | ||||
| 172 | + | // Check for Alt+number (but not with Ctrl or Shift) | |||
| 173 | + | if state.Has(gdk.AltMask) && !state.Has(gdk.ControlMask) && !state.Has(gdk.ShiftMask) { | |||
| 174 | + | // Check if this is a number key | |||
| 175 | + | if tabIndex, ok := keycodeToTab[keycode]; ok { | |||
| 176 | + | log.Printf("[window-shortcuts] Hardware keycode shortcut: Alt+keycode(%d) -> tab %d", keycode, tabIndex) | |||
| 177 | + | h.handleDirectTabSwitch(tabIndex) | |||
| 178 | + | return true // Handled | |||
| 179 | + | } | |||
| 180 | + | } | |||
| 181 | + | ||||
| 182 | + | return false | |||
| 183 | + | }) | |||
| 184 | + | ||||
| 185 | + | gtkWindow.AddController(keyController) | |||
| 186 | + | log.Printf("[window-shortcuts] Registered hardware keycode shortcuts for Alt+number (AZERTY support)") | |||
| 187 | + | ||||
| 188 | + | return nil | |||
| 189 | + | } | |||
| 190 | + | ||||
| 121 | func (h *WindowShortcutHandler) handleOmniboxToggle() { | 191 | func (h *WindowShortcutHandler) handleOmniboxToggle() { | ||
| 122 | h.handleUIToggle(&h.lastOmniboxToggle, "omnibox", "omnibox-nav-toggle") | 192 | h.handleUIToggle(&h.lastOmniboxToggle, "omnibox", "omnibox-nav-toggle") | ||
| 123 | } | 193 | } | ||
| @@ -455,6 +525,32 @@ func (h *WindowShortcutHandler) handleTabMode() { | |||||
| 455 | h.app.tabManager.EnterTabMode() | 525 | h.app.tabManager.EnterTabMode() | ||
| 456 | } | 526 | } | ||
| 457 | 527 | ||||
| 528 | + | // handleNextTab switches to the next tab. | |||
| 529 | + | func (h *WindowShortcutHandler) handleNextTab() { | |||
| 530 | + | if h.app == nil || h.app.tabManager == nil { | |||
| 531 | + | log.Printf("[window-shortcuts] Cannot switch tab: tab manager not available") | |||
| 532 | + | return | |||
| 533 | + | } | |||
| 534 | + | ||||
| 535 | + | log.Printf("[window-shortcuts] Switching to next tab") | |||
| 536 | + | if err := h.app.tabManager.NextTab(); err != nil { | |||
| 537 | + | log.Printf("[window-shortcuts] Failed to switch to next tab: %v", err) | |||
| 538 | + | } | |||
| 539 | + | } | |||
| 540 | + | ||||
| 541 | + | // handlePrevTab switches to the previous tab. | |||
| 542 | + | func (h *WindowShortcutHandler) handlePrevTab() { | |||
| 543 | + | if h.app == nil || h.app.tabManager == nil { | |||
| 544 | + | log.Printf("[window-shortcuts] Cannot switch tab: tab manager not available") | |||
| 545 | + | return | |||
| 546 | + | } | |||
| 547 | + | ||||
| 548 | + | log.Printf("[window-shortcuts] Switching to previous tab") | |||
| 549 | + | if err := h.app.tabManager.PreviousTab(); err != nil { | |||
| 550 | + | log.Printf("[window-shortcuts] Failed to switch to previous tab: %v", err) | |||
| 551 | + | } | |||
| 552 | + | } | |||
| 553 | + | ||||
| 458 | // handleDirectTabSwitch switches to a specific tab by index (0-based). | 554 | // handleDirectTabSwitch switches to a specific tab by index (0-based). | ||
| 459 | func (h *WindowShortcutHandler) handleDirectTabSwitch(index int) { | 555 | func (h *WindowShortcutHandler) handleDirectTabSwitch(index int) { | ||
| 460 | if h.app == nil || h.app.tabManager == nil { | 556 | if h.app == nil || h.app.tabManager == nil { | ||
M internal/app/browser/workspace_css.go +15 -6
| @@ -44,6 +44,14 @@ func getStackTitleBorderColor(isDark bool) string { | |||
| 44 | 44 | return "#e5e5e5" // Slightly darker than #f0f0f0 | |
| 45 | 45 | } | |
| 46 | 46 | ||
| 47 | + | // getActiveTabBg returns a distinct background color for the active tab | |
| 48 | + | func getActiveTabBg(isDark bool) string { | |
| 49 | + | if isDark { | |
| 50 | + | return "#707070" // Significantly lighter gray than inactive (#404040) - similar to stacked panes | |
| 51 | + | } | |
| 52 | + | return "#c0c0c0" // Significantly darker gray than inactive (#f0f0f0) - similar to stacked panes | |
| 53 | + | } | |
| 54 | + | ||
| 47 | 55 | // getActivePaneBorderColor returns the border color for active panes based on config and theme | |
| 48 | 56 | func getActivePaneBorderColor(styling config.WorkspaceStylingConfig, isDark bool) string { | |
| 49 | 57 | // Use configured border color if set | |
| @@ -215,23 +223,24 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||
| 215 | 223 | } | |
| 216 | 224 | ||
| 217 | 225 | /* Tab button styling - matches stacked pane titles */ | |
| 218 | - | .tab-button { | |
| 226 | + | button.tab-button { | |
| 219 | 227 | background-color: %s; | |
| 228 | + | background-image: none; | |
| 220 | 229 | border: none; | |
| 221 | 230 | border-right: 1px solid %s; | |
| 222 | 231 | border-radius: 0; | |
| 223 | 232 | padding: %dpx %dpx; | |
| 224 | - | min-width: 100px; | |
| 225 | - | max-width: 200px; | |
| 226 | 233 | transition: background-color %dms ease-in-out; | |
| 227 | 234 | } | |
| 228 | 235 | ||
| 229 | - | .tab-button:hover { | |
| 236 | + | button.tab-button:hover { | |
| 230 | 237 | background-color: %s; | |
| 238 | + | background-image: none; | |
| 231 | 239 | } | |
| 232 | 240 | ||
| 233 | - | .tab-button-active { | |
| 241 | + | button.tab-button.tab-button-active { | |
| 234 | 242 | background-color: %s; | |
| 243 | + | background-image: none; | |
| 235 | 244 | border: none; | |
| 236 | 245 | border-right: 1px solid %s; | |
| 237 | 246 | border-radius: 0; | |
| @@ -293,7 +302,7 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||
| 293 | 302 | titlePaddingHorizontal, // tab-button padding horizontal | |
| 294 | 303 | styling.TransitionDuration, // tab-button transition | |
| 295 | 304 | getStackTitleHoverBg(isDark), // tab-button:hover background | |
| 296 | - | getStackTitleHoverBg(isDark), // tab-button-active background (lighter/brighter) | |
| 305 | + | getActiveTabBg(isDark), // tab-button-active background (much more visible) | |
| 297 | 306 | getStackTitleBorderColor(isDark), // tab-button-active border-right | |
| 298 | 307 | titleFontSize, // tab-title font-size | |
| 299 | 308 | getStackTitleTextColor(isDark), // tab-title color | |
| @@ -44,6 +44,14 @@ func getStackTitleBorderColor(isDark bool) string { | |||||
| 44 | return "#e5e5e5" // Slightly darker than #f0f0f0 | 44 | return "#e5e5e5" // Slightly darker than #f0f0f0 | ||
| 45 | } | 45 | } | ||
| 46 | 46 | ||||
| 47 | + | // getActiveTabBg returns a distinct background color for the active tab | |||
| 48 | + | func getActiveTabBg(isDark bool) string { | |||
| 49 | + | if isDark { | |||
| 50 | + | return "#707070" // Significantly lighter gray than inactive (#404040) - similar to stacked panes | |||
| 51 | + | } | |||
| 52 | + | return "#c0c0c0" // Significantly darker gray than inactive (#f0f0f0) - similar to stacked panes | |||
| 53 | + | } | |||
| 54 | + | ||||
| 47 | // getActivePaneBorderColor returns the border color for active panes based on config and theme | 55 | // getActivePaneBorderColor returns the border color for active panes based on config and theme | ||
| 48 | func getActivePaneBorderColor(styling config.WorkspaceStylingConfig, isDark bool) string { | 56 | func getActivePaneBorderColor(styling config.WorkspaceStylingConfig, isDark bool) string { | ||
| 49 | // Use configured border color if set | 57 | // Use configured border color if set | ||
| @@ -215,23 +223,24 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||||
| 215 | } | 223 | } | ||
| 216 | 224 | ||||
| 217 | /* Tab button styling - matches stacked pane titles */ | 225 | /* Tab button styling - matches stacked pane titles */ | ||
| 218 | - | .tab-button { | 226 | + | button.tab-button { |
| 219 | background-color: %s; | 227 | background-color: %s; | ||
| 228 | + | background-image: none; | |||
| 220 | border: none; | 229 | border: none; | ||
| 221 | border-right: 1px solid %s; | 230 | border-right: 1px solid %s; | ||
| 222 | border-radius: 0; | 231 | border-radius: 0; | ||
| 223 | padding: %dpx %dpx; | 232 | padding: %dpx %dpx; | ||
| 224 | - | min-width: 100px; | |||
| 225 | - | max-width: 200px; | |||
| 226 | transition: background-color %dms ease-in-out; | 233 | transition: background-color %dms ease-in-out; | ||
| 227 | } | 234 | } | ||
| 228 | 235 | ||||
| 229 | - | .tab-button:hover { | 236 | + | button.tab-button:hover { |
| 230 | background-color: %s; | 237 | background-color: %s; | ||
| 238 | + | background-image: none; | |||
| 231 | } | 239 | } | ||
| 232 | 240 | ||||
| 233 | - | .tab-button-active { | 241 | + | button.tab-button.tab-button-active { |
| 234 | background-color: %s; | 242 | background-color: %s; | ||
| 243 | + | background-image: none; | |||
| 235 | border: none; | 244 | border: none; | ||
| 236 | border-right: 1px solid %s; | 245 | border-right: 1px solid %s; | ||
| 237 | border-radius: 0; | 246 | border-radius: 0; | ||
| @@ -293,7 +302,7 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||||
| 293 | titlePaddingHorizontal, // tab-button padding horizontal | 302 | titlePaddingHorizontal, // tab-button padding horizontal | ||
| 294 | styling.TransitionDuration, // tab-button transition | 303 | styling.TransitionDuration, // tab-button transition | ||
| 295 | getStackTitleHoverBg(isDark), // tab-button:hover background | 304 | getStackTitleHoverBg(isDark), // tab-button:hover background | ||
| 296 | - | getStackTitleHoverBg(isDark), // tab-button-active background (lighter/brighter) | 305 | + | getActiveTabBg(isDark), // tab-button-active background (much more visible) |
| 297 | getStackTitleBorderColor(isDark), // tab-button-active border-right | 306 | getStackTitleBorderColor(isDark), // tab-button-active border-right | ||
| 298 | titleFontSize, // tab-title font-size | 307 | titleFontSize, // tab-title font-size | ||
| 299 | getStackTitleTextColor(isDark), // tab-title color | 308 | getStackTitleTextColor(isDark), // tab-title color | ||
M internal/app/environment/locale.go +34 -24
| @@ -12,38 +12,21 @@ import ( | |||
| 12 | 12 | func DetectAndSetKeyboardLocale() { | |
| 13 | 13 | const colonSplitParts = 2 // Number of parts when splitting on colon | |
| 14 | 14 | ||
| 15 | + | var locale string | |
| 16 | + | ||
| 15 | 17 | // 0) Explicit override | |
| 16 | - | locale := os.Getenv("DUMBER_KEYBOARD_LOCALE") | |
| 18 | + | locale = os.Getenv("DUMBER_KEYBOARD_LOCALE") | |
| 17 | 19 | if locale == "" { | |
| 18 | 20 | locale = os.Getenv("DUMB_BROWSER_KEYBOARD_LOCALE") // legacy prefix | |
| 19 | 21 | } | |
| 20 | - | // 1) Environment | |
| 21 | - | if locale == "" { | |
| 22 | - | locale = os.Getenv("LC_ALL") | |
| 23 | - | } | |
| 24 | - | if locale == "" { | |
| 25 | - | locale = os.Getenv("LANG") | |
| 26 | - | } | |
| 27 | - | if locale == "" { | |
| 28 | - | locale = os.Getenv("LC_CTYPE") | |
| 29 | - | } | |
| 30 | - | // Trim variants | |
| 31 | - | if i := strings.IndexByte(locale, '.'); i > 0 { | |
| 32 | - | locale = locale[:i] | |
| 33 | - | } | |
| 34 | - | if i := strings.IndexByte(locale, '@'); i > 0 { | |
| 35 | - | locale = locale[:i] | |
| 36 | - | } | |
| 37 | - | if i := strings.IndexByte(locale, '_'); i > 0 { | |
| 38 | - | locale = locale[:i] | |
| 39 | - | } | |
| 40 | 22 | ||
| 41 | - | // 2) Try XKB env override | |
| 23 | + | // 1) XKB env override (actual keyboard layout) | |
| 42 | 24 | if locale == "" { | |
| 43 | 25 | locale = os.Getenv("XKB_DEFAULT_LAYOUT") | |
| 44 | 26 | } | |
| 45 | 27 | ||
| 46 | - | // 3) Best-effort probe of setxkbmap/localectl (non-fatal) | |
| 28 | + | // 2) Best-effort probe of localectl/setxkbmap (actual keyboard layout) | |
| 29 | + | // Check these BEFORE LANG since LANG is system language, not keyboard layout | |
| 47 | 30 | if locale == "" { | |
| 48 | 31 | if out, err := exec.Command("localectl", "status").Output(); err == nil { | |
| 49 | 32 | s := string(out) | |
| @@ -55,6 +38,7 @@ func DetectAndSetKeyboardLocale() { | |||
| 55 | 38 | cand := strings.TrimSpace(parts[1]) | |
| 56 | 39 | if cand != "" { | |
| 57 | 40 | locale = cand | |
| 41 | + | log.Printf("[locale] keyboard layout from localectl: %s", locale) | |
| 58 | 42 | } | |
| 59 | 43 | } | |
| 60 | 44 | break | |
| @@ -73,6 +57,7 @@ func DetectAndSetKeyboardLocale() { | |||
| 73 | 57 | cand := strings.TrimSpace(parts[1]) | |
| 74 | 58 | if cand != "" { | |
| 75 | 59 | locale = cand | |
| 60 | + | log.Printf("[locale] keyboard layout from setxkbmap: %s", locale) | |
| 76 | 61 | } | |
| 77 | 62 | } | |
| 78 | 63 | break | |
| @@ -81,9 +66,34 @@ func DetectAndSetKeyboardLocale() { | |||
| 81 | 66 | } | |
| 82 | 67 | } | |
| 83 | 68 | ||
| 69 | + | // 3) Fallback to system locale (language, not keyboard layout) | |
| 70 | + | if locale == "" { | |
| 71 | + | locale = os.Getenv("LC_ALL") | |
| 72 | + | } | |
| 73 | + | if locale == "" { | |
| 74 | + | locale = os.Getenv("LANG") | |
| 75 | + | } | |
| 76 | + | if locale == "" { | |
| 77 | + | locale = os.Getenv("LC_CTYPE") | |
| 78 | + | } | |
| 79 | + | // Trim variants from locale | |
| 80 | + | if locale != "" { | |
| 81 | + | if i := strings.IndexByte(locale, '.'); i > 0 { | |
| 82 | + | locale = locale[:i] | |
| 83 | + | } | |
| 84 | + | if i := strings.IndexByte(locale, '@'); i > 0 { | |
| 85 | + | locale = locale[:i] | |
| 86 | + | } | |
| 87 | + | if i := strings.IndexByte(locale, '_'); i > 0 { | |
| 88 | + | locale = locale[:i] | |
| 89 | + | } | |
| 90 | + | log.Printf("[locale] keyboard layout from system locale: %s", locale) | |
| 91 | + | } | |
| 92 | + | ||
| 84 | 93 | if locale == "" { | |
| 85 | 94 | locale = "en" | |
| 95 | + | log.Printf("[locale] keyboard layout defaulted to: %s", locale) | |
| 86 | 96 | } | |
| 87 | - | // No layout-specific remaps; log for diagnostics only | |
| 97 | + | ||
| 88 | 98 | log.Printf("[locale] keyboard locale detected: %s", locale) | |
| 89 | 99 | } | |
| @@ -12,38 +12,21 @@ import ( | |||||
| 12 | func DetectAndSetKeyboardLocale() { | 12 | func DetectAndSetKeyboardLocale() { | ||
| 13 | const colonSplitParts = 2 // Number of parts when splitting on colon | 13 | const colonSplitParts = 2 // Number of parts when splitting on colon | ||
| 14 | 14 | ||||
| 15 | + | var locale string | |||
| 16 | + | ||||
| 15 | // 0) Explicit override | 17 | // 0) Explicit override | ||
| 16 | - | locale := os.Getenv("DUMBER_KEYBOARD_LOCALE") | 18 | + | locale = os.Getenv("DUMBER_KEYBOARD_LOCALE") |
| 17 | if locale == "" { | 19 | if locale == "" { | ||
| 18 | locale = os.Getenv("DUMB_BROWSER_KEYBOARD_LOCALE") // legacy prefix | 20 | locale = os.Getenv("DUMB_BROWSER_KEYBOARD_LOCALE") // legacy prefix | ||
| 19 | } | 21 | } | ||
| 20 | - | // 1) Environment | |||
| 21 | - | if locale == "" { | |||
| 22 | - | locale = os.Getenv("LC_ALL") | |||
| 23 | - | } | |||
| 24 | - | if locale == "" { | |||
| 25 | - | locale = os.Getenv("LANG") | |||
| 26 | - | } | |||
| 27 | - | if locale == "" { | |||
| 28 | - | locale = os.Getenv("LC_CTYPE") | |||
| 29 | - | } | |||
| 30 | - | // Trim variants | |||
| 31 | - | if i := strings.IndexByte(locale, '.'); i > 0 { | |||
| 32 | - | locale = locale[:i] | |||
| 33 | - | } | |||
| 34 | - | if i := strings.IndexByte(locale, '@'); i > 0 { | |||
| 35 | - | locale = locale[:i] | |||
| 36 | - | } | |||
| 37 | - | if i := strings.IndexByte(locale, '_'); i > 0 { | |||
| 38 | - | locale = locale[:i] | |||
| 39 | - | } | |||
| 40 | 22 | ||||
| 41 | - | // 2) Try XKB env override | 23 | + | // 1) XKB env override (actual keyboard layout) |
| 42 | if locale == "" { | 24 | if locale == "" { | ||
| 43 | locale = os.Getenv("XKB_DEFAULT_LAYOUT") | 25 | locale = os.Getenv("XKB_DEFAULT_LAYOUT") | ||
| 44 | } | 26 | } | ||
| 45 | 27 | ||||
| 46 | - | // 3) Best-effort probe of setxkbmap/localectl (non-fatal) | 28 | + | // 2) Best-effort probe of localectl/setxkbmap (actual keyboard layout) |
| 29 | + | // Check these BEFORE LANG since LANG is system language, not keyboard layout | |||
| 47 | if locale == "" { | 30 | if locale == "" { | ||
| 48 | if out, err := exec.Command("localectl", "status").Output(); err == nil { | 31 | if out, err := exec.Command("localectl", "status").Output(); err == nil { | ||
| 49 | s := string(out) | 32 | s := string(out) | ||
| @@ -55,6 +38,7 @@ func DetectAndSetKeyboardLocale() { | |||||
| 55 | cand := strings.TrimSpace(parts[1]) | 38 | cand := strings.TrimSpace(parts[1]) | ||
| 56 | if cand != "" { | 39 | if cand != "" { | ||
| 57 | locale = cand | 40 | locale = cand | ||
| 41 | + | log.Printf("[locale] keyboard layout from localectl: %s", locale) | |||
| 58 | } | 42 | } | ||
| 59 | } | 43 | } | ||
| 60 | break | 44 | break | ||
| @@ -73,6 +57,7 @@ func DetectAndSetKeyboardLocale() { | |||||
| 73 | cand := strings.TrimSpace(parts[1]) | 57 | cand := strings.TrimSpace(parts[1]) | ||
| 74 | if cand != "" { | 58 | if cand != "" { | ||
| 75 | locale = cand | 59 | locale = cand | ||
| 60 | + | log.Printf("[locale] keyboard layout from setxkbmap: %s", locale) | |||
| 76 | } | 61 | } | ||
| 77 | } | 62 | } | ||
| 78 | break | 63 | break | ||
| @@ -81,9 +66,34 @@ func DetectAndSetKeyboardLocale() { | |||||
| 81 | } | 66 | } | ||
| 82 | } | 67 | } | ||
| 83 | 68 | ||||
| 69 | + | // 3) Fallback to system locale (language, not keyboard layout) | |||
| 70 | + | if locale == "" { | |||
| 71 | + | locale = os.Getenv("LC_ALL") | |||
| 72 | + | } | |||
| 73 | + | if locale == "" { | |||
| 74 | + | locale = os.Getenv("LANG") | |||
| 75 | + | } | |||
| 76 | + | if locale == "" { | |||
| 77 | + | locale = os.Getenv("LC_CTYPE") | |||
| 78 | + | } | |||
| 79 | + | // Trim variants from locale | |||
| 80 | + | if locale != "" { | |||
| 81 | + | if i := strings.IndexByte(locale, '.'); i > 0 { | |||
| 82 | + | locale = locale[:i] | |||
| 83 | + | } | |||
| 84 | + | if i := strings.IndexByte(locale, '@'); i > 0 { | |||
| 85 | + | locale = locale[:i] | |||
| 86 | + | } | |||
| 87 | + | if i := strings.IndexByte(locale, '_'); i > 0 { | |||
| 88 | + | locale = locale[:i] | |||
| 89 | + | } | |||
| 90 | + | log.Printf("[locale] keyboard layout from system locale: %s", locale) | |||
| 91 | + | } | |||
| 92 | + | ||||
| 84 | if locale == "" { | 93 | if locale == "" { | ||
| 85 | locale = "en" | 94 | locale = "en" | ||
| 95 | + | log.Printf("[locale] keyboard layout defaulted to: %s", locale) | |||
| 86 | } | 96 | } | ||
| 87 | - | // No layout-specific remaps; log for diagnostics only | 97 | + | |
| 88 | log.Printf("[locale] keyboard locale detected: %s", locale) | 98 | log.Printf("[locale] keyboard locale detected: %s", locale) | ||
| 89 | } | 99 | } | ||
M pkg/webkit/shortcuts.go +6 -0
| @@ -138,6 +138,12 @@ func convertToGtkFormat(key string) string { | |||
| 138 | 138 | parts = append(parts, "Up") | |
| 139 | 139 | case "down", "arrowdown": | |
| 140 | 140 | parts = append(parts, "Down") | |
| 141 | + | case "tab": | |
| 142 | + | parts = append(parts, "Tab") | |
| 143 | + | case "escape": | |
| 144 | + | parts = append(parts, "Escape") | |
| 145 | + | case "return", "enter": | |
| 146 | + | parts = append(parts, "Return") | |
| 141 | 147 | default: | |
| 142 | 148 | // Single letter keys like l, f, c, w, etc. | |
| 143 | 149 | parts = append(parts, part) | |
| @@ -138,6 +138,12 @@ func convertToGtkFormat(key string) string { | |||||
| 138 | parts = append(parts, "Up") | 138 | parts = append(parts, "Up") | ||
| 139 | case "down", "arrowdown": | 139 | case "down", "arrowdown": | ||
| 140 | parts = append(parts, "Down") | 140 | parts = append(parts, "Down") | ||
| 141 | + | case "tab": | |||
| 142 | + | parts = append(parts, "Tab") | |||
| 143 | + | case "escape": | |||
| 144 | + | parts = append(parts, "Escape") | |||
| 145 | + | case "return", "enter": | |||
| 146 | + | parts = append(parts, "Return") | |||
| 141 | default: | 147 | default: | ||
| 142 | // Single letter keys like l, f, c, w, etc. | 148 | // Single letter keys like l, f, c, w, etc. | ||
| 143 | parts = append(parts, part) | 149 | parts = append(parts, part) | ||