v0.9.0 636c44c

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
26  constructor() {
27    // Use GTK preference if available, otherwise detect from system
28    const prefersDark = window.__dumber_gtk_prefers_dark ?? false;
29
30    this.config = {
31      isDark: prefersDark,
32      colorScheme: prefersDark ? "dark" : "light",
33      metaContent: prefersDark ? "dark light" : "light dark",
34      rootStyle: prefersDark
35        ? ":root{color-scheme:dark;}"
36        : ":root{color-scheme:light;}",
37    };
38
39    this.initialize();
40  }
41
42  private initialize(): void {
43    try {
44      console.log(`[dumber] color-scheme set: ${this.config.colorScheme}`);
45      console.log(
46        `[dumber] GTK detected color mode: ${this.config.colorScheme}`,
47      );
48
49      // Notify native layer of theme preference
50      this.notifyNativeLayer();
51
52      // Inject color-scheme meta tag
53      this.injectColorSchemeMeta();
54
55      // Apply root styles
56      this.applyRootStyles();
57
58      // Override matchMedia for proper prefers-color-scheme support
59      this.overrideMatchMedia();
60
61      // Set up theme integration with existing UI components
62      this.setupThemeIntegration();
63    } catch (error) {
64      console.warn("[dumber] color-scheme injection failed", error);
65    }
66  }
67
68  private notifyNativeLayer(): void {
69    try {
70      (window as any).webkit?.messageHandlers?.dumber?.postMessage(
71        JSON.stringify({
72          type: "theme",
73          value: this.config.colorScheme,
74        }),
75      );
76    } catch {
77      // Ignore errors - native layer may not be available
78    }
79  }
80
81  private injectColorSchemeMeta(): void {
82    const meta = document.createElement("meta");
83    meta.name = "color-scheme";
84    meta.content = this.config.metaContent;
85    document.documentElement.appendChild(meta);
86  }
87
88  private applyRootStyles(): void {
89    const style = document.createElement("style");
90    style.textContent = this.config.rootStyle;
91    document.documentElement.appendChild(style);
92  }
93
94  private overrideMatchMedia(): void {
95    const darkQuery = "(prefers-color-scheme: dark)";
96    const lightQuery = "(prefers-color-scheme: light)";
97    const originalMatchMedia = window.matchMedia;
98
99    window.matchMedia = (query: string): MediaQueryList => {
100      if (
101        typeof query === "string" &&
102        (query.includes(darkQuery) || query.includes(lightQuery))
103      ) {
104        // Create a mock MediaQueryList that reflects our GTK preference
105        const matches = query.includes("dark")
106          ? this.config.isDark
107          : !this.config.isDark;
108
109        return {
110          matches,
111          media: query,
112          onchange: null,
113          addListener: () => {},
114          removeListener: () => {},
115          addEventListener: () => {},
116          removeEventListener: () => {},
117          dispatchEvent: () => false,
118        } as MediaQueryList;
119      }
120
121      // For non-color-scheme queries, use the original implementation
122      return originalMatchMedia.call(window, query);
123    };
124  }
125
126  private setupThemeIntegration(): void {
127    // Integrate with unified theme setter if available
128    if (window.__dumber_setTheme) {
129      window.__dumber_setTheme(this.config.colorScheme);
130    } else {
131      // Retry after a short delay in case the bridge hasn't loaded yet
132      setTimeout(() => {
133        if (window.__dumber_setTheme) {
134          window.__dumber_setTheme(this.config.colorScheme);
135        } else {
136          // Fallback: directly apply dark class for Tailwind compatibility
137          console.warn("[dumber] Theme setter not available, using fallback");
138          if (this.config.isDark) {
139            document.documentElement.classList.add("dark");
140          } else {
141            document.documentElement.classList.remove("dark");
142          }
143        }
144      }, 100);
145    }
146  }
147
148  /**
149   * Updates the color scheme preference at runtime
150   * @param isDark Whether to use dark mode
151   */
152  public updateColorScheme(isDark: boolean): void {
153    this.config.isDark = isDark;
154    this.config.colorScheme = isDark ? "dark" : "light";
155    this.config.metaContent = isDark ? "dark light" : "light dark";
156    this.config.rootStyle = isDark
157      ? ":root{color-scheme:dark;}"
158      : ":root{color-scheme:light;}";
159
160    // Re-apply all configurations
161    this.initialize();
162  }
163}
164
165// Initialize color scheme management
166const colorSchemeManager = new ColorSchemeManager();
167
168// Export for potential runtime updates
169(window as any).__dumber_color_scheme_manager = colorSchemeManager;
170
171export default colorSchemeManager;

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