detached 630065a

feat(tabs): implement inline tab rename functionality

Add inline editing capability for tab names with full keyboard support:

Changes:
- tab_bar.go: Add startInlineRename(), finishInlineRename(), cancelRename()
  - Replace tab button label with Entry widget for editing
  - Handle Enter (save), Escape (cancel), and focus-out events
  - Preserve and restore button focus behavior during rename
  - Add trimWhitespace() and isWhitespace() helpers

- tab_manager.go: Add rename state tracking
  - Add renameInProgress, renamingTab fields
  - Add renamePrevFocusOnClick, renamePrevCanFocus fields
  - Add IsRenameInProgress() method for external checks

- tab_mode.go: Convert handleRenameTab() to inline editing
  - Replace dialog approach with startInlineRename()
  - Guard against rename when only 1 tab (tab bar hidden)
  - Tab mode exits automatically on rename completion

- window_shortcuts.go: Add rename detection to prevent key interception
  - Add isRenameInProgress() check in keycode handler
  - Let ALL keys propagate to Entry widget during rename
  - Prevents tab mode action keys from interfering with text input

This enables users to rename tabs with Alt+T → r, providing a seamless
inline editing experience similar to modern IDEs.
commit
630065ade68318a3bc54739e299ef9efd06218ba
author
bnema · · 2025-11-18 18:06 UTC
parent
e68eda9
signature
GPG Unverified2B82207D370127A0 The key is unknown
4 files · +250 -19
M internal/app/browser/tab_bar.go +205 -0
@@ -6,6 +6,7 @@ import (
66
77 "github.com/bnema/dumber/internal/logging"
88 "github.com/bnema/dumber/pkg/webkit"
9+ "github.com/diamondburned/gotk4/pkg/gdk/v4"
910 "github.com/diamondburned/gotk4/pkg/gtk/v4"
1011 )
1112
@@ -238,3 +239,207 @@ func (tm *TabManager) setTabActiveStyle(tab *Tab, active bool) {
238239 }
239240 })
240241 }
242+
243+// startInlineRename replaces the tab button's label with an entry widget for inline editing.
244+func (tm *TabManager) startInlineRename(tab *Tab) {
245+ if tab.titleButton == nil {
246+ logging.Error(fmt.Sprintf("[tabs] Cannot start rename: button is nil for tab %s", tab.id))
247+ return
248+ }
249+
250+ // Check if rename already in progress
251+ tm.mu.Lock()
252+ if tm.renameInProgress {
253+ logging.Debug("[tabs] Rename already in progress, ignoring")
254+ tm.mu.Unlock()
255+ return
256+ }
257+ tm.renameInProgress = true
258+ tm.renamingTab = tab
259+
260+ // Stop tab mode timer during rename
261+ if tm.tabModeTimer != nil {
262+ tm.tabModeTimer.Stop()
263+ tm.tabModeTimer = nil
264+ }
265+ tm.mu.Unlock()
266+
267+ webkit.RunOnMainThread(func() {
268+ button, ok := tab.titleButton.(*gtk.Button)
269+ if !ok {
270+ logging.Error(fmt.Sprintf("[tabs] Tab button is not a *gtk.Button: %s", tab.id))
271+ tm.cancelRename()
272+ return
273+ }
274+
275+ // Get current text
276+ currentText := tab.title
277+ if tab.customTitle != "" {
278+ currentText = tab.customTitle
279+ }
280+
281+ // Store original label for restoration
282+ originalLabel := button.Child()
283+
284+ // Create entry widget
285+ entry := gtk.NewEntry()
286+ if entry == nil {
287+ logging.Error("[tabs] Failed to create entry widget for rename")
288+ tm.cancelRename()
289+ return
290+ }
291+
292+ // Store current focus behavior so we can restore it later
293+ prevFocusOnClick := button.FocusOnClick()
294+ prevCanFocus := button.CanFocus()
295+ tm.mu.Lock()
296+ tm.renamePrevFocusOnClick = prevFocusOnClick
297+ tm.renamePrevCanFocus = prevCanFocus
298+ tm.mu.Unlock()
299+
300+ // Allow the entry to receive focus while rename is active
301+ button.SetFocusOnClick(true)
302+ button.SetCanFocus(true)
303+
304+ // Configure entry BEFORE adding to widget tree
305+ entry.SetText(currentText)
306+ entry.AddCSSClass("tab-rename-entry")
307+ entry.SetCanFocus(true) // Explicitly allow focus
308+ entry.SetFocusable(true) // GTK4 focusability
309+
310+ // Handle Enter key - save
311+ entry.ConnectActivate(func() {
312+ newName := entry.Text()
313+ logging.Debug(fmt.Sprintf("[tabs] Rename activate (Enter): '%s' -> '%s'", currentText, newName))
314+ tm.finishInlineRename(tab, newName, originalLabel)
315+ })
316+
317+ // Handle Escape key - cancel
318+ keyController := gtk.NewEventControllerKey()
319+ keyController.ConnectKeyPressed(func(keyval, keycode uint, state gdk.ModifierType) bool {
320+ const gdkKeyEscape = 0xff1b
321+ if keyval == gdkKeyEscape {
322+ logging.Debug("[tabs] Rename cancelled (Escape)")
323+ tm.finishInlineRename(tab, "", originalLabel) // Empty = cancel
324+ return true
325+ }
326+ return false
327+ })
328+ entry.AddController(keyController)
329+
330+ // Handle focus-out - cancel (safety)
331+ focusController := gtk.NewEventControllerFocus()
332+ focusController.ConnectLeave(func() {
333+ logging.Debug("[tabs] Rename cancelled (focus-out)")
334+ tm.finishInlineRename(tab, "", originalLabel)
335+ })
336+ entry.AddController(focusController)
337+
338+ // Replace label with entry
339+ button.SetChild(entry)
340+
341+ // Make entry visible first
342+ webkit.WidgetSetVisible(entry, true)
343+
344+ // Grab focus after widget is visible and realized
345+ // Use idle callback to ensure widget is fully realized
346+ entry.GrabFocus()
347+
348+ // Select all text after focus
349+ entry.SelectRegion(0, -1)
350+
351+ logging.Debug(fmt.Sprintf("[tabs] Started inline rename for tab %s", tab.id))
352+ })
353+}
354+
355+// cancelRename clears the rename state without changing anything
356+func (tm *TabManager) cancelRename() {
357+ tm.mu.Lock()
358+ defer tm.mu.Unlock()
359+ tm.renameInProgress = false
360+ tm.renamingTab = nil
361+ tm.renamePrevFocusOnClick = false
362+ tm.renamePrevCanFocus = false
363+ logging.Debug("[tabs] Rename state cleared")
364+}
365+
366+// finishInlineRename restores the label and saves the new name if provided.
367+func (tm *TabManager) finishInlineRename(tab *Tab, newName string, originalLabel gtk.Widgetter) {
368+ // Guard against multiple calls
369+ tm.mu.Lock()
370+ if !tm.renameInProgress {
371+ logging.Debug("[tabs] finishInlineRename called but no rename in progress, ignoring")
372+ tm.mu.Unlock()
373+ return
374+ }
375+ if tm.renamingTab != tab {
376+ logging.Debug(fmt.Sprintf("[tabs] finishInlineRename called for wrong tab (expected %s, got %s)",
377+ tm.renamingTab.id, tab.id))
378+ tm.mu.Unlock()
379+ return
380+ }
381+
382+ // Clear rename state immediately to prevent re-entry
383+ tm.renameInProgress = false
384+ tm.renamingTab = nil
385+ tm.mu.Unlock()
386+
387+ webkit.RunOnMainThread(func() {
388+ button, ok := tab.titleButton.(*gtk.Button)
389+ if !ok {
390+ logging.Error(fmt.Sprintf("[tabs] Tab button is not a *gtk.Button during finish: %s", tab.id))
391+ return
392+ }
393+
394+ // Restore original label
395+ button.SetChild(originalLabel)
396+
397+ // Restore original focus behavior now that rename is finished
398+ tm.mu.Lock()
399+ prevFocusOnClick := tm.renamePrevFocusOnClick
400+ prevCanFocus := tm.renamePrevCanFocus
401+ tm.renamePrevFocusOnClick = false
402+ tm.renamePrevCanFocus = false
403+ tm.mu.Unlock()
404+ button.SetFocusOnClick(prevFocusOnClick)
405+ button.SetCanFocus(prevCanFocus)
406+
407+ // Save new name if provided and not just whitespace
408+ newName = trimWhitespace(newName)
409+ if newName != "" {
410+ tab.customTitle = newName
411+ logging.Info(fmt.Sprintf("[tabs] Tab %s renamed to '%s'", tab.id, newName))
412+ } else {
413+ logging.Debug(fmt.Sprintf("[tabs] Tab %s rename cancelled", tab.id))
414+ }
415+
416+ // Update label text
417+ tm.updateTabButton(tab)
418+
419+ // Exit tab mode
420+ tm.ExitTabMode("rename-complete")
421+ })
422+}
423+
424+// trimWhitespace removes leading and trailing whitespace from a string.
425+func trimWhitespace(s string) string {
426+ start := 0
427+ end := len(s)
428+
429+ // Trim leading whitespace
430+ for start < end && isWhitespace(s[start]) {
431+ start++
432+ }
433+
434+ // Trim trailing whitespace
435+ for end > start && isWhitespace(s[end-1]) {
436+ end--
437+ }
438+
439+ return s[start:end]
440+}
441+
442+// isWhitespace checks if a byte is a whitespace character.
443+func isWhitespace(b byte) bool {
444+ return b == ' ' || b == '\t' || b == '\n' || b == '\r'
445+}
@@ -6,6 +6,7 @@ import (
6 6
7 "github.com/bnema/dumber/internal/logging"7 "github.com/bnema/dumber/internal/logging"
8 "github.com/bnema/dumber/pkg/webkit"8 "github.com/bnema/dumber/pkg/webkit"
9+ "github.com/diamondburned/gotk4/pkg/gdk/v4"
9 "github.com/diamondburned/gotk4/pkg/gtk/v4"10 "github.com/diamondburned/gotk4/pkg/gtk/v4"
10 )11 )
11 12
@@ -238,3 +239,207 @@ func (tm *TabManager) setTabActiveStyle(tab *Tab, active bool) {
238 }239 }
239 })240 })
240 }241 }
242+
243+// startInlineRename replaces the tab button's label with an entry widget for inline editing.
244+func (tm *TabManager) startInlineRename(tab *Tab) {
245+ if tab.titleButton == nil {
246+ logging.Error(fmt.Sprintf("[tabs] Cannot start rename: button is nil for tab %s", tab.id))
247+ return
248+ }
249+
250+ // Check if rename already in progress
251+ tm.mu.Lock()
252+ if tm.renameInProgress {
253+ logging.Debug("[tabs] Rename already in progress, ignoring")
254+ tm.mu.Unlock()
255+ return
256+ }
257+ tm.renameInProgress = true
258+ tm.renamingTab = tab
259+
260+ // Stop tab mode timer during rename
261+ if tm.tabModeTimer != nil {
262+ tm.tabModeTimer.Stop()
263+ tm.tabModeTimer = nil
264+ }
265+ tm.mu.Unlock()
266+
267+ webkit.RunOnMainThread(func() {
268+ button, ok := tab.titleButton.(*gtk.Button)
269+ if !ok {
270+ logging.Error(fmt.Sprintf("[tabs] Tab button is not a *gtk.Button: %s", tab.id))
271+ tm.cancelRename()
272+ return
273+ }
274+
275+ // Get current text
276+ currentText := tab.title
277+ if tab.customTitle != "" {
278+ currentText = tab.customTitle
279+ }
280+
281+ // Store original label for restoration
282+ originalLabel := button.Child()
283+
284+ // Create entry widget
285+ entry := gtk.NewEntry()
286+ if entry == nil {
287+ logging.Error("[tabs] Failed to create entry widget for rename")
288+ tm.cancelRename()
289+ return
290+ }
291+
292+ // Store current focus behavior so we can restore it later
293+ prevFocusOnClick := button.FocusOnClick()
294+ prevCanFocus := button.CanFocus()
295+ tm.mu.Lock()
296+ tm.renamePrevFocusOnClick = prevFocusOnClick
297+ tm.renamePrevCanFocus = prevCanFocus
298+ tm.mu.Unlock()
299+
300+ // Allow the entry to receive focus while rename is active
301+ button.SetFocusOnClick(true)
302+ button.SetCanFocus(true)
303+
304+ // Configure entry BEFORE adding to widget tree
305+ entry.SetText(currentText)
306+ entry.AddCSSClass("tab-rename-entry")
307+ entry.SetCanFocus(true) // Explicitly allow focus
308+ entry.SetFocusable(true) // GTK4 focusability
309+
310+ // Handle Enter key - save
311+ entry.ConnectActivate(func() {
312+ newName := entry.Text()
313+ logging.Debug(fmt.Sprintf("[tabs] Rename activate (Enter): '%s' -> '%s'", currentText, newName))
314+ tm.finishInlineRename(tab, newName, originalLabel)
315+ })
316+
317+ // Handle Escape key - cancel
318+ keyController := gtk.NewEventControllerKey()
319+ keyController.ConnectKeyPressed(func(keyval, keycode uint, state gdk.ModifierType) bool {
320+ const gdkKeyEscape = 0xff1b
321+ if keyval == gdkKeyEscape {
322+ logging.Debug("[tabs] Rename cancelled (Escape)")
323+ tm.finishInlineRename(tab, "", originalLabel) // Empty = cancel
324+ return true
325+ }
326+ return false
327+ })
328+ entry.AddController(keyController)
329+
330+ // Handle focus-out - cancel (safety)
331+ focusController := gtk.NewEventControllerFocus()
332+ focusController.ConnectLeave(func() {
333+ logging.Debug("[tabs] Rename cancelled (focus-out)")
334+ tm.finishInlineRename(tab, "", originalLabel)
335+ })
336+ entry.AddController(focusController)
337+
338+ // Replace label with entry
339+ button.SetChild(entry)
340+
341+ // Make entry visible first
342+ webkit.WidgetSetVisible(entry, true)
343+
344+ // Grab focus after widget is visible and realized
345+ // Use idle callback to ensure widget is fully realized
346+ entry.GrabFocus()
347+
348+ // Select all text after focus
349+ entry.SelectRegion(0, -1)
350+
351+ logging.Debug(fmt.Sprintf("[tabs] Started inline rename for tab %s", tab.id))
352+ })
353+}
354+
355+// cancelRename clears the rename state without changing anything
356+func (tm *TabManager) cancelRename() {
357+ tm.mu.Lock()
358+ defer tm.mu.Unlock()
359+ tm.renameInProgress = false
360+ tm.renamingTab = nil
361+ tm.renamePrevFocusOnClick = false
362+ tm.renamePrevCanFocus = false
363+ logging.Debug("[tabs] Rename state cleared")
364+}
365+
366+// finishInlineRename restores the label and saves the new name if provided.
367+func (tm *TabManager) finishInlineRename(tab *Tab, newName string, originalLabel gtk.Widgetter) {
368+ // Guard against multiple calls
369+ tm.mu.Lock()
370+ if !tm.renameInProgress {
371+ logging.Debug("[tabs] finishInlineRename called but no rename in progress, ignoring")
372+ tm.mu.Unlock()
373+ return
374+ }
375+ if tm.renamingTab != tab {
376+ logging.Debug(fmt.Sprintf("[tabs] finishInlineRename called for wrong tab (expected %s, got %s)",
377+ tm.renamingTab.id, tab.id))
378+ tm.mu.Unlock()
379+ return
380+ }
381+
382+ // Clear rename state immediately to prevent re-entry
383+ tm.renameInProgress = false
384+ tm.renamingTab = nil
385+ tm.mu.Unlock()
386+
387+ webkit.RunOnMainThread(func() {
388+ button, ok := tab.titleButton.(*gtk.Button)
389+ if !ok {
390+ logging.Error(fmt.Sprintf("[tabs] Tab button is not a *gtk.Button during finish: %s", tab.id))
391+ return
392+ }
393+
394+ // Restore original label
395+ button.SetChild(originalLabel)
396+
397+ // Restore original focus behavior now that rename is finished
398+ tm.mu.Lock()
399+ prevFocusOnClick := tm.renamePrevFocusOnClick
400+ prevCanFocus := tm.renamePrevCanFocus
401+ tm.renamePrevFocusOnClick = false
402+ tm.renamePrevCanFocus = false
403+ tm.mu.Unlock()
404+ button.SetFocusOnClick(prevFocusOnClick)
405+ button.SetCanFocus(prevCanFocus)
406+
407+ // Save new name if provided and not just whitespace
408+ newName = trimWhitespace(newName)
409+ if newName != "" {
410+ tab.customTitle = newName
411+ logging.Info(fmt.Sprintf("[tabs] Tab %s renamed to '%s'", tab.id, newName))
412+ } else {
413+ logging.Debug(fmt.Sprintf("[tabs] Tab %s rename cancelled", tab.id))
414+ }
415+
416+ // Update label text
417+ tm.updateTabButton(tab)
418+
419+ // Exit tab mode
420+ tm.ExitTabMode("rename-complete")
421+ })
422+}
423+
424+// trimWhitespace removes leading and trailing whitespace from a string.
425+func trimWhitespace(s string) string {
426+ start := 0
427+ end := len(s)
428+
429+ // Trim leading whitespace
430+ for start < end && isWhitespace(s[start]) {
431+ start++
432+ }
433+
434+ // Trim trailing whitespace
435+ for end > start && isWhitespace(s[end-1]) {
436+ end--
437+ }
438+
439+ return s[start:end]
440+}
441+
442+// isWhitespace checks if a byte is a whitespace character.
443+func isWhitespace(b byte) bool {
444+ return b == ' ' || b == '\t' || b == '\n' || b == '\r'
445+}
M internal/app/browser/tab_manager.go +15 -0
@@ -29,6 +29,13 @@ type TabManager struct {
2929 tabModeActive bool
3030 tabModeTimer *time.Timer
3131
32+ // Rename state
33+ renameInProgress bool
34+ renamingTab *Tab
35+ // Stores original focus behaviour while inline rename is active
36+ renamePrevFocusOnClick bool
37+ renamePrevCanFocus bool
38+
3239 // Tab button click handling (pattern from StackedPaneManager)
3340 buttonToTab map[uint64]*Tab // Maps button ID to tab for click handling
3441 nextButtonID uint64 // Atomic counter for generating unique button IDs
@@ -453,6 +460,14 @@ func (tm *TabManager) IsTabModeActive() bool {
453460 return tm.tabModeActive
454461 }
455462
463+// IsRenameInProgress returns whether a tab rename is currently in progress.
464+func (tm *TabManager) IsRenameInProgress() bool {
465+ tm.mu.RLock()
466+ defer tm.mu.RUnlock()
467+
468+ return tm.renameInProgress
469+}
470+
456471 // NextTab switches to the next tab (wraps around).
457472 func (tm *TabManager) NextTab() error {
458473 tm.mu.Lock()
@@ -29,6 +29,13 @@ type TabManager struct {
29 tabModeActive bool29 tabModeActive bool
30 tabModeTimer *time.Timer30 tabModeTimer *time.Timer
31 31
32+ // Rename state
33+ renameInProgress bool
34+ renamingTab *Tab
35+ // Stores original focus behaviour while inline rename is active
36+ renamePrevFocusOnClick bool
37+ renamePrevCanFocus bool
38+
32 // Tab button click handling (pattern from StackedPaneManager)39 // Tab button click handling (pattern from StackedPaneManager)
33 buttonToTab map[uint64]*Tab // Maps button ID to tab for click handling40 buttonToTab map[uint64]*Tab // Maps button ID to tab for click handling
34 nextButtonID uint64 // Atomic counter for generating unique button IDs41 nextButtonID uint64 // Atomic counter for generating unique button IDs
@@ -453,6 +460,14 @@ func (tm *TabManager) IsTabModeActive() bool {
453 return tm.tabModeActive460 return tm.tabModeActive
454 }461 }
455 462
463+// IsRenameInProgress returns whether a tab rename is currently in progress.
464+func (tm *TabManager) IsRenameInProgress() bool {
465+ tm.mu.RLock()
466+ defer tm.mu.RUnlock()
467+
468+ return tm.renameInProgress
469+}
470+
456 // NextTab switches to the next tab (wraps around).471 // NextTab switches to the next tab (wraps around).
457 func (tm *TabManager) NextTab() error {472 func (tm *TabManager) NextTab() error {
458 tm.mu.Lock()473 tm.mu.Lock()
M internal/app/browser/tab_mode.go +15 -19
@@ -172,37 +172,33 @@ func (tm *TabManager) handlePreviousTab() {
172172 }
173173 }
174174
175-// handleRenameTab shows a dialog to rename the active tab.
175+// handleRenameTab starts inline editing of the active tab's name.
176176 func (tm *TabManager) handleRenameTab() {
177- logging.Debug("[tab-mode] Opening rename dialog")
178-
179- // Exit tab mode first so the dialog can be interacted with
180- tm.ExitTabMode("rename-dialog")
177+ logging.Debug("[tab-mode] Starting inline tab rename")
181178
182179 tm.mu.RLock()
183180 activeIndex := tm.activeIndex
184- var currentTitle string
181+ tabCount := len(tm.tabs)
182+ var activeTab *Tab
185183 if activeIndex >= 0 && activeIndex < len(tm.tabs) {
186- tab := tm.tabs[activeIndex]
187- if tab.customTitle != "" {
188- currentTitle = tab.customTitle
189- } else {
190- currentTitle = tab.title
191- }
184+ activeTab = tm.tabs[activeIndex]
192185 }
193186 tm.mu.RUnlock()
194187
195- if activeIndex < 0 {
196- logging.Error("[tab-mode] No active tab to rename")
188+ // Guard: Don't allow rename if only 1 tab (tab bar is hidden)
189+ if tabCount <= 1 {
190+ logging.Warn("[tab-mode] Cannot rename: only one tab (tab bar hidden)")
197191 return
198192 }
199193
200- // Show rename dialog (will be implemented in Step 10)
201- // For now, just log
202- logging.Info(fmt.Sprintf("[tab-mode] Would show rename dialog for tab %d (current: %s)", activeIndex, currentTitle))
194+ if activeTab == nil {
195+ logging.Error("[tab-mode] No active tab to rename")
196+ return
197+ }
203198
204- // TODO: Implement GTK dialog in Step 10
205- // tm.showRenameDialog(activeIndex, currentTitle)
199+ // Start inline rename (tab mode will be exited by finishInlineRename)
200+ logging.Info(fmt.Sprintf("[tab-mode] Starting inline rename for tab %s", activeTab.id))
201+ tm.startInlineRename(activeTab)
206202 }
207203
208204 // applyTabModeBorder applies visual indicator when tab mode is active.
@@ -172,37 +172,33 @@ func (tm *TabManager) handlePreviousTab() {
172 }172 }
173 }173 }
174 174
175-// handleRenameTab shows a dialog to rename the active tab.175+// handleRenameTab starts inline editing of the active tab's name.
176 func (tm *TabManager) handleRenameTab() {176 func (tm *TabManager) handleRenameTab() {
177- logging.Debug("[tab-mode] Opening rename dialog")177+ logging.Debug("[tab-mode] Starting inline tab rename")
178-
179- // Exit tab mode first so the dialog can be interacted with
180- tm.ExitTabMode("rename-dialog")
181 178
182 tm.mu.RLock()179 tm.mu.RLock()
183 activeIndex := tm.activeIndex180 activeIndex := tm.activeIndex
184- var currentTitle string181+ tabCount := len(tm.tabs)
182+ var activeTab *Tab
185 if activeIndex >= 0 && activeIndex < len(tm.tabs) {183 if activeIndex >= 0 && activeIndex < len(tm.tabs) {
186- tab := tm.tabs[activeIndex]184+ activeTab = tm.tabs[activeIndex]
187- if tab.customTitle != "" {
188- currentTitle = tab.customTitle
189- } else {
190- currentTitle = tab.title
191- }
192 }185 }
193 tm.mu.RUnlock()186 tm.mu.RUnlock()
194 187
195- if activeIndex < 0 {188+ // Guard: Don't allow rename if only 1 tab (tab bar is hidden)
196- logging.Error("[tab-mode] No active tab to rename")189+ if tabCount <= 1 {
190+ logging.Warn("[tab-mode] Cannot rename: only one tab (tab bar hidden)")
197 return191 return
198 }192 }
199 193
200- // Show rename dialog (will be implemented in Step 10)194+ if activeTab == nil {
201- // For now, just log195+ logging.Error("[tab-mode] No active tab to rename")
202- logging.Info(fmt.Sprintf("[tab-mode] Would show rename dialog for tab %d (current: %s)", activeIndex, currentTitle))196+ return
197+ }
203 198
204- // TODO: Implement GTK dialog in Step 10199+ // Start inline rename (tab mode will be exited by finishInlineRename)
205- // tm.showRenameDialog(activeIndex, currentTitle)200+ logging.Info(fmt.Sprintf("[tab-mode] Starting inline rename for tab %s", activeTab.id))
201+ tm.startInlineRename(activeTab)
206 }202 }
207 203
208 // applyTabModeBorder applies visual indicator when tab mode is active.204 // applyTabModeBorder applies visual indicator when tab mode is active.
M internal/app/browser/window_shortcuts.go +15 -0
@@ -187,8 +187,15 @@ func (h *WindowShortcutHandler) registerKeycodeShortcuts() error {
187187 }
188188
189189 // Handle tab mode action keys (only when tab mode is active)
190+ // IMPORTANT: If rename is in progress, let ALL keys through to the Entry widget
190191 // These keys need to be checked here so they can propagate when tab mode is not active
191192 if h.isTabModeActive() {
193+ // Check if rename is in progress - if so, don't intercept ANY keys
194+ // This allows the Entry widget to receive all keyboard input
195+ if h.isRenameInProgress() {
196+ return false // Let all keys propagate to Entry widget
197+ }
198+
192199 handled := false
193200 action := ""
194201
@@ -630,6 +637,14 @@ func (h *WindowShortcutHandler) isTabModeActive() bool {
630637 return h.app.tabManager.IsTabModeActive()
631638 }
632639
640+// isRenameInProgress checks if a tab rename is currently in progress
641+func (h *WindowShortcutHandler) isRenameInProgress() bool {
642+ if h.app == nil || h.app.tabManager == nil {
643+ return false
644+ }
645+ return h.app.tabManager.IsRenameInProgress()
646+}
647+
633648 // handleTabModeAction handles tab mode action keys (n, x, l, h, etc.)
634649 // Only processes actions when tab mode is active
635650 func (h *WindowShortcutHandler) handleTabModeAction(action string) {
@@ -187,8 +187,15 @@ func (h *WindowShortcutHandler) registerKeycodeShortcuts() error {
187 }187 }
188 188
189 // Handle tab mode action keys (only when tab mode is active)189 // Handle tab mode action keys (only when tab mode is active)
190+ // IMPORTANT: If rename is in progress, let ALL keys through to the Entry widget
190 // These keys need to be checked here so they can propagate when tab mode is not active191 // These keys need to be checked here so they can propagate when tab mode is not active
191 if h.isTabModeActive() {192 if h.isTabModeActive() {
193+ // Check if rename is in progress - if so, don't intercept ANY keys
194+ // This allows the Entry widget to receive all keyboard input
195+ if h.isRenameInProgress() {
196+ return false // Let all keys propagate to Entry widget
197+ }
198+
192 handled := false199 handled := false
193 action := ""200 action := ""
194 201
@@ -630,6 +637,14 @@ func (h *WindowShortcutHandler) isTabModeActive() bool {
630 return h.app.tabManager.IsTabModeActive()637 return h.app.tabManager.IsTabModeActive()
631 }638 }
632 639
640+// isRenameInProgress checks if a tab rename is currently in progress
641+func (h *WindowShortcutHandler) isRenameInProgress() bool {
642+ if h.app == nil || h.app.tabManager == nil {
643+ return false
644+ }
645+ return h.app.tabManager.IsRenameInProgress()
646+}
647+
633 // handleTabModeAction handles tab mode action keys (n, x, l, h, etc.)648 // handleTabModeAction handles tab mode action keys (n, x, l, h, etc.)
634 // Only processes actions when tab mode is active649 // Only processes actions when tab mode is active
635 func (h *WindowShortcutHandler) handleTabModeAction(action string) {650 func (h *WindowShortcutHandler) handleTabModeAction(action string) {

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