1package usecase
2
3import (
4 "context"
5 "sort"
6 "strings"
7
8 "github.com/bnema/dumber/internal/domain/url"
9 "github.com/bnema/dumber/internal/logging"
10)
11
12// SearchShortcut represents a search shortcut configuration.
13// This is a domain-level type to avoid depending on infrastructure config.
14type SearchShortcut struct {
15 URL string
16 Description string
17}
18
19// BangSuggestion represents a configured bang shortcut for display.
20type BangSuggestion struct {
21 Key string
22 Description string
23}
24
25// SearchShortcutsUseCase handles bang shortcut filtering and resolution.
26type SearchShortcutsUseCase struct {
27 shortcuts map[string]SearchShortcut
28}
29
30// NewSearchShortcutsUseCase creates a new search shortcuts use case.
31func NewSearchShortcutsUseCase(shortcuts map[string]SearchShortcut) *SearchShortcutsUseCase {
32 return &SearchShortcutsUseCase{
33 shortcuts: shortcuts,
34 }
35}
36
37// FilterBangsInput contains parameters for filtering bang suggestions.
38type FilterBangsInput struct {
39 Query string // e.g., "!" or "!g" or "!g query"
40}
41
42// FilterBangsOutput contains filtered bang suggestions.
43type FilterBangsOutput struct {
44 Suggestions []BangSuggestion
45}
46
47// FilterBangs returns bang shortcuts matching the query prefix.
48// Query should start with "!". The prefix is extracted before any space.
49func (uc *SearchShortcutsUseCase) FilterBangs(ctx context.Context, input FilterBangsInput) *FilterBangsOutput {
50 log := logging.FromContext(ctx)
51
52 suggestions := uc.buildBangSuggestions(input.Query)
53
54 log.Debug().
55 Str("query", input.Query).
56 Int("matches", len(suggestions)).
57 Msg("filtered bang suggestions")
58
59 return &FilterBangsOutput{Suggestions: suggestions}
60}
61
62// DetectBangKeyInput contains parameters for detecting a completed bang key.
63type DetectBangKeyInput struct {
64 Query string // e.g., "!gh query"
65}
66
67// DetectBangKeyOutput contains the detected bang key if found.
68type DetectBangKeyOutput struct {
69 Key string // The matched key (empty if not found)
70 Description string // Description of the matched shortcut
71}
72
73// DetectBangKey checks if the query contains a valid, completed bang key.
74// A completed bang key requires "!<key> " with a space after the key.
75func (uc *SearchShortcutsUseCase) DetectBangKey(ctx context.Context, input DetectBangKeyInput) *DetectBangKeyOutput {
76 log := logging.FromContext(ctx)
77
78 key := uc.detectBangKey(input.Query)
79 if key == "" {
80 return &DetectBangKeyOutput{}
81 }
82
83 shortcut, ok := uc.shortcuts[key]
84 if !ok {
85 return &DetectBangKeyOutput{}
86 }
87
88 description := shortcut.Description
89 if description == "" {
90 description = shortcut.URL
91 }
92
93 log.Debug().
94 Str("query", input.Query).
95 Str("detected_key", key).
96 Msg("detected bang key")
97
98 return &DetectBangKeyOutput{
99 Key: key,
100 Description: description,
101 }
102}
103
104// BuildNavigationTextInput contains parameters for building navigation text.
105type BuildNavigationTextInput struct {
106 EntryText string // e.g., "!GH dumber"
107}
108
109// BuildNavigationTextOutput contains the normalized navigation text.
110type BuildNavigationTextOutput struct {
111 Text string // Normalized text (e.g., "!gh dumber") or empty if invalid
112 Valid bool // Whether the entry text represents a valid bang navigation
113}
114
115// BuildNavigationText normalizes a bang query for navigation.
116// Returns the normalized text with the canonical key case.
117func (uc *SearchShortcutsUseCase) BuildNavigationText(ctx context.Context, input BuildNavigationTextInput) *BuildNavigationTextOutput {
118 log := logging.FromContext(ctx)
119
120 text := uc.buildBangNavigationText(input.EntryText)
121 if text == "" {
122 return &BuildNavigationTextOutput{}
123 }
124
125 log.Debug().
126 Str("entry_text", input.EntryText).
127 Str("navigation_text", text).
128 Msg("built bang navigation text")
129
130 return &BuildNavigationTextOutput{
131 Text: text,
132 Valid: true,
133 }
134}
135
136// GetShortcut returns a shortcut by key (case-insensitive).
137func (uc *SearchShortcutsUseCase) GetShortcut(key string) (SearchShortcut, bool) {
138 normalizedKey, ok := uc.normalizeBangKey(key)
139 if !ok {
140 return SearchShortcut{}, false
141 }
142 shortcut, ok := uc.shortcuts[normalizedKey]
143 return shortcut, ok
144}
145
146// ShortcutURLs returns a map of shortcut keys to URL templates.
147// This is useful for passing to url.BuildSearchURL.
148func (uc *SearchShortcutsUseCase) ShortcutURLs() map[string]string {
149 result := make(map[string]string, len(uc.shortcuts))
150 for key, shortcut := range uc.shortcuts {
151 result[key] = shortcut.URL
152 }
153 return result
154}
155
156// buildBangSuggestions filters and sorts shortcuts matching the query prefix.
157func (uc *SearchShortcutsUseCase) buildBangSuggestions(query string) []BangSuggestion {
158 prefix := strings.TrimPrefix(query, "!")
159 if idx := strings.Index(prefix, " "); idx >= 0 {
160 prefix = prefix[:idx]
161 }
162 prefix = strings.ToLower(prefix)
163
164 suggestions := make([]BangSuggestion, 0, len(uc.shortcuts))
165 for key, shortcut := range uc.shortcuts {
166 if !strings.HasPrefix(strings.ToLower(key), prefix) {
167 continue
168 }
169 description := shortcut.Description
170 if description == "" {
171 description = shortcut.URL
172 }
173 suggestions = append(suggestions, BangSuggestion{
174 Key: key,
175 Description: description,
176 })
177 }
178
179 sort.Slice(suggestions, func(i, j int) bool {
180 return strings.ToLower(suggestions[i].Key) < strings.ToLower(suggestions[j].Key)
181 })
182
183 return suggestions
184}
185
186// detectBangKey returns the matched shortcut key if the query has a valid bang prefix.
187func (uc *SearchShortcutsUseCase) detectBangKey(query string) string {
188 spaceIdx := strings.Index(query, " ")
189 if !strings.HasPrefix(query, "!") || spaceIdx <= 1 {
190 return ""
191 }
192
193 candidate := query[1:spaceIdx]
194 for key := range uc.shortcuts {
195 if strings.EqualFold(key, candidate) {
196 return key
197 }
198 }
199
200 return ""
201}
202
203// normalizeBangKey returns the canonical key for a shortcut (case-insensitive lookup).
204func (uc *SearchShortcutsUseCase) normalizeBangKey(shortcutKey string) (string, bool) {
205 for key := range uc.shortcuts {
206 if strings.EqualFold(key, shortcutKey) {
207 return key, true
208 }
209 }
210 return "", false
211}
212
213// buildBangNavigationText normalizes a bang query for navigation.
214func (uc *SearchShortcutsUseCase) buildBangNavigationText(entryText string) string {
215 shortcutKey, query, found := url.ParseBangShortcut(entryText)
216 if !found || query == "" {
217 return ""
218 }
219
220 resolvedKey, ok := uc.normalizeBangKey(shortcutKey)
221 if !ok {
222 return ""
223 }
224
225 return "!" + resolvedKey + " " + query
226}