detached 24d74a4

feat(omnibox): add favorite indicator for history items

commit
24d74a44d6034d29556444136fdb37ef257995ba
author
brice · · 2025-12-28 08:26 UTC
parent
e869777
signature
unsigned
4 files · +120 -19
M CHANGELOG.md +1 -0
@@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
55 ## [Unreleased]
66
77 ### Added
8+- **Omnibox favorite indicator**: History items that are also bookmarked now display a yellowish left border and subtle background tint in the omnibox. Uses parallel async loading to avoid blocking browser startup.
89 - **Mode indicator toaster**: Visual notification showing current modal mode (PANE MODE, TAB MODE, SESSION MODE, RESIZE MODE) at bottom-left corner with mode-specific colors. Configurable via `workspace.styling.mode_indicator_toaster_enabled` (default: true).
910 - **Config migration system**: Detects missing config keys when new settings are added in updates and provides tools to add them.
1011 - `dumber config status`: Shows config file path and number of new settings available.
@@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
5 ## [Unreleased]5 ## [Unreleased]
6 6
7 ### Added7 ### Added
8+- **Omnibox favorite indicator**: History items that are also bookmarked now display a yellowish left border and subtle background tint in the omnibox. Uses parallel async loading to avoid blocking browser startup.
8 - **Mode indicator toaster**: Visual notification showing current modal mode (PANE MODE, TAB MODE, SESSION MODE, RESIZE MODE) at bottom-left corner with mode-specific colors. Configurable via `workspace.styling.mode_indicator_toaster_enabled` (default: true).9 - **Mode indicator toaster**: Visual notification showing current modal mode (PANE MODE, TAB MODE, SESSION MODE, RESIZE MODE) at bottom-left corner with mode-specific colors. Configurable via `workspace.styling.mode_indicator_toaster_enabled` (default: true).
9 - **Config migration system**: Detects missing config keys when new settings are added in updates and provides tools to add them.10 - **Config migration system**: Detects missing config keys when new settings are added in updates and provides tools to add them.
10 - `dumber config status`: Shows config file path and number of new settings available.11 - `dumber config status`: Shows config file path and number of new settings available.
M internal/application/usecase/manage_favorites.go +19 -0
@@ -196,6 +196,25 @@ func (uc *ManageFavoritesUseCase) IsFavorite(ctx context.Context, url string) (b
196196 return fav != nil, nil
197197 }
198198
199+// GetAllURLs returns a set of all favorited URLs for efficient batch lookup.
200+func (uc *ManageFavoritesUseCase) GetAllURLs(ctx context.Context) (map[string]struct{}, error) {
201+ log := logging.FromContext(ctx)
202+ log.Debug().Msg("getting all favorite URLs")
203+
204+ favs, err := uc.favoriteRepo.GetAll(ctx)
205+ if err != nil {
206+ return nil, fmt.Errorf("failed to get favorites: %w", err)
207+ }
208+
209+ urls := make(map[string]struct{}, len(favs))
210+ for _, fav := range favs {
211+ urls[fav.URL] = struct{}{}
212+ }
213+
214+ log.Debug().Int("count", len(urls)).Msg("retrieved favorite URLs")
215+ return urls, nil
216+}
217+
199218 // GetTree builds a complete hierarchical view of folders and favorites.
200219 func (uc *ManageFavoritesUseCase) GetTree(ctx context.Context) (*entity.FavoriteTree, error) {
201220 log := logging.FromContext(ctx)
@@ -196,6 +196,25 @@ func (uc *ManageFavoritesUseCase) IsFavorite(ctx context.Context, url string) (b
196 return fav != nil, nil196 return fav != nil, nil
197 }197 }
198 198
199+// GetAllURLs returns a set of all favorited URLs for efficient batch lookup.
200+func (uc *ManageFavoritesUseCase) GetAllURLs(ctx context.Context) (map[string]struct{}, error) {
201+ log := logging.FromContext(ctx)
202+ log.Debug().Msg("getting all favorite URLs")
203+
204+ favs, err := uc.favoriteRepo.GetAll(ctx)
205+ if err != nil {
206+ return nil, fmt.Errorf("failed to get favorites: %w", err)
207+ }
208+
209+ urls := make(map[string]struct{}, len(favs))
210+ for _, fav := range favs {
211+ urls[fav.URL] = struct{}{}
212+ }
213+
214+ log.Debug().Int("count", len(urls)).Msg("retrieved favorite URLs")
215+ return urls, nil
216+}
217+
199 // GetTree builds a complete hierarchical view of folders and favorites.218 // GetTree builds a complete hierarchical view of folders and favorites.
200 func (uc *ManageFavoritesUseCase) GetTree(ctx context.Context) (*entity.FavoriteTree, error) {219 func (uc *ManageFavoritesUseCase) GetTree(ctx context.Context) (*entity.FavoriteTree, error) {
201 log := logging.FromContext(ctx)220 log := logging.FromContext(ctx)
M internal/ui/component/omnibox.go +84 -19
@@ -37,6 +37,7 @@ type Suggestion struct {
3737 URL string
3838 Title string
3939 FaviconURL string
40+ IsFavorite bool // Indicates if this URL is also bookmarked as a favorite
4041 }
4142
4243 // Favorite represents a bookmarked URL.
@@ -596,21 +597,43 @@ func (o *Omnibox) performSearch() {
596597 return
597598 }
598599
599- searchInput := usecase.SearchInput{
600- Query: query,
601- Limit: OmniboxListDefaults.MaxResults,
600+ // Run history search and favorite URL fetch in parallel
601+ type searchResult struct {
602+ output *usecase.SearchOutput
603+ err error
602604 }
603- output, err := o.historyUC.Search(ctx, searchInput)
604- if err != nil {
605- log.Error().Err(err).Msg("history search failed")
605+ searchCh := make(chan searchResult, 1)
606+ favCh := make(chan map[string]struct{}, 1)
607+
608+ go func() {
609+ searchInput := usecase.SearchInput{
610+ Query: query,
611+ Limit: OmniboxListDefaults.MaxResults,
612+ }
613+ output, err := o.historyUC.Search(ctx, searchInput)
614+ searchCh <- searchResult{output, err}
615+ }()
616+
617+ go func() {
618+ favCh <- o.getFavoriteURLs(ctx)
619+ }()
620+
621+ // Wait for both results
622+ sr := <-searchCh
623+ favoriteURLs := <-favCh
624+
625+ if sr.err != nil {
626+ log.Error().Err(sr.err).Msg("history search failed")
606627 return
607628 }
608629
609- suggestions := make([]Suggestion, 0, len(output.Matches))
610- for _, r := range output.Matches {
630+ suggestions := make([]Suggestion, 0, len(sr.output.Matches))
631+ for _, r := range sr.output.Matches {
632+ _, isFav := favoriteURLs[r.Entry.URL]
611633 suggestions = append(suggestions, Suggestion{
612- URL: r.Entry.URL,
613- Title: r.Entry.Title,
634+ URL: r.Entry.URL,
635+ Title: r.Entry.Title,
636+ IsFavorite: isFav,
614637 })
615638 }
616639
@@ -636,17 +659,40 @@ func (o *Omnibox) loadInitialHistory() {
636659 return
637660
638661 case "most_visited", "recent", "":
639- // TODO: Implement GetMostVisited in use case if needed
640- results, err := o.historyUC.GetRecent(ctx, OmniboxListDefaults.MaxResults, 0)
641- if err != nil {
642- log.Error().Err(err).Msg("failed to load recent history")
662+ // Run history fetch and favorite URL fetch in parallel
663+ type historyResult struct {
664+ results []*entity.HistoryEntry
665+ err error
666+ }
667+ historyCh := make(chan historyResult, 1)
668+ favCh := make(chan map[string]struct{}, 1)
669+
670+ go func() {
671+ // TODO: Implement GetMostVisited in use case if needed
672+ results, err := o.historyUC.GetRecent(ctx, OmniboxListDefaults.MaxResults, 0)
673+ historyCh <- historyResult{results, err}
674+ }()
675+
676+ go func() {
677+ favCh <- o.getFavoriteURLs(ctx)
678+ }()
679+
680+ // Wait for both results
681+ hr := <-historyCh
682+ favoriteURLs := <-favCh
683+
684+ if hr.err != nil {
685+ log.Error().Err(hr.err).Msg("failed to load recent history")
643686 return
644687 }
645- suggestions = make([]Suggestion, 0, len(results))
646- for _, r := range results {
688+
689+ suggestions = make([]Suggestion, 0, len(hr.results))
690+ for _, r := range hr.results {
691+ _, isFav := favoriteURLs[r.URL]
647692 suggestions = append(suggestions, Suggestion{
648- URL: r.URL,
649- Title: r.Title,
693+ URL: r.URL,
694+ Title: r.Title,
695+ IsFavorite: isFav,
650696 })
651697 }
652698 }
@@ -898,7 +944,11 @@ func (o *Omnibox) createRowWithFavicon(rawURL, title, fallbackIcon string, index
898944
899945 // createSuggestionRow creates a ListBoxRow for a suggestion.
900946 func (o *Omnibox) createSuggestionRow(s Suggestion, index int) *gtk.ListBoxRow {
901- return o.createRowWithFavicon(s.URL, s.Title, "web-browser-symbolic", index)
947+ row := o.createRowWithFavicon(s.URL, s.Title, "web-browser-symbolic", index)
948+ if row != nil && s.IsFavorite {
949+ row.AddCssClass("omnibox-row-favorite")
950+ }
951+ return row
902952 }
903953
904954 // createFavoriteRow creates a ListBoxRow for a favorite.
@@ -1340,6 +1390,21 @@ func (o *Omnibox) idleAddUpdateSuggestions(suggestions []Suggestion) {
13401390 glib.IdleAdd(&cb, 0)
13411391 }
13421392
1393+// getFavoriteURLs returns a set of all favorited URLs for batch lookup.
1394+// Returns empty map on error to avoid blocking history display.
1395+func (o *Omnibox) getFavoriteURLs(ctx context.Context) map[string]struct{} {
1396+ if o.favoritesUC == nil {
1397+ return make(map[string]struct{})
1398+ }
1399+ urls, err := o.favoritesUC.GetAllURLs(ctx)
1400+ if err != nil {
1401+ log := logging.FromContext(ctx)
1402+ log.Warn().Err(err).Msg("failed to load favorite URLs for indicator")
1403+ return make(map[string]struct{})
1404+ }
1405+ return urls
1406+}
1407+
13431408 // idleAddUpdateFavorites schedules updateFavorites on the GTK main thread.
13441409 func (o *Omnibox) idleAddUpdateFavorites(favorites []Favorite) {
13451410 var cb glib.SourceFunc = func(data uintptr) bool {
@@ -37,6 +37,7 @@ type Suggestion struct {
37 URL string37 URL string
38 Title string38 Title string
39 FaviconURL string39 FaviconURL string
40+ IsFavorite bool // Indicates if this URL is also bookmarked as a favorite
40 }41 }
41 42
42 // Favorite represents a bookmarked URL.43 // Favorite represents a bookmarked URL.
@@ -596,21 +597,43 @@ func (o *Omnibox) performSearch() {
596 return597 return
597 }598 }
598 599
599- searchInput := usecase.SearchInput{600+ // Run history search and favorite URL fetch in parallel
600- Query: query,601+ type searchResult struct {
601- Limit: OmniboxListDefaults.MaxResults,602+ output *usecase.SearchOutput
603+ err error
602 }604 }
603- output, err := o.historyUC.Search(ctx, searchInput)605+ searchCh := make(chan searchResult, 1)
604- if err != nil {606+ favCh := make(chan map[string]struct{}, 1)
605- log.Error().Err(err).Msg("history search failed")607+
608+ go func() {
609+ searchInput := usecase.SearchInput{
610+ Query: query,
611+ Limit: OmniboxListDefaults.MaxResults,
612+ }
613+ output, err := o.historyUC.Search(ctx, searchInput)
614+ searchCh <- searchResult{output, err}
615+ }()
616+
617+ go func() {
618+ favCh <- o.getFavoriteURLs(ctx)
619+ }()
620+
621+ // Wait for both results
622+ sr := <-searchCh
623+ favoriteURLs := <-favCh
624+
625+ if sr.err != nil {
626+ log.Error().Err(sr.err).Msg("history search failed")
606 return627 return
607 }628 }
608 629
609- suggestions := make([]Suggestion, 0, len(output.Matches))630+ suggestions := make([]Suggestion, 0, len(sr.output.Matches))
610- for _, r := range output.Matches {631+ for _, r := range sr.output.Matches {
632+ _, isFav := favoriteURLs[r.Entry.URL]
611 suggestions = append(suggestions, Suggestion{633 suggestions = append(suggestions, Suggestion{
612- URL: r.Entry.URL,634+ URL: r.Entry.URL,
613- Title: r.Entry.Title,635+ Title: r.Entry.Title,
636+ IsFavorite: isFav,
614 })637 })
615 }638 }
616 639
@@ -636,17 +659,40 @@ func (o *Omnibox) loadInitialHistory() {
636 return659 return
637 660
638 case "most_visited", "recent", "":661 case "most_visited", "recent", "":
639- // TODO: Implement GetMostVisited in use case if needed662+ // Run history fetch and favorite URL fetch in parallel
640- results, err := o.historyUC.GetRecent(ctx, OmniboxListDefaults.MaxResults, 0)663+ type historyResult struct {
641- if err != nil {664+ results []*entity.HistoryEntry
642- log.Error().Err(err).Msg("failed to load recent history")665+ err error
666+ }
667+ historyCh := make(chan historyResult, 1)
668+ favCh := make(chan map[string]struct{}, 1)
669+
670+ go func() {
671+ // TODO: Implement GetMostVisited in use case if needed
672+ results, err := o.historyUC.GetRecent(ctx, OmniboxListDefaults.MaxResults, 0)
673+ historyCh <- historyResult{results, err}
674+ }()
675+
676+ go func() {
677+ favCh <- o.getFavoriteURLs(ctx)
678+ }()
679+
680+ // Wait for both results
681+ hr := <-historyCh
682+ favoriteURLs := <-favCh
683+
684+ if hr.err != nil {
685+ log.Error().Err(hr.err).Msg("failed to load recent history")
643 return686 return
644 }687 }
645- suggestions = make([]Suggestion, 0, len(results))688+
646- for _, r := range results {689+ suggestions = make([]Suggestion, 0, len(hr.results))
690+ for _, r := range hr.results {
691+ _, isFav := favoriteURLs[r.URL]
647 suggestions = append(suggestions, Suggestion{692 suggestions = append(suggestions, Suggestion{
648- URL: r.URL,693+ URL: r.URL,
649- Title: r.Title,694+ Title: r.Title,
695+ IsFavorite: isFav,
650 })696 })
651 }697 }
652 }698 }
@@ -898,7 +944,11 @@ func (o *Omnibox) createRowWithFavicon(rawURL, title, fallbackIcon string, index
898 944
899 // createSuggestionRow creates a ListBoxRow for a suggestion.945 // createSuggestionRow creates a ListBoxRow for a suggestion.
900 func (o *Omnibox) createSuggestionRow(s Suggestion, index int) *gtk.ListBoxRow {946 func (o *Omnibox) createSuggestionRow(s Suggestion, index int) *gtk.ListBoxRow {
901- return o.createRowWithFavicon(s.URL, s.Title, "web-browser-symbolic", index)947+ row := o.createRowWithFavicon(s.URL, s.Title, "web-browser-symbolic", index)
948+ if row != nil && s.IsFavorite {
949+ row.AddCssClass("omnibox-row-favorite")
950+ }
951+ return row
902 }952 }
903 953
904 // createFavoriteRow creates a ListBoxRow for a favorite.954 // createFavoriteRow creates a ListBoxRow for a favorite.
@@ -1340,6 +1390,21 @@ func (o *Omnibox) idleAddUpdateSuggestions(suggestions []Suggestion) {
1340 glib.IdleAdd(&cb, 0)1390 glib.IdleAdd(&cb, 0)
1341 }1391 }
1342 1392
1393+// getFavoriteURLs returns a set of all favorited URLs for batch lookup.
1394+// Returns empty map on error to avoid blocking history display.
1395+func (o *Omnibox) getFavoriteURLs(ctx context.Context) map[string]struct{} {
1396+ if o.favoritesUC == nil {
1397+ return make(map[string]struct{})
1398+ }
1399+ urls, err := o.favoritesUC.GetAllURLs(ctx)
1400+ if err != nil {
1401+ log := logging.FromContext(ctx)
1402+ log.Warn().Err(err).Msg("failed to load favorite URLs for indicator")
1403+ return make(map[string]struct{})
1404+ }
1405+ return urls
1406+}
1407+
1343 // idleAddUpdateFavorites schedules updateFavorites on the GTK main thread.1408 // idleAddUpdateFavorites schedules updateFavorites on the GTK main thread.
1344 func (o *Omnibox) idleAddUpdateFavorites(favorites []Favorite) {1409 func (o *Omnibox) idleAddUpdateFavorites(favorites []Favorite) {
1345 var cb glib.SourceFunc = func(data uintptr) bool {1410 var cb glib.SourceFunc = func(data uintptr) bool {
M internal/ui/theme/css.go +16 -0
@@ -299,6 +299,22 @@ entry.omnibox-entry > *:focus-visible {
299299 border-left: 0.1875em solid var(--accent);
300300 }
301301
302+/* Favorite indicator in history mode - yellowish accent */
303+.omnibox-row.omnibox-row-favorite {
304+ background-color: alpha(var(--warning), 0.08);
305+ border-left: 0.1875em solid var(--warning);
306+}
307+
308+.omnibox-row.omnibox-row-favorite:hover {
309+ background-color: alpha(var(--warning), 0.15);
310+ border-left: 0.1875em solid var(--warning);
311+}
312+
313+.omnibox-row.omnibox-row-favorite:selected {
314+ background-color: alpha(var(--warning), 0.2);
315+ border-left: 0.1875em solid var(--warning);
316+}
317+
302318 /* Favicon in omnibox rows */
303319 .omnibox-favicon {
304320 min-width: 1em;
@@ -299,6 +299,22 @@ entry.omnibox-entry > *:focus-visible {
299 border-left: 0.1875em solid var(--accent);299 border-left: 0.1875em solid var(--accent);
300 }300 }
301 301
302+/* Favorite indicator in history mode - yellowish accent */
303+.omnibox-row.omnibox-row-favorite {
304+ background-color: alpha(var(--warning), 0.08);
305+ border-left: 0.1875em solid var(--warning);
306+}
307+
308+.omnibox-row.omnibox-row-favorite:hover {
309+ background-color: alpha(var(--warning), 0.15);
310+ border-left: 0.1875em solid var(--warning);
311+}
312+
313+.omnibox-row.omnibox-row-favorite:selected {
314+ background-color: alpha(var(--warning), 0.2);
315+ border-left: 0.1875em solid var(--warning);
316+}
317+
302 /* Favicon in omnibox rows */318 /* Favicon in omnibox rows */
303 .omnibox-favicon {319 .omnibox-favicon {
304 min-width: 1em;320 min-width: 1em;

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