v0.14.0 76d1892

stores.svelte.ts RawHistoryFolder

1/**
2 * Omnibox State Management
3 *
4 * Svelte 5 rune-based state management for omnibox functionality
5 */
6
7import type {
8  OmniboxMode,
9  ViewMode,
10  Suggestion,
11  Favorite,
12  FindMatch,
13  HighlightNode,
14  OmniboxConfig,
15  SearchShortcut,
16} from "./types";
17
18// Default configuration
19const DEFAULT_CONFIG: Required<OmniboxConfig> = {
20  maxMatches: 2000,
21  debounceDelay: 120,
22  defaultLimit: 10,
23};
24
25// Load viewMode from localStorage or default to "history"
26function loadViewMode(): ViewMode {
27  try {
28    const stored = localStorage.getItem("dumber_omnibox_viewMode");
29    if (stored === "favorites" || stored === "history") {
30      return stored;
31    }
32  } catch (e) {
33    console.warn("Failed to load viewMode from localStorage:", e);
34  }
35  return "history";
36}
37
38// Global state using Svelte 5 runes
39let visible = $state(false);
40let mode = $state<OmniboxMode>("omnibox");
41let viewMode = $state<ViewMode>(loadViewMode());
42let suggestions = $state<Suggestion[]>([]);
43let favorites = $state<Favorite[]>([]);
44let matches = $state<FindMatch[]>([]);
45let selectedIndex = $state(-1);
46let activeIndex = $state(-1);
47let faded = $state(false);
48let inputValue = $state("");
49let highlightNodes = $state<HighlightNode[]>([]);
50let prevOverflow = $state("");
51let debounceTimer = $state(0);
52let searchShortcuts = $state<Record<string, SearchShortcut>>({});
53
54// Configuration
55let config = $state<Required<OmniboxConfig>>(DEFAULT_CONFIG);
56
57export const omniboxStore = {
58  // State getters
59  get visible() {
60    return visible;
61  },
62  get mode() {
63    return mode;
64  },
65  get viewMode() {
66    return viewMode;
67  },
68  get suggestions() {
69    return suggestions;
70  },
71  get favorites() {
72    return favorites;
73  },
74  get matches() {
75    return matches;
76  },
77  get selectedIndex() {
78    return selectedIndex;
79  },
80  get activeIndex() {
81    return activeIndex;
82  },
83  get faded() {
84    return faded;
85  },
86  get inputValue() {
87    return inputValue;
88  },
89  get highlightNodes() {
90    return highlightNodes;
91  },
92  get prevOverflow() {
93    return prevOverflow;
94  },
95  get config() {
96    return config;
97  },
98  get searchShortcuts() {
99    return searchShortcuts;
100  },
101
102  // Computed getters
103  get hasContent() {
104    if (mode === "find") {
105      return matches.length > 0;
106    }
107    // In omnibox mode, check the current view
108    return viewMode === "history" ? suggestions.length > 0 : favorites.length > 0;
109  },
110  get totalItems() {
111    if (mode === "find") {
112      return matches.length;
113    }
114    // In omnibox mode, return count based on current view
115    return viewMode === "history" ? suggestions.length : favorites.length;
116  },
117  get selectedItem() {
118    if (selectedIndex < 0) return null;
119    if (mode === "find") {
120      return matches[selectedIndex];
121    }
122    // In omnibox mode, return item from current view
123    return viewMode === "history"
124      ? suggestions[selectedIndex]
125      : favorites[selectedIndex];
126  },
127
128  // Actions
129  setVisible(value: boolean) {
130    if (value && !visible) {
131      // Store scroll state when opening
132      prevOverflow = document.documentElement.style.overflow;
133      document.documentElement.style.overflow = "hidden";
134    } else if (!value && visible) {
135      // Restore scroll state when closing
136      document.documentElement.style.overflow = prevOverflow || "";
137      prevOverflow = "";
138
139      // Clear find highlights when closing
140      if (mode === "find") {
141        this.clearHighlights();
142      }
143    }
144    visible = value;
145  },
146
147  setMode(newMode: OmniboxMode) {
148    // Clear previous mode's state
149    if (newMode === "omnibox") {
150      // Switching to omnibox mode - clear find state
151      this.clearHighlights();
152    } else if (newMode === "find") {
153      // Switching to find mode - clear suggestions
154      this.updateSuggestions([]);
155    }
156
157    mode = newMode;
158    selectedIndex = -1;
159    faded = false;
160    inputValue = ""; // Clear input for fresh start
161  },
162
163  setInputValue(value: string) {
164    inputValue = value;
165  },
166
167  setFaded(value: boolean) {
168    faded = value;
169  },
170
171  setSelectedIndex(index: number) {
172    const maxIndex = this.totalItems - 1;
173    if (maxIndex < 0) {
174      selectedIndex = -1;
175    } else {
176      selectedIndex = Math.max(0, Math.min(index, maxIndex));
177    }
178  },
179
180  selectNext() {
181    if (this.totalItems === 0) return;
182    const newIndex =
183      selectedIndex < 0 ? 0 : (selectedIndex + 1) % this.totalItems;
184    this.setSelectedIndex(newIndex);
185    faded = true;
186  },
187
188  selectPrevious() {
189    if (this.totalItems === 0) return;
190    const newIndex =
191      selectedIndex <= 0 ? this.totalItems - 1 : selectedIndex - 1;
192    this.setSelectedIndex(newIndex);
193    faded = true;
194  },
195
196  open(newMode: OmniboxMode = "omnibox", initialQuery?: string) {
197    this.setMode(newMode);
198    this.setVisible(true);
199    if (typeof initialQuery === "string") {
200      inputValue = initialQuery;
201    }
202  },
203
204  close() {
205    this.setVisible(false);
206    faded = false;
207  },
208
209  toggle() {
210    if (!visible) {
211      // When opening via toggle (Ctrl+L), always open in omnibox mode
212      this.setMode("omnibox");
213      this.setVisible(true);
214    } else {
215      this.setVisible(false);
216    }
217  },
218
219  updateSuggestions(newSuggestions: Suggestion[]) {
220    suggestions = Array.isArray(newSuggestions) ? newSuggestions : [];
221    selectedIndex = -1;
222  },
223
224  updateMatches(newMatches: FindMatch[]) {
225    matches = Array.isArray(newMatches) ? newMatches : [];
226    selectedIndex = newMatches.length > 0 ? 0 : -1;
227  },
228
229  clearHighlights() {
230    try {
231      highlightNodes.forEach(({ span, text }) => {
232        const parent = span.parentNode;
233        if (parent) {
234          parent.replaceChild(text, span);
235          parent.normalize();
236        }
237      });
238    } catch (error) {
239      console.warn("Error clearing highlights:", error);
240    }
241    highlightNodes = [];
242    matches = [];
243    selectedIndex = -1;
244    activeIndex = -1;
245  },
246
247  addHighlightNode(node: HighlightNode) {
248    highlightNodes = [...highlightNodes, node];
249  },
250
251  setActiveIndex(index: number) {
252    activeIndex = index;
253  },
254
255  setDebounceTimer(timerId: number) {
256    if (debounceTimer) {
257      clearTimeout(debounceTimer);
258    }
259    debounceTimer = timerId;
260  },
261
262  clearDebounceTimer() {
263    if (debounceTimer) {
264      clearTimeout(debounceTimer);
265      debounceTimer = 0;
266    }
267  },
268
269  updateConfig(newConfig: Partial<OmniboxConfig>) {
270    config = { ...config, ...newConfig };
271  },
272
273  updateSearchShortcuts(shortcuts: Record<string, SearchShortcut>) {
274    searchShortcuts = shortcuts;
275  },
276
277  setViewMode(newViewMode: ViewMode) {
278    viewMode = newViewMode;
279    selectedIndex = -1; // Reset selection when switching views
280
281    // Persist to localStorage
282    try {
283      localStorage.setItem("dumber_omnibox_viewMode", newViewMode);
284    } catch (e) {
285      console.warn("Failed to save viewMode to localStorage:", e);
286    }
287  },
288
289  updateFavorites(newFavorites: Favorite[]) {
290    favorites = Array.isArray(newFavorites) ? newFavorites : [];
291    // Reset selection if we're in favorites view
292    if (viewMode === "favorites") {
293      selectedIndex = -1;
294    }
295  },
296
297  reset() {
298    visible = false;
299    mode = "omnibox";
300    // Keep viewMode persistent - it's user preference
301    suggestions = [];
302    favorites = [];
303    matches = [];
304    selectedIndex = -1;
305    activeIndex = -1;
306    faded = false;
307    inputValue = "";
308    prevOverflow = "";
309    this.clearHighlights();
310    this.clearDebounceTimer();
311  },
312};

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