v0.11.0 dc50343

color-scheme.ts RawHistoryFolder

1/**
2 * Color Scheme Detection Module
3 *
4 * Handles GTK theme detection and applies appropriate color scheme preferences
5 * to web pages. This module is injected at document-start to ensure proper
6 * theme initialization before page content loads.
7 */
8
9declare global {
10  interface Window {
11    __dumber_gtk_prefers_dark?: boolean;
12    __dumber_setTheme?: (theme: "light" | "dark") => void;
13  }
14}
15
16interface ColorSchemeConfig {
17  isDark: boolean;
18  colorScheme: "light" | "dark";
19  metaContent: string;
20  rootStyle: string;
21}
22
23class ColorSchemeManager {
24  private config: ColorSchemeConfig;
25  private metaElement: HTMLMetaElement | null = null;
26  private styleElement: HTMLStyleElement | null = null;
27  private userPreference: "light" | "dark" | null = null;
28  private static readonly STORAGE_KEY = "dumber.theme";
29  private static instance: ColorSchemeManager | null = null;
30  private static matchMediaPatched = false;
31  private static originalMatchMedia: typeof window.matchMedia | null = null;
32
33  constructor() {
34    ColorSchemeManager.instance = this;
35
36    this.userPreference = this.getStoredPreference();
37
38    // Use GTK preference if available, otherwise detect from system
39    const prefersDark = this.userPreference
40      ? this.userPreference === "dark"
41      : (window.__dumber_gtk_prefers_dark ?? false);
42
43    this.config = {
44      isDark: prefersDark,
45      colorScheme: prefersDark ? "dark" : "light",
46      metaContent: prefersDark ? "dark light" : "light dark",
47      rootStyle: prefersDark
48        ? ":root{color-scheme:dark;}"
49        : ":root{color-scheme:light;}",
50    };
51
52    this.initialize();
53  }
54
55  private initialize(): void {
56    try {
57      console.log(`[dumber] color-scheme set: ${this.config.colorScheme}`);
58      console.log(
59        `[dumber] GTK detected color mode: ${this.config.colorScheme}`,
60      );
61
62      // Notify native layer of theme preference
63      this.notifyNativeLayer();
64
65      // Inject color-scheme meta tag
66      this.injectColorSchemeMeta();
67
68      // Apply root styles
69      this.applyRootStyles();
70
71      // Override matchMedia for proper prefers-color-scheme support
72      this.overrideMatchMedia();
73
74      // Set up theme integration with existing UI components
75      this.setupThemeIntegration();
76    } catch (error) {
77      console.warn("[dumber] color-scheme injection failed", error);
78    }
79  }
80
81  private notifyNativeLayer(): void {
82    try {
83      (window as any).webkit?.messageHandlers?.dumber?.postMessage(
84        JSON.stringify({
85          type: "theme",
86          value: this.config.colorScheme,
87        }),
88      );
89    } catch {
90      // Ignore errors - native layer may not be available
91    }
92  }
93
94  private injectColorSchemeMeta(): void {
95    if (this.metaElement && this.metaElement.isConnected) {
96      this.metaElement.content = this.config.metaContent;
97      return;
98    }
99
100    const existing = document.querySelector(
101      'meta[name="color-scheme"]',
102    ) as HTMLMetaElement | null;
103    const meta = existing ?? document.createElement("meta");
104    meta.name = "color-scheme";
105    meta.content = this.config.metaContent;
106    if (!existing) {
107      document.documentElement.appendChild(meta);
108    }
109    this.metaElement = meta;
110  }
111
112  private applyRootStyles(): void {
113    if (this.styleElement && this.styleElement.isConnected) {
114      this.styleElement.textContent = this.config.rootStyle;
115      return;
116    }
117
118    const existing = document.querySelector(
119      "style[data-dumber-color-scheme]",
120    ) as HTMLStyleElement | null;
121    const style = existing ?? document.createElement("style");
122    style.setAttribute("data-dumber-color-scheme", "");
123    style.textContent = this.config.rootStyle;
124    if (!existing) {
125      document.documentElement.appendChild(style);
126    }
127    this.styleElement = style;
128  }
129
130  private overrideMatchMedia(): void {
131    if (ColorSchemeManager.matchMediaPatched) {
132      return;
133    }
134
135    const darkQuery = "(prefers-color-scheme: dark)";
136    const lightQuery = "(prefers-color-scheme: light)";
137    ColorSchemeManager.originalMatchMedia = window.matchMedia.bind(window);
138
139    window.matchMedia = (query: string): MediaQueryList => {
140      const manager = ColorSchemeManager.instance;
141      if (
142        manager &&
143        typeof query === "string" &&
144        (query.includes(darkQuery) || query.includes(lightQuery))
145      ) {
146        const matches = query.includes("dark")
147          ? manager.config.isDark
148          : !manager.config.isDark;
149
150        return {
151          matches,
152          media: query,
153          onchange: null,
154          addListener: () => {},
155          removeListener: () => {},
156          addEventListener: () => {},
157          removeEventListener: () => {},
158          dispatchEvent: () => false,
159        } as MediaQueryList;
160      }
161
162      const original = ColorSchemeManager.originalMatchMedia;
163      if (!original) {
164        throw new Error("Original matchMedia not available");
165      }
166
167      return original.call(window, query);
168    };
169
170    ColorSchemeManager.matchMediaPatched = true;
171  }
172
173  private setupThemeIntegration(): void {
174    // Integrate with unified theme setter if available
175    if (window.__dumber_setTheme) {
176      window.__dumber_setTheme(this.config.colorScheme);
177    } else {
178      // Retry after a short delay in case the bridge hasn't loaded yet
179      setTimeout(() => {
180        if (window.__dumber_setTheme) {
181          window.__dumber_setTheme(this.config.colorScheme);
182        } else {
183          // Fallback: directly apply dark class for Tailwind compatibility
184          console.warn("[dumber] Theme setter not available, using fallback");
185          if (this.config.isDark) {
186            document.documentElement.classList.add("dark");
187          } else {
188            document.documentElement.classList.remove("dark");
189          }
190          try {
191            window.__dumber_applyPalette?.(
192              this.config.isDark ? "dark" : "light",
193            );
194          } catch {
195            /* noop */
196          }
197        }
198      }, 100);
199    }
200  }
201
202  /**
203   * Updates the color scheme preference at runtime
204   * @param isDark Whether to use dark mode
205   */
206  public updateColorScheme(isDark: boolean): void {
207    this.config.isDark = isDark;
208    this.config.colorScheme = isDark ? "dark" : "light";
209    this.config.metaContent = isDark ? "dark light" : "light dark";
210    this.config.rootStyle = isDark
211      ? ":root{color-scheme:dark;}"
212      : ":root{color-scheme:light;}";
213
214    // Re-apply all configurations
215    this.initialize();
216  }
217
218  private getStoredPreference(): "light" | "dark" | null {
219    try {
220      const value = localStorage.getItem(ColorSchemeManager.STORAGE_KEY);
221      if (value === "light" || value === "dark") {
222        return value;
223      }
224    } catch (error) {
225      console.warn("[dumber] Failed to read stored theme preference", error);
226    }
227    return null;
228  }
229
230  private storePreference(theme: "light" | "dark"): void {
231    try {
232      localStorage.setItem(ColorSchemeManager.STORAGE_KEY, theme);
233    } catch (error) {
234      console.warn("[dumber] Failed to persist theme preference", error);
235    }
236  }
237
238  /**
239   * Persist and apply an explicit user preference
240   */
241  public setUserPreference(theme: "light" | "dark"): void {
242    if (this.userPreference === theme && this.config.colorScheme === theme) {
243      return;
244    }
245    this.userPreference = theme;
246    this.storePreference(theme);
247    this.updateColorScheme(theme === "dark");
248  }
249
250  public getCurrentTheme(): "light" | "dark" {
251    return this.config.colorScheme;
252  }
253}
254
255// Initialize color scheme management
256const colorSchemeManager = new ColorSchemeManager();
257
258// Export for potential runtime updates
259(window as any).__dumber_color_scheme_manager = colorSchemeManager;
260
261export default colorSchemeManager;
262
263declare global {
264  interface Window {
265    __dumber_color_scheme_manager?: {
266      setUserPreference: (theme: "light" | "dark") => void;
267      getCurrentTheme: () => "light" | "dark";
268    };
269  }
270}

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