feat(omnibox): fix space key behavior and add 'y' to copy selected URL
- commit
- e6ce1fedf1c3b4970ced7700da3f06293f0d85f2
- author
- brice · · 2025-12-17 18:51 UTC
- committer
- brice · · 2025-12-17 20:35 UTC
- parent
- 36d1371
- signature
- unsigned
2 files · +102 -9
M internal/ui/component/omnibox.go +98 -9
| @@ -7,6 +7,7 @@ import ( | |||
| 7 | 7 | "sync" | |
| 8 | 8 | "time" | |
| 9 | 9 | ||
| 10 | + | "github.com/bnema/dumber/internal/application/port" | |
| 10 | 11 | "github.com/bnema/dumber/internal/application/usecase" | |
| 11 | 12 | "github.com/bnema/dumber/internal/domain/entity" | |
| 12 | 13 | "github.com/bnema/dumber/internal/domain/url" | |
| @@ -74,11 +75,13 @@ type Omnibox struct { | |||
| 74 | 75 | selectedIndex int | |
| 75 | 76 | suggestions []Suggestion | |
| 76 | 77 | favorites []Favorite | |
| 78 | + | hasNavigated bool // true if user navigated with arrow keys (enables space to toggle favorite) | |
| 77 | 79 | ||
| 78 | 80 | // Dependencies | |
| 79 | 81 | historyUC *usecase.SearchHistoryUseCase | |
| 80 | 82 | favoritesUC *usecase.ManageFavoritesUseCase | |
| 81 | 83 | faviconCache *cache.FaviconCache | |
| 84 | + | clipboard port.Clipboard | |
| 82 | 85 | shortcuts map[string]config.SearchShortcut | |
| 83 | 86 | defaultSearch string | |
| 84 | 87 | initialBehavior string | |
| @@ -87,6 +90,7 @@ type Omnibox struct { | |||
| 87 | 90 | // Callbacks | |
| 88 | 91 | onNavigate func(url string) | |
| 89 | 92 | onClose func() | |
| 93 | + | onToast func(message string) | |
| 90 | 94 | ||
| 91 | 95 | // Debouncing | |
| 92 | 96 | debounceTimer *time.Timer | |
| @@ -110,11 +114,13 @@ type OmniboxConfig struct { | |||
| 110 | 114 | HistoryUC *usecase.SearchHistoryUseCase | |
| 111 | 115 | FavoritesUC *usecase.ManageFavoritesUseCase | |
| 112 | 116 | FaviconCache *cache.FaviconCache | |
| 117 | + | Clipboard port.Clipboard | |
| 113 | 118 | Shortcuts map[string]config.SearchShortcut | |
| 114 | 119 | DefaultSearch string | |
| 115 | 120 | InitialBehavior string | |
| 116 | - | UIScale float64 // UI scale for favicon sizing | |
| 117 | - | OnNavigate func(url string) // Callback when user navigates via omnibox | |
| 121 | + | UIScale float64 // UI scale for favicon sizing | |
| 122 | + | OnNavigate func(url string) // Callback when user navigates via omnibox | |
| 123 | + | OnToast func(message string) // Callback to show toast notification | |
| 118 | 124 | } | |
| 119 | 125 | ||
| 120 | 126 | // NewOmnibox creates a new native GTK4 omnibox widget. | |
| @@ -133,9 +139,11 @@ func NewOmnibox(ctx context.Context, cfg OmniboxConfig) *Omnibox { | |||
| 133 | 139 | historyUC: cfg.HistoryUC, | |
| 134 | 140 | favoritesUC: cfg.FavoritesUC, | |
| 135 | 141 | faviconCache: cfg.FaviconCache, | |
| 142 | + | clipboard: cfg.Clipboard, | |
| 136 | 143 | shortcuts: cfg.Shortcuts, | |
| 137 | 144 | defaultSearch: cfg.DefaultSearch, | |
| 138 | 145 | initialBehavior: cfg.InitialBehavior, | |
| 146 | + | onToast: cfg.OnToast, | |
| 139 | 147 | ctx: ctx, | |
| 140 | 148 | uiScale: uiScale, | |
| 141 | 149 | } | |
| @@ -452,9 +460,27 @@ func (o *Omnibox) handleKeyPress(keyval uint, state gdk.ModifierType) bool { | |||
| 452 | 460 | return true | |
| 453 | 461 | ||
| 454 | 462 | case uint(gdk.KEY_space): | |
| 455 | - | // Space toggles favorite: add in history mode, remove in favorites mode | |
| 456 | - | o.toggleFavorite() | |
| 457 | - | return true | |
| 463 | + | // Space toggles favorite only if user has navigated with arrow keys | |
| 464 | + | // Otherwise, let space pass through to entry for typing | |
| 465 | + | o.mu.RLock() | |
| 466 | + | navigated := o.hasNavigated | |
| 467 | + | o.mu.RUnlock() | |
| 468 | + | if navigated { | |
| 469 | + | o.toggleFavorite() | |
| 470 | + | return true | |
| 471 | + | } | |
| 472 | + | return false // Let entry handle space for typing | |
| 473 | + | ||
| 474 | + | case uint(gdk.KEY_y): | |
| 475 | + | // 'y' yanks (copies) the selected URL to clipboard when navigating | |
| 476 | + | o.mu.RLock() | |
| 477 | + | navigated := o.hasNavigated | |
| 478 | + | o.mu.RUnlock() | |
| 479 | + | if navigated { | |
| 480 | + | o.yankSelectedURL() | |
| 481 | + | return true | |
| 482 | + | } | |
| 483 | + | return false // Let entry handle 'y' for typing | |
| 458 | 484 | ||
| 459 | 485 | case uint(gdk.KEY_1), uint(gdk.KEY_2), uint(gdk.KEY_3), uint(gdk.KEY_4), uint(gdk.KEY_5), | |
| 460 | 486 | uint(gdk.KEY_6), uint(gdk.KEY_7), uint(gdk.KEY_8), uint(gdk.KEY_9): | |
| @@ -476,6 +502,11 @@ func (o *Omnibox) handleKeyPress(keyval uint, state gdk.ModifierType) bool { | |||
| 476 | 502 | ||
| 477 | 503 | // onEntryChanged handles text input changes with debouncing. | |
| 478 | 504 | func (o *Omnibox) onEntryChanged() { | |
| 505 | + | // Reset navigation state when user types - space should type, not toggle favorite | |
| 506 | + | o.mu.Lock() | |
| 507 | + | o.hasNavigated = false | |
| 508 | + | o.mu.Unlock() | |
| 509 | + | ||
| 479 | 510 | o.debounceMu.Lock() | |
| 480 | 511 | if o.debounceTimer != nil { | |
| 481 | 512 | o.debounceTimer.Stop() | |
| @@ -852,7 +883,7 @@ func (o *Omnibox) selectIndex(index int) { | |||
| 852 | 883 | ||
| 853 | 884 | // selectNext moves selection down. | |
| 854 | 885 | func (o *Omnibox) selectNext() { | |
| 855 | - | o.mu.RLock() | |
| 886 | + | o.mu.Lock() | |
| 856 | 887 | current := o.selectedIndex | |
| 857 | 888 | mode := o.viewMode | |
| 858 | 889 | var maxIndex int | |
| @@ -861,7 +892,8 @@ func (o *Omnibox) selectNext() { | |||
| 861 | 892 | } else { | |
| 862 | 893 | maxIndex = len(o.favorites) - 1 | |
| 863 | 894 | } | |
| 864 | - | o.mu.RUnlock() | |
| 895 | + | o.hasNavigated = true // User is navigating with arrow keys | |
| 896 | + | o.mu.Unlock() | |
| 865 | 897 | ||
| 866 | 898 | if maxIndex < 0 { | |
| 867 | 899 | return | |
| @@ -876,7 +908,7 @@ func (o *Omnibox) selectNext() { | |||
| 876 | 908 | ||
| 877 | 909 | // selectPrevious moves selection up. | |
| 878 | 910 | func (o *Omnibox) selectPrevious() { | |
| 879 | - | o.mu.RLock() | |
| 911 | + | o.mu.Lock() | |
| 880 | 912 | current := o.selectedIndex | |
| 881 | 913 | mode := o.viewMode | |
| 882 | 914 | var maxIndex int | |
| @@ -885,7 +917,8 @@ func (o *Omnibox) selectPrevious() { | |||
| 885 | 917 | } else { | |
| 886 | 918 | maxIndex = len(o.favorites) - 1 | |
| 887 | 919 | } | |
| 888 | - | o.mu.RUnlock() | |
| 920 | + | o.hasNavigated = true // User is navigating with arrow keys | |
| 921 | + | o.mu.Unlock() | |
| 889 | 922 | ||
| 890 | 923 | if maxIndex < 0 { | |
| 891 | 924 | return | |
| @@ -1034,6 +1067,62 @@ func (o *Omnibox) toggleFavorite() { | |||
| 1034 | 1067 | } | |
| 1035 | 1068 | } | |
| 1036 | 1069 | ||
| 1070 | + | // yankSelectedURL copies the URL of the selected item to clipboard. | |
| 1071 | + | func (o *Omnibox) yankSelectedURL() { | |
| 1072 | + | log := logging.FromContext(o.ctx) | |
| 1073 | + | ||
| 1074 | + | if o.clipboard == nil { | |
| 1075 | + | log.Warn().Msg("yank URL: clipboard is nil") | |
| 1076 | + | return | |
| 1077 | + | } | |
| 1078 | + | ||
| 1079 | + | o.mu.RLock() | |
| 1080 | + | mode := o.viewMode | |
| 1081 | + | idx := o.selectedIndex | |
| 1082 | + | suggestions := o.suggestions | |
| 1083 | + | favorites := o.favorites | |
| 1084 | + | o.mu.RUnlock() | |
| 1085 | + | ||
| 1086 | + | var url string | |
| 1087 | + | if mode == ViewModeHistory { | |
| 1088 | + | if idx < 0 || idx >= len(suggestions) { | |
| 1089 | + | log.Debug().Int("index", idx).Msg("yank URL: invalid selection") | |
| 1090 | + | return | |
| 1091 | + | } | |
| 1092 | + | url = suggestions[idx].URL | |
| 1093 | + | } else { | |
| 1094 | + | if idx < 0 || idx >= len(favorites) { | |
| 1095 | + | log.Debug().Int("index", idx).Msg("yank URL: invalid selection") | |
| 1096 | + | return | |
| 1097 | + | } | |
| 1098 | + | url = favorites[idx].URL | |
| 1099 | + | } | |
| 1100 | + | ||
| 1101 | + | if url == "" { | |
| 1102 | + | log.Debug().Msg("yank URL: empty URL") | |
| 1103 | + | return | |
| 1104 | + | } | |
| 1105 | + | ||
| 1106 | + | go func() { | |
| 1107 | + | ctx := o.ctx | |
| 1108 | + | if err := o.clipboard.WriteText(ctx, url); err != nil { | |
| 1109 | + | log.Error().Err(err).Str("url", url).Msg("failed to copy URL to clipboard") | |
| 1110 | + | return | |
| 1111 | + | } | |
| 1112 | + | log.Debug().Str("url", url).Msg("URL copied to clipboard") | |
| 1113 | + | ||
| 1114 | + | // Show toast notification on success (must run on GTK main thread) | |
| 1115 | + | if o.onToast != nil { | |
| 1116 | + | var cb glib.SourceFunc | |
| 1117 | + | cb = func(_ uintptr) bool { | |
| 1118 | + | o.onToast("URL copied") | |
| 1119 | + | return false // Don't repeat | |
| 1120 | + | } | |
| 1121 | + | glib.IdleAdd(&cb, 0) | |
| 1122 | + | } | |
| 1123 | + | }() | |
| 1124 | + | } | |
| 1125 | + | ||
| 1037 | 1126 | // buildURL constructs a URL from input, handling search shortcuts. | |
| 1038 | 1127 | func (o *Omnibox) buildURL(input string) string { | |
| 1039 | 1128 | if input == "" { | |
| @@ -7,6 +7,7 @@ import ( | |||||
| 7 | "sync" | 7 | "sync" | ||
| 8 | "time" | 8 | "time" | ||
| 9 | 9 | ||||
| 10 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 10 | "github.com/bnema/dumber/internal/application/usecase" | 11 | "github.com/bnema/dumber/internal/application/usecase" | ||
| 11 | "github.com/bnema/dumber/internal/domain/entity" | 12 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 12 | "github.com/bnema/dumber/internal/domain/url" | 13 | "github.com/bnema/dumber/internal/domain/url" | ||
| @@ -74,11 +75,13 @@ type Omnibox struct { | |||||
| 74 | selectedIndex int | 75 | selectedIndex int | ||
| 75 | suggestions []Suggestion | 76 | suggestions []Suggestion | ||
| 76 | favorites []Favorite | 77 | favorites []Favorite | ||
| 78 | + | hasNavigated bool // true if user navigated with arrow keys (enables space to toggle favorite) | |||
| 77 | 79 | ||||
| 78 | // Dependencies | 80 | // Dependencies | ||
| 79 | historyUC *usecase.SearchHistoryUseCase | 81 | historyUC *usecase.SearchHistoryUseCase | ||
| 80 | favoritesUC *usecase.ManageFavoritesUseCase | 82 | favoritesUC *usecase.ManageFavoritesUseCase | ||
| 81 | faviconCache *cache.FaviconCache | 83 | faviconCache *cache.FaviconCache | ||
| 84 | + | clipboard port.Clipboard | |||
| 82 | shortcuts map[string]config.SearchShortcut | 85 | shortcuts map[string]config.SearchShortcut | ||
| 83 | defaultSearch string | 86 | defaultSearch string | ||
| 84 | initialBehavior string | 87 | initialBehavior string | ||
| @@ -87,6 +90,7 @@ type Omnibox struct { | |||||
| 87 | // Callbacks | 90 | // Callbacks | ||
| 88 | onNavigate func(url string) | 91 | onNavigate func(url string) | ||
| 89 | onClose func() | 92 | onClose func() | ||
| 93 | + | onToast func(message string) | |||
| 90 | 94 | ||||
| 91 | // Debouncing | 95 | // Debouncing | ||
| 92 | debounceTimer *time.Timer | 96 | debounceTimer *time.Timer | ||
| @@ -110,11 +114,13 @@ type OmniboxConfig struct { | |||||
| 110 | HistoryUC *usecase.SearchHistoryUseCase | 114 | HistoryUC *usecase.SearchHistoryUseCase | ||
| 111 | FavoritesUC *usecase.ManageFavoritesUseCase | 115 | FavoritesUC *usecase.ManageFavoritesUseCase | ||
| 112 | FaviconCache *cache.FaviconCache | 116 | FaviconCache *cache.FaviconCache | ||
| 117 | + | Clipboard port.Clipboard | |||
| 113 | Shortcuts map[string]config.SearchShortcut | 118 | Shortcuts map[string]config.SearchShortcut | ||
| 114 | DefaultSearch string | 119 | DefaultSearch string | ||
| 115 | InitialBehavior string | 120 | InitialBehavior string | ||
| 116 | - | UIScale float64 // UI scale for favicon sizing | 121 | + | UIScale float64 // UI scale for favicon sizing |
| 117 | - | OnNavigate func(url string) // Callback when user navigates via omnibox | 122 | + | OnNavigate func(url string) // Callback when user navigates via omnibox |
| 123 | + | OnToast func(message string) // Callback to show toast notification | |||
| 118 | } | 124 | } | ||
| 119 | 125 | ||||
| 120 | // NewOmnibox creates a new native GTK4 omnibox widget. | 126 | // NewOmnibox creates a new native GTK4 omnibox widget. | ||
| @@ -133,9 +139,11 @@ func NewOmnibox(ctx context.Context, cfg OmniboxConfig) *Omnibox { | |||||
| 133 | historyUC: cfg.HistoryUC, | 139 | historyUC: cfg.HistoryUC, | ||
| 134 | favoritesUC: cfg.FavoritesUC, | 140 | favoritesUC: cfg.FavoritesUC, | ||
| 135 | faviconCache: cfg.FaviconCache, | 141 | faviconCache: cfg.FaviconCache, | ||
| 142 | + | clipboard: cfg.Clipboard, | |||
| 136 | shortcuts: cfg.Shortcuts, | 143 | shortcuts: cfg.Shortcuts, | ||
| 137 | defaultSearch: cfg.DefaultSearch, | 144 | defaultSearch: cfg.DefaultSearch, | ||
| 138 | initialBehavior: cfg.InitialBehavior, | 145 | initialBehavior: cfg.InitialBehavior, | ||
| 146 | + | onToast: cfg.OnToast, | |||
| 139 | ctx: ctx, | 147 | ctx: ctx, | ||
| 140 | uiScale: uiScale, | 148 | uiScale: uiScale, | ||
| 141 | } | 149 | } | ||
| @@ -452,9 +460,27 @@ func (o *Omnibox) handleKeyPress(keyval uint, state gdk.ModifierType) bool { | |||||
| 452 | return true | 460 | return true | ||
| 453 | 461 | ||||
| 454 | case uint(gdk.KEY_space): | 462 | case uint(gdk.KEY_space): | ||
| 455 | - | // Space toggles favorite: add in history mode, remove in favorites mode | 463 | + | // Space toggles favorite only if user has navigated with arrow keys |
| 456 | - | o.toggleFavorite() | 464 | + | // Otherwise, let space pass through to entry for typing |
| 457 | - | return true | 465 | + | o.mu.RLock() |
| 466 | + | navigated := o.hasNavigated | |||
| 467 | + | o.mu.RUnlock() | |||
| 468 | + | if navigated { | |||
| 469 | + | o.toggleFavorite() | |||
| 470 | + | return true | |||
| 471 | + | } | |||
| 472 | + | return false // Let entry handle space for typing | |||
| 473 | + | ||||
| 474 | + | case uint(gdk.KEY_y): | |||
| 475 | + | // 'y' yanks (copies) the selected URL to clipboard when navigating | |||
| 476 | + | o.mu.RLock() | |||
| 477 | + | navigated := o.hasNavigated | |||
| 478 | + | o.mu.RUnlock() | |||
| 479 | + | if navigated { | |||
| 480 | + | o.yankSelectedURL() | |||
| 481 | + | return true | |||
| 482 | + | } | |||
| 483 | + | return false // Let entry handle 'y' for typing | |||
| 458 | 484 | ||||
| 459 | case uint(gdk.KEY_1), uint(gdk.KEY_2), uint(gdk.KEY_3), uint(gdk.KEY_4), uint(gdk.KEY_5), | 485 | case uint(gdk.KEY_1), uint(gdk.KEY_2), uint(gdk.KEY_3), uint(gdk.KEY_4), uint(gdk.KEY_5), | ||
| 460 | uint(gdk.KEY_6), uint(gdk.KEY_7), uint(gdk.KEY_8), uint(gdk.KEY_9): | 486 | uint(gdk.KEY_6), uint(gdk.KEY_7), uint(gdk.KEY_8), uint(gdk.KEY_9): | ||
| @@ -476,6 +502,11 @@ func (o *Omnibox) handleKeyPress(keyval uint, state gdk.ModifierType) bool { | |||||
| 476 | 502 | ||||
| 477 | // onEntryChanged handles text input changes with debouncing. | 503 | // onEntryChanged handles text input changes with debouncing. | ||
| 478 | func (o *Omnibox) onEntryChanged() { | 504 | func (o *Omnibox) onEntryChanged() { | ||
| 505 | + | // Reset navigation state when user types - space should type, not toggle favorite | |||
| 506 | + | o.mu.Lock() | |||
| 507 | + | o.hasNavigated = false | |||
| 508 | + | o.mu.Unlock() | |||
| 509 | + | ||||
| 479 | o.debounceMu.Lock() | 510 | o.debounceMu.Lock() | ||
| 480 | if o.debounceTimer != nil { | 511 | if o.debounceTimer != nil { | ||
| 481 | o.debounceTimer.Stop() | 512 | o.debounceTimer.Stop() | ||
| @@ -852,7 +883,7 @@ func (o *Omnibox) selectIndex(index int) { | |||||
| 852 | 883 | ||||
| 853 | // selectNext moves selection down. | 884 | // selectNext moves selection down. | ||
| 854 | func (o *Omnibox) selectNext() { | 885 | func (o *Omnibox) selectNext() { | ||
| 855 | - | o.mu.RLock() | 886 | + | o.mu.Lock() |
| 856 | current := o.selectedIndex | 887 | current := o.selectedIndex | ||
| 857 | mode := o.viewMode | 888 | mode := o.viewMode | ||
| 858 | var maxIndex int | 889 | var maxIndex int | ||
| @@ -861,7 +892,8 @@ func (o *Omnibox) selectNext() { | |||||
| 861 | } else { | 892 | } else { | ||
| 862 | maxIndex = len(o.favorites) - 1 | 893 | maxIndex = len(o.favorites) - 1 | ||
| 863 | } | 894 | } | ||
| 864 | - | o.mu.RUnlock() | 895 | + | o.hasNavigated = true // User is navigating with arrow keys |
| 896 | + | o.mu.Unlock() | |||
| 865 | 897 | ||||
| 866 | if maxIndex < 0 { | 898 | if maxIndex < 0 { | ||
| 867 | return | 899 | return | ||
| @@ -876,7 +908,7 @@ func (o *Omnibox) selectNext() { | |||||
| 876 | 908 | ||||
| 877 | // selectPrevious moves selection up. | 909 | // selectPrevious moves selection up. | ||
| 878 | func (o *Omnibox) selectPrevious() { | 910 | func (o *Omnibox) selectPrevious() { | ||
| 879 | - | o.mu.RLock() | 911 | + | o.mu.Lock() |
| 880 | current := o.selectedIndex | 912 | current := o.selectedIndex | ||
| 881 | mode := o.viewMode | 913 | mode := o.viewMode | ||
| 882 | var maxIndex int | 914 | var maxIndex int | ||
| @@ -885,7 +917,8 @@ func (o *Omnibox) selectPrevious() { | |||||
| 885 | } else { | 917 | } else { | ||
| 886 | maxIndex = len(o.favorites) - 1 | 918 | maxIndex = len(o.favorites) - 1 | ||
| 887 | } | 919 | } | ||
| 888 | - | o.mu.RUnlock() | 920 | + | o.hasNavigated = true // User is navigating with arrow keys |
| 921 | + | o.mu.Unlock() | |||
| 889 | 922 | ||||
| 890 | if maxIndex < 0 { | 923 | if maxIndex < 0 { | ||
| 891 | return | 924 | return | ||
| @@ -1034,6 +1067,62 @@ func (o *Omnibox) toggleFavorite() { | |||||
| 1034 | } | 1067 | } | ||
| 1035 | } | 1068 | } | ||
| 1036 | 1069 | ||||
| 1070 | + | // yankSelectedURL copies the URL of the selected item to clipboard. | |||
| 1071 | + | func (o *Omnibox) yankSelectedURL() { | |||
| 1072 | + | log := logging.FromContext(o.ctx) | |||
| 1073 | + | ||||
| 1074 | + | if o.clipboard == nil { | |||
| 1075 | + | log.Warn().Msg("yank URL: clipboard is nil") | |||
| 1076 | + | return | |||
| 1077 | + | } | |||
| 1078 | + | ||||
| 1079 | + | o.mu.RLock() | |||
| 1080 | + | mode := o.viewMode | |||
| 1081 | + | idx := o.selectedIndex | |||
| 1082 | + | suggestions := o.suggestions | |||
| 1083 | + | favorites := o.favorites | |||
| 1084 | + | o.mu.RUnlock() | |||
| 1085 | + | ||||
| 1086 | + | var url string | |||
| 1087 | + | if mode == ViewModeHistory { | |||
| 1088 | + | if idx < 0 || idx >= len(suggestions) { | |||
| 1089 | + | log.Debug().Int("index", idx).Msg("yank URL: invalid selection") | |||
| 1090 | + | return | |||
| 1091 | + | } | |||
| 1092 | + | url = suggestions[idx].URL | |||
| 1093 | + | } else { | |||
| 1094 | + | if idx < 0 || idx >= len(favorites) { | |||
| 1095 | + | log.Debug().Int("index", idx).Msg("yank URL: invalid selection") | |||
| 1096 | + | return | |||
| 1097 | + | } | |||
| 1098 | + | url = favorites[idx].URL | |||
| 1099 | + | } | |||
| 1100 | + | ||||
| 1101 | + | if url == "" { | |||
| 1102 | + | log.Debug().Msg("yank URL: empty URL") | |||
| 1103 | + | return | |||
| 1104 | + | } | |||
| 1105 | + | ||||
| 1106 | + | go func() { | |||
| 1107 | + | ctx := o.ctx | |||
| 1108 | + | if err := o.clipboard.WriteText(ctx, url); err != nil { | |||
| 1109 | + | log.Error().Err(err).Str("url", url).Msg("failed to copy URL to clipboard") | |||
| 1110 | + | return | |||
| 1111 | + | } | |||
| 1112 | + | log.Debug().Str("url", url).Msg("URL copied to clipboard") | |||
| 1113 | + | ||||
| 1114 | + | // Show toast notification on success (must run on GTK main thread) | |||
| 1115 | + | if o.onToast != nil { | |||
| 1116 | + | var cb glib.SourceFunc | |||
| 1117 | + | cb = func(_ uintptr) bool { | |||
| 1118 | + | o.onToast("URL copied") | |||
| 1119 | + | return false // Don't repeat | |||
| 1120 | + | } | |||
| 1121 | + | glib.IdleAdd(&cb, 0) | |||
| 1122 | + | } | |||
| 1123 | + | }() | |||
| 1124 | + | } | |||
| 1125 | + | ||||
| 1037 | // buildURL constructs a URL from input, handling search shortcuts. | 1126 | // buildURL constructs a URL from input, handling search shortcuts. | ||
| 1038 | func (o *Omnibox) buildURL(input string) string { | 1127 | func (o *Omnibox) buildURL(input string) string { | ||
| 1039 | if input == "" { | 1128 | if input == "" { | ||
M internal/ui/dispatcher/keyboard.go +4 -0
| @@ -199,6 +199,10 @@ func (d *KeyboardDispatcher) handleZoom(ctx context.Context, action string) erro | |||
| 199 | 199 | // Notify omnibox to update zoom indicator | |
| 200 | 200 | d.navCoord.NotifyZoomChanged(ctx, newZoom.ZoomFactor) | |
| 201 | 201 | ||
| 202 | + | // Show zoom toast on the active pane | |
| 203 | + | zoomPercent := int(newZoom.ZoomFactor * 100) | |
| 204 | + | d.wsCoord.ShowZoomToast(ctx, zoomPercent) | |
| 205 | + | ||
| 202 | 206 | log.Debug(). | |
| 203 | 207 | Str("domain", domain). | |
| 204 | 208 | Str("action", action). | |
| @@ -199,6 +199,10 @@ func (d *KeyboardDispatcher) handleZoom(ctx context.Context, action string) erro | |||||
| 199 | // Notify omnibox to update zoom indicator | 199 | // Notify omnibox to update zoom indicator | ||
| 200 | d.navCoord.NotifyZoomChanged(ctx, newZoom.ZoomFactor) | 200 | d.navCoord.NotifyZoomChanged(ctx, newZoom.ZoomFactor) | ||
| 201 | 201 | ||||
| 202 | + | // Show zoom toast on the active pane | |||
| 203 | + | zoomPercent := int(newZoom.ZoomFactor * 100) | |||
| 204 | + | d.wsCoord.ShowZoomToast(ctx, zoomPercent) | |||
| 205 | + | ||||
| 202 | log.Debug(). | 206 | log.Debug(). | ||
| 203 | Str("domain", domain). | 207 | Str("domain", domain). | ||
| 204 | Str("action", action). | 208 | Str("action", action). | ||