detached 5a2eb46

feat(workspace): add favicon display and click navigation to stacked pane title bars

commit
5a2eb4637d3e2a3c36f997ae9a392c661c806f21
author
bamen · · 2025-10-06 16:18 UTC
committer
bamen · · 2025-10-06 19:18 UTC
parent
b184813
signature
unsigned
3 files · +165 -37
M internal/app/browser/stacked_panes.go +152 -37
@@ -1,23 +1,29 @@
11 package browser
22
33 import (
4+ "context"
45 "errors"
56 "fmt"
67 "log"
8+ "sync/atomic"
79 "time"
810
11+ "github.com/bnema/dumber/internal/cache"
912 "github.com/bnema/dumber/pkg/webkit"
1013 )
1114
1215 // StackedPaneManager handles all stacked pane operations
1316 type StackedPaneManager struct {
14- wm *WorkspaceManager
17+ wm *WorkspaceManager
18+ titleBarToPane map[uintptr]*paneNode
19+ nextTitleBarID uint64
1520 }
1621
1722 // NewStackedPaneManager creates a new stacked pane manager
1823 func NewStackedPaneManager(wm *WorkspaceManager) *StackedPaneManager {
1924 return &StackedPaneManager{
20- wm: wm,
25+ wm: wm,
26+ titleBarToPane: make(map[uintptr]*paneNode),
2127 }
2228 }
2329
@@ -409,6 +415,15 @@ func (spm *StackedPaneManager) UpdateStackVisibility(stackNode *paneNode) {
409415 }
410416 if pane.titleBar != 0 {
411417 webkit.WidgetSetVisible(pane.titleBar, true)
418+
419+ // Refresh title bar to pick up any newly downloaded favicons
420+ if pane.pane != nil && pane.pane.webView != nil {
421+ title := pane.pane.webView.GetTitle()
422+ if title == "" {
423+ title = "New Tab"
424+ }
425+ spm.updateTitleBarLabel(pane, title)
426+ }
412427 }
413428 }
414429 }
@@ -494,43 +509,31 @@ func (spm *StackedPaneManager) NavigateStack(direction string) bool {
494509
495510 // createTitleBar creates a title bar widget for a pane in a stack
496511 func (spm *StackedPaneManager) createTitleBar(pane *paneNode) uintptr {
497- titleBox := webkit.NewBox(webkit.OrientationHorizontal, 8)
498- if titleBox == 0 {
499- log.Printf("[workspace] failed to create title box")
500- return 0
501- }
502-
503- webkit.WidgetAddCSSClass(titleBox, "stacked-pane-title")
504- webkit.WidgetSetHExpand(titleBox, true)
505- webkit.WidgetSetVExpand(titleBox, false)
506-
507- // Get the actual title from the WebView instead of hardcoding "New Tab"
508- var titleText string
512+ // Get the actual title and URL from the WebView
513+ var titleText, pageURL string
509514 if pane.pane != nil && pane.pane.webView != nil {
510515 titleText = pane.pane.webView.GetTitle()
511516 if titleText == "" {
512517 titleText = "New Tab" // Fallback only when title is actually empty
513518 }
519+ pageURL = pane.pane.webView.GetCurrentURL()
514520 } else {
515521 titleText = "New Tab"
516522 }
517523
518- // Create title label with the actual title
519- titleLabel := webkit.NewLabel(titleText)
520- if titleLabel == 0 {
521- log.Printf("[workspace] failed to create title label")
522- return titleBox
523- }
524-
525- webkit.WidgetAddCSSClass(titleLabel, "stacked-pane-title-text")
526- webkit.LabelSetMaxWidthChars(titleLabel, 50)
527- webkit.LabelSetEllipsize(titleLabel, webkit.EllipsizeEnd)
524+ titleBar := spm.createTitleBarWithTitle(titleText, pageURL)
525+ if titleBar != 0 {
526+ // Store the mapping from titleBar ID to pane
527+ titleBarID := atomic.AddUint64(&spm.nextTitleBarID, 1)
528+ spm.titleBarToPane[uintptr(titleBarID)] = pane
528529
529- webkit.BoxAppend(titleBox, titleLabel)
530- webkit.WidgetShow(titleBox)
531- webkit.WidgetShow(titleLabel)
530+ // Attach click handler
531+ webkit.WidgetAttachClickHandler(titleBar, func() {
532+ spm.handleTitleBarClick(uintptr(titleBarID))
533+ })
534+ }
532535
533- return titleBox
536+ return titleBar
534537 }
535538
536539 // UpdateTitleBar updates the title bar label for a WebView in stacked panes
@@ -578,19 +581,28 @@ func (spm *StackedPaneManager) updateTitleBarLabel(node *paneNode, title string)
578581 return
579582 }
580583
581- // Format the title for display
582- displayTitle := title
583- if len(displayTitle) > 50 {
584- displayTitle = displayTitle[:47] + "..."
584+ // Get the current URL from the WebView for favicon
585+ var pageURL string
586+ if node.pane != nil && node.pane.webView != nil {
587+ pageURL = node.pane.webView.GetCurrentURL()
585588 }
586589
587- // Create new title bar with updated title
588- newTitleBar := spm.createTitleBarWithTitle(displayTitle)
590+ // Create new title bar with updated title and favicon
591+ newTitleBar := spm.createTitleBarWithTitle(title, pageURL)
589592 if newTitleBar == 0 {
590593 log.Printf("[workspace] failed to create new title bar")
591594 return
592595 }
593596
597+ // Store the mapping from titleBar ID to pane and attach click handler
598+ titleBarID := atomic.AddUint64(&spm.nextTitleBarID, 1)
599+ spm.titleBarToPane[uintptr(titleBarID)] = node
600+
601+ // Attach click handler
602+ webkit.WidgetAttachClickHandler(newTitleBar, func() {
603+ spm.handleTitleBarClick(uintptr(titleBarID))
604+ })
605+
594606 // Replace the old title bar in the stack
595607 if node.parent != nil && node.parent.isStacked && node.parent.stackWrapper != 0 {
596608 // Find the correct insertion position based on the pane's index in the stack
@@ -628,8 +640,8 @@ func (spm *StackedPaneManager) updateTitleBarLabel(node *paneNode, title string)
628640 }
629641 }
630642
631-// createTitleBarWithTitle creates a title bar with a specific title
632-func (spm *StackedPaneManager) createTitleBarWithTitle(title string) uintptr {
643+// createTitleBarWithTitle creates a title bar with a specific title and optional favicon
644+func (spm *StackedPaneManager) createTitleBarWithTitle(title string, pageURL string) uintptr {
633645 titleBox := webkit.NewBox(webkit.OrientationHorizontal, 8)
634646 if titleBox == 0 {
635647 return 0
@@ -639,6 +651,75 @@ func (spm *StackedPaneManager) createTitleBarWithTitle(title string) uintptr {
639651 webkit.WidgetSetHExpand(titleBox, true)
640652 webkit.WidgetSetVExpand(titleBox, false)
641653
654+ // Add favicon placeholder for debugging (always visible)
655+ var faviconImg uintptr
656+ var debugReason string
657+
658+ if pageURL == "" {
659+ debugReason = "empty pageURL"
660+ } else if spm.wm == nil {
661+ debugReason = "nil workspace manager"
662+ } else if spm.wm.app == nil {
663+ debugReason = "nil app"
664+ } else if spm.wm.app.queries == nil {
665+ debugReason = "nil queries"
666+ } else {
667+ // Query database for favicon URL
668+ ctx := context.Background()
669+ entry, err := spm.wm.app.queries.GetHistoryEntry(ctx, pageURL)
670+ if err != nil {
671+ debugReason = fmt.Sprintf("DB query failed: %v", err)
672+ log.Printf("[favicon] Failed to get history entry for %s: %v", pageURL, err)
673+ } else if !entry.FaviconUrl.Valid || entry.FaviconUrl.String == "" {
674+ debugReason = "no favicon URL in DB"
675+ log.Printf("[favicon] No favicon URL in DB for %s", pageURL)
676+ } else {
677+ faviconURL := entry.FaviconUrl.String
678+ log.Printf("[favicon] Found favicon URL in DB for %s: %s", pageURL, faviconURL)
679+
680+ // Use favicon cache with favicon URL (same as dmenu)
681+ faviconCache, err := cache.NewFaviconCache()
682+ if err != nil {
683+ debugReason = fmt.Sprintf("cache init failed: %v", err)
684+ log.Printf("[favicon] Failed to create cache: %v", err)
685+ } else {
686+ faviconPath := faviconCache.GetCachedPath(faviconURL)
687+ if faviconPath != "" {
688+ log.Printf("[favicon] Loading cached favicon from: %s", faviconPath)
689+ faviconImg = webkit.ImageNewFromFile(faviconPath)
690+ if faviconImg != 0 {
691+ webkit.ImageSetPixelSize(faviconImg, 16)
692+ webkit.WidgetAddCSSClass(faviconImg, "stacked-pane-favicon")
693+ log.Printf("[favicon] Successfully created favicon image widget")
694+ } else {
695+ debugReason = "image widget creation failed"
696+ log.Printf("[favicon] Failed to create image widget from %s", faviconPath)
697+ }
698+ } else {
699+ debugReason = "not cached yet"
700+ log.Printf("[favicon] Favicon not cached yet, starting download: %s", faviconURL)
701+ // Start async download for next time (same as dmenu)
702+ faviconCache.CacheAsync(faviconURL)
703+ }
704+ }
705+ }
706+ }
707+
708+ // Add favicon or placeholder
709+ if faviconImg != 0 {
710+ webkit.BoxAppend(titleBox, faviconImg)
711+ webkit.WidgetShow(faviconImg)
712+ } else {
713+ // DEBUG: Add visible placeholder label to show code is running
714+ placeholderLabel := webkit.NewLabel("🌐")
715+ if placeholderLabel != 0 {
716+ webkit.WidgetAddCSSClass(placeholderLabel, "stacked-pane-favicon-placeholder")
717+ webkit.BoxAppend(titleBox, placeholderLabel)
718+ webkit.WidgetShow(placeholderLabel)
719+ log.Printf("[favicon] Using placeholder for %s (reason: %s)", pageURL, debugReason)
720+ }
721+ }
722+
642723 // Create title label with the specified title
643724 titleLabel := webkit.NewLabel(title)
644725 if titleLabel == 0 {
@@ -646,7 +727,6 @@ func (spm *StackedPaneManager) createTitleBarWithTitle(title string) uintptr {
646727 }
647728
648729 webkit.WidgetAddCSSClass(titleLabel, "stacked-pane-title-text")
649- webkit.LabelSetMaxWidthChars(titleLabel, 50)
650730 webkit.LabelSetEllipsize(titleLabel, webkit.EllipsizeEnd)
651731
652732 webkit.BoxAppend(titleBox, titleLabel)
@@ -852,3 +932,38 @@ func (spm *StackedPaneManager) CloseStackedPane(node *paneNode) error {
852932 return nil
853933 }
854934 }
935+
936+// handleTitleBarClick handles clicks on title bars to switch the active pane
937+func (spm *StackedPaneManager) handleTitleBarClick(titleBarID uintptr) {
938+ pane, exists := spm.titleBarToPane[titleBarID]
939+ if !exists || pane == nil || pane.parent == nil || !pane.parent.isStacked {
940+ return
941+ }
942+
943+ stackNode := pane.parent
944+
945+ // Find the index of the clicked pane
946+ for i, p := range stackNode.stackedPanes {
947+ if p == pane {
948+ // Only switch if it's not already active
949+ if i != stackNode.activeStackIndex {
950+ log.Printf("[workspace] title bar clicked: switching from pane %d to pane %d",
951+ stackNode.activeStackIndex, i)
952+
953+ // Update title bar for the pane transitioning from ACTIVE to INACTIVE
954+ currentActivePane := stackNode.stackedPanes[stackNode.activeStackIndex]
955+ if currentActivePane.pane != nil && currentActivePane.pane.webView != nil && currentActivePane.titleBar != 0 {
956+ currentTitle := currentActivePane.pane.webView.GetTitle()
957+ if currentTitle != "" {
958+ spm.updateTitleBarLabel(currentActivePane, currentTitle)
959+ }
960+ }
961+
962+ stackNode.activeStackIndex = i
963+ spm.UpdateStackVisibility(stackNode)
964+ spm.wm.SetActivePane(pane, SourceStackNav)
965+ }
966+ return
967+ }
968+ }
969+}
@@ -1,23 +1,29 @@
1 package browser1 package browser
2 2
3 import (3 import (
4+ "context"
4 "errors"5 "errors"
5 "fmt"6 "fmt"
6 "log"7 "log"
8+ "sync/atomic"
7 "time"9 "time"
8 10
11+ "github.com/bnema/dumber/internal/cache"
9 "github.com/bnema/dumber/pkg/webkit"12 "github.com/bnema/dumber/pkg/webkit"
10 )13 )
11 14
12 // StackedPaneManager handles all stacked pane operations15 // StackedPaneManager handles all stacked pane operations
13 type StackedPaneManager struct {16 type StackedPaneManager struct {
14- wm *WorkspaceManager17+ wm *WorkspaceManager
18+ titleBarToPane map[uintptr]*paneNode
19+ nextTitleBarID uint64
15 }20 }
16 21
17 // NewStackedPaneManager creates a new stacked pane manager22 // NewStackedPaneManager creates a new stacked pane manager
18 func NewStackedPaneManager(wm *WorkspaceManager) *StackedPaneManager {23 func NewStackedPaneManager(wm *WorkspaceManager) *StackedPaneManager {
19 return &StackedPaneManager{24 return &StackedPaneManager{
20- wm: wm,25+ wm: wm,
26+ titleBarToPane: make(map[uintptr]*paneNode),
21 }27 }
22 }28 }
23 29
@@ -409,6 +415,15 @@ func (spm *StackedPaneManager) UpdateStackVisibility(stackNode *paneNode) {
409 }415 }
410 if pane.titleBar != 0 {416 if pane.titleBar != 0 {
411 webkit.WidgetSetVisible(pane.titleBar, true)417 webkit.WidgetSetVisible(pane.titleBar, true)
418+
419+ // Refresh title bar to pick up any newly downloaded favicons
420+ if pane.pane != nil && pane.pane.webView != nil {
421+ title := pane.pane.webView.GetTitle()
422+ if title == "" {
423+ title = "New Tab"
424+ }
425+ spm.updateTitleBarLabel(pane, title)
426+ }
412 }427 }
413 }428 }
414 }429 }
@@ -494,43 +509,31 @@ func (spm *StackedPaneManager) NavigateStack(direction string) bool {
494 509
495 // createTitleBar creates a title bar widget for a pane in a stack510 // createTitleBar creates a title bar widget for a pane in a stack
496 func (spm *StackedPaneManager) createTitleBar(pane *paneNode) uintptr {511 func (spm *StackedPaneManager) createTitleBar(pane *paneNode) uintptr {
497- titleBox := webkit.NewBox(webkit.OrientationHorizontal, 8)512+ // Get the actual title and URL from the WebView
498- if titleBox == 0 {513+ var titleText, pageURL string
499- log.Printf("[workspace] failed to create title box")
500- return 0
501- }
502-
503- webkit.WidgetAddCSSClass(titleBox, "stacked-pane-title")
504- webkit.WidgetSetHExpand(titleBox, true)
505- webkit.WidgetSetVExpand(titleBox, false)
506-
507- // Get the actual title from the WebView instead of hardcoding "New Tab"
508- var titleText string
509 if pane.pane != nil && pane.pane.webView != nil {514 if pane.pane != nil && pane.pane.webView != nil {
510 titleText = pane.pane.webView.GetTitle()515 titleText = pane.pane.webView.GetTitle()
511 if titleText == "" {516 if titleText == "" {
512 titleText = "New Tab" // Fallback only when title is actually empty517 titleText = "New Tab" // Fallback only when title is actually empty
513 }518 }
519+ pageURL = pane.pane.webView.GetCurrentURL()
514 } else {520 } else {
515 titleText = "New Tab"521 titleText = "New Tab"
516 }522 }
517 523
518- // Create title label with the actual title524+ titleBar := spm.createTitleBarWithTitle(titleText, pageURL)
519- titleLabel := webkit.NewLabel(titleText)525+ if titleBar != 0 {
520- if titleLabel == 0 {526+ // Store the mapping from titleBar ID to pane
521- log.Printf("[workspace] failed to create title label")527+ titleBarID := atomic.AddUint64(&spm.nextTitleBarID, 1)
522- return titleBox528+ spm.titleBarToPane[uintptr(titleBarID)] = pane
523- }
524-
525- webkit.WidgetAddCSSClass(titleLabel, "stacked-pane-title-text")
526- webkit.LabelSetMaxWidthChars(titleLabel, 50)
527- webkit.LabelSetEllipsize(titleLabel, webkit.EllipsizeEnd)
528 529
529- webkit.BoxAppend(titleBox, titleLabel)530+ // Attach click handler
530- webkit.WidgetShow(titleBox)531+ webkit.WidgetAttachClickHandler(titleBar, func() {
531- webkit.WidgetShow(titleLabel)532+ spm.handleTitleBarClick(uintptr(titleBarID))
533+ })
534+ }
532 535
533- return titleBox536+ return titleBar
534 }537 }
535 538
536 // UpdateTitleBar updates the title bar label for a WebView in stacked panes539 // UpdateTitleBar updates the title bar label for a WebView in stacked panes
@@ -578,19 +581,28 @@ func (spm *StackedPaneManager) updateTitleBarLabel(node *paneNode, title string)
578 return581 return
579 }582 }
580 583
581- // Format the title for display584+ // Get the current URL from the WebView for favicon
582- displayTitle := title585+ var pageURL string
583- if len(displayTitle) > 50 {586+ if node.pane != nil && node.pane.webView != nil {
584- displayTitle = displayTitle[:47] + "..."587+ pageURL = node.pane.webView.GetCurrentURL()
585 }588 }
586 589
587- // Create new title bar with updated title590+ // Create new title bar with updated title and favicon
588- newTitleBar := spm.createTitleBarWithTitle(displayTitle)591+ newTitleBar := spm.createTitleBarWithTitle(title, pageURL)
589 if newTitleBar == 0 {592 if newTitleBar == 0 {
590 log.Printf("[workspace] failed to create new title bar")593 log.Printf("[workspace] failed to create new title bar")
591 return594 return
592 }595 }
593 596
597+ // Store the mapping from titleBar ID to pane and attach click handler
598+ titleBarID := atomic.AddUint64(&spm.nextTitleBarID, 1)
599+ spm.titleBarToPane[uintptr(titleBarID)] = node
600+
601+ // Attach click handler
602+ webkit.WidgetAttachClickHandler(newTitleBar, func() {
603+ spm.handleTitleBarClick(uintptr(titleBarID))
604+ })
605+
594 // Replace the old title bar in the stack606 // Replace the old title bar in the stack
595 if node.parent != nil && node.parent.isStacked && node.parent.stackWrapper != 0 {607 if node.parent != nil && node.parent.isStacked && node.parent.stackWrapper != 0 {
596 // Find the correct insertion position based on the pane's index in the stack608 // Find the correct insertion position based on the pane's index in the stack
@@ -628,8 +640,8 @@ func (spm *StackedPaneManager) updateTitleBarLabel(node *paneNode, title string)
628 }640 }
629 }641 }
630 642
631-// createTitleBarWithTitle creates a title bar with a specific title643+// createTitleBarWithTitle creates a title bar with a specific title and optional favicon
632-func (spm *StackedPaneManager) createTitleBarWithTitle(title string) uintptr {644+func (spm *StackedPaneManager) createTitleBarWithTitle(title string, pageURL string) uintptr {
633 titleBox := webkit.NewBox(webkit.OrientationHorizontal, 8)645 titleBox := webkit.NewBox(webkit.OrientationHorizontal, 8)
634 if titleBox == 0 {646 if titleBox == 0 {
635 return 0647 return 0
@@ -639,6 +651,75 @@ func (spm *StackedPaneManager) createTitleBarWithTitle(title string) uintptr {
639 webkit.WidgetSetHExpand(titleBox, true)651 webkit.WidgetSetHExpand(titleBox, true)
640 webkit.WidgetSetVExpand(titleBox, false)652 webkit.WidgetSetVExpand(titleBox, false)
641 653
654+ // Add favicon placeholder for debugging (always visible)
655+ var faviconImg uintptr
656+ var debugReason string
657+
658+ if pageURL == "" {
659+ debugReason = "empty pageURL"
660+ } else if spm.wm == nil {
661+ debugReason = "nil workspace manager"
662+ } else if spm.wm.app == nil {
663+ debugReason = "nil app"
664+ } else if spm.wm.app.queries == nil {
665+ debugReason = "nil queries"
666+ } else {
667+ // Query database for favicon URL
668+ ctx := context.Background()
669+ entry, err := spm.wm.app.queries.GetHistoryEntry(ctx, pageURL)
670+ if err != nil {
671+ debugReason = fmt.Sprintf("DB query failed: %v", err)
672+ log.Printf("[favicon] Failed to get history entry for %s: %v", pageURL, err)
673+ } else if !entry.FaviconUrl.Valid || entry.FaviconUrl.String == "" {
674+ debugReason = "no favicon URL in DB"
675+ log.Printf("[favicon] No favicon URL in DB for %s", pageURL)
676+ } else {
677+ faviconURL := entry.FaviconUrl.String
678+ log.Printf("[favicon] Found favicon URL in DB for %s: %s", pageURL, faviconURL)
679+
680+ // Use favicon cache with favicon URL (same as dmenu)
681+ faviconCache, err := cache.NewFaviconCache()
682+ if err != nil {
683+ debugReason = fmt.Sprintf("cache init failed: %v", err)
684+ log.Printf("[favicon] Failed to create cache: %v", err)
685+ } else {
686+ faviconPath := faviconCache.GetCachedPath(faviconURL)
687+ if faviconPath != "" {
688+ log.Printf("[favicon] Loading cached favicon from: %s", faviconPath)
689+ faviconImg = webkit.ImageNewFromFile(faviconPath)
690+ if faviconImg != 0 {
691+ webkit.ImageSetPixelSize(faviconImg, 16)
692+ webkit.WidgetAddCSSClass(faviconImg, "stacked-pane-favicon")
693+ log.Printf("[favicon] Successfully created favicon image widget")
694+ } else {
695+ debugReason = "image widget creation failed"
696+ log.Printf("[favicon] Failed to create image widget from %s", faviconPath)
697+ }
698+ } else {
699+ debugReason = "not cached yet"
700+ log.Printf("[favicon] Favicon not cached yet, starting download: %s", faviconURL)
701+ // Start async download for next time (same as dmenu)
702+ faviconCache.CacheAsync(faviconURL)
703+ }
704+ }
705+ }
706+ }
707+
708+ // Add favicon or placeholder
709+ if faviconImg != 0 {
710+ webkit.BoxAppend(titleBox, faviconImg)
711+ webkit.WidgetShow(faviconImg)
712+ } else {
713+ // DEBUG: Add visible placeholder label to show code is running
714+ placeholderLabel := webkit.NewLabel("🌐")
715+ if placeholderLabel != 0 {
716+ webkit.WidgetAddCSSClass(placeholderLabel, "stacked-pane-favicon-placeholder")
717+ webkit.BoxAppend(titleBox, placeholderLabel)
718+ webkit.WidgetShow(placeholderLabel)
719+ log.Printf("[favicon] Using placeholder for %s (reason: %s)", pageURL, debugReason)
720+ }
721+ }
722+
642 // Create title label with the specified title723 // Create title label with the specified title
643 titleLabel := webkit.NewLabel(title)724 titleLabel := webkit.NewLabel(title)
644 if titleLabel == 0 {725 if titleLabel == 0 {
@@ -646,7 +727,6 @@ func (spm *StackedPaneManager) createTitleBarWithTitle(title string) uintptr {
646 }727 }
647 728
648 webkit.WidgetAddCSSClass(titleLabel, "stacked-pane-title-text")729 webkit.WidgetAddCSSClass(titleLabel, "stacked-pane-title-text")
649- webkit.LabelSetMaxWidthChars(titleLabel, 50)
650 webkit.LabelSetEllipsize(titleLabel, webkit.EllipsizeEnd)730 webkit.LabelSetEllipsize(titleLabel, webkit.EllipsizeEnd)
651 731
652 webkit.BoxAppend(titleBox, titleLabel)732 webkit.BoxAppend(titleBox, titleLabel)
@@ -852,3 +932,38 @@ func (spm *StackedPaneManager) CloseStackedPane(node *paneNode) error {
852 return nil932 return nil
853 }933 }
854 }934 }
935+
936+// handleTitleBarClick handles clicks on title bars to switch the active pane
937+func (spm *StackedPaneManager) handleTitleBarClick(titleBarID uintptr) {
938+ pane, exists := spm.titleBarToPane[titleBarID]
939+ if !exists || pane == nil || pane.parent == nil || !pane.parent.isStacked {
940+ return
941+ }
942+
943+ stackNode := pane.parent
944+
945+ // Find the index of the clicked pane
946+ for i, p := range stackNode.stackedPanes {
947+ if p == pane {
948+ // Only switch if it's not already active
949+ if i != stackNode.activeStackIndex {
950+ log.Printf("[workspace] title bar clicked: switching from pane %d to pane %d",
951+ stackNode.activeStackIndex, i)
952+
953+ // Update title bar for the pane transitioning from ACTIVE to INACTIVE
954+ currentActivePane := stackNode.stackedPanes[stackNode.activeStackIndex]
955+ if currentActivePane.pane != nil && currentActivePane.pane.webView != nil && currentActivePane.titleBar != 0 {
956+ currentTitle := currentActivePane.pane.webView.GetTitle()
957+ if currentTitle != "" {
958+ spm.updateTitleBarLabel(currentActivePane, currentTitle)
959+ }
960+ }
961+
962+ stackNode.activeStackIndex = i
963+ spm.UpdateStackVisibility(stackNode)
964+ spm.wm.SetActivePane(pane, SourceStackNav)
965+ }
966+ return
967+ }
968+ }
969+}
M internal/app/browser/webview.go +9 -0
@@ -144,6 +144,15 @@ func (app *BrowserApp) buildPane(view *webkit.WebView) (*BrowserPane, error) {
144144 app.workspace.RegisterNavigationHandler(view)
145145 }
146146
147+ // Register favicon handlers for this webview (needed for each pane)
148+ // This ensures favicons are detected and cached for all panes, not just the main webview
149+ view.RegisterFaviconURIChangedHandler(func(pageURI, faviconURI string) {
150+ // Call the private handler through a public wrapper or directly via browserService
151+ // We can't call handleFaviconURIChanged directly as it's private, so we need to expose it
152+ // For now, duplicate the minimal logic here
153+ app.browserService.ProcessFaviconURI(pageURI, faviconURI)
154+ })
155+
147156 return pane, nil
148157 }
149158
@@ -144,6 +144,15 @@ func (app *BrowserApp) buildPane(view *webkit.WebView) (*BrowserPane, error) {
144 app.workspace.RegisterNavigationHandler(view)144 app.workspace.RegisterNavigationHandler(view)
145 }145 }
146 146
147+ // Register favicon handlers for this webview (needed for each pane)
148+ // This ensures favicons are detected and cached for all panes, not just the main webview
149+ view.RegisterFaviconURIChangedHandler(func(pageURI, faviconURI string) {
150+ // Call the private handler through a public wrapper or directly via browserService
151+ // We can't call handleFaviconURIChanged directly as it's private, so we need to expose it
152+ // For now, duplicate the minimal logic here
153+ app.browserService.ProcessFaviconURI(pageURI, faviconURI)
154+ })
155+
147 return pane, nil156 return pane, nil
148 }157 }
149 158
M internal/app/browser/workspace_css.go +4 -0
@@ -117,6 +117,10 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string {
117117 font-weight: 500;
118118 }
119119
120+ .stacked-pane-favicon {
121+ margin-right: 4px;
122+ }
123+
120124 .stacked-pane-active {
121125 /* Active pane is fully visible */
122126 }
@@ -117,6 +117,10 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string {
117 font-weight: 500;117 font-weight: 500;
118 }118 }
119 119
120+ .stacked-pane-favicon {
121+ margin-right: 4px;
122+ }
123+
120 .stacked-pane-active {124 .stacked-pane-active {
121 /* Active pane is fully visible */125 /* Active pane is fully visible */
122 }126 }

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