Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"@effect/platform-node": "catalog:",
"effect": "catalog:",
"electron": "40.9.3",
"electron-updater": "^6.6.2"
"electron-updater": "^6.6.2",
"jsonc-parser": "^3.3.1"
},
"devDependencies": {
"@t3tools/client-runtime": "workspace:*",
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/clientPersistence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const clientSettings: ClientSettings = {
sidebarProjectSortOrder: "manual",
sidebarThreadSortOrder: "created_at",
timestampFormat: "24-hour",
themePreference: { mode: "system" },
};

const savedRegistryRecord: PersistedSavedEnvironmentRecord = {
Expand Down
59 changes: 54 additions & 5 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type {
DesktopUpdateActionResult,
DesktopUpdateCheckResult,
DesktopUpdateState,
DesktopWindowThemeColors,
} from "@t3tools/contracts";
import { autoUpdater } from "electron-updater";

Expand Down Expand Up @@ -84,12 +85,21 @@ import { isArm64HostRunningIntelBuild, resolveDesktopRuntimeInfo } from "./runti
import { resolveDesktopAppBranding } from "./appBranding.ts";
import { bindFirstRevealTrigger, type RevealSubscription } from "./windowReveal.ts";
import { resolveTailscaleAdvertisedEndpoints } from "./tailscaleEndpointProvider.ts";
import {
discoverEditorColorThemes,
getEditorThemePreferences,
loadEditorColorTheme,
} from "./vscodeThemeDiscovery.ts";

syncShellEnvironment();

const PICK_FOLDER_CHANNEL = "desktop:pick-folder";
const CONFIRM_CHANNEL = "desktop:confirm";
const SET_THEME_CHANNEL = "desktop:set-theme";
const DISCOVER_COLOR_THEMES_CHANNEL = "desktop:discover-color-themes";
const LOAD_COLOR_THEME_CHANNEL = "desktop:load-color-theme";
const GET_EDITOR_THEME_PREFERENCES_CHANNEL = "desktop:get-editor-theme-preferences";
const SET_WINDOW_THEME_COLORS_CHANNEL = "desktop:set-window-theme-colors";
const CONTEXT_MENU_CHANNEL = "desktop:context-menu";
const OPEN_EXTERNAL_CHANNEL = "desktop:open-external";
const MENU_ACTION_CHANNEL = "desktop:menu-action";
Expand Down Expand Up @@ -118,6 +128,7 @@ const DESKTOP_SETTINGS_PATH = Path.join(STATE_DIR, "desktop-settings.json");
const CLIENT_SETTINGS_PATH = Path.join(STATE_DIR, "client-settings.json");
const SAVED_ENVIRONMENT_REGISTRY_PATH = Path.join(STATE_DIR, "saved-environments.json");
const DESKTOP_SCHEME = "t3";
let windowThemeColors: DesktopWindowThemeColors | null = null;
const ROOT_DIR = Path.resolve(__dirname, "../../..");
const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL);
// Dev-only SSH launcher override. Set this to an absolute path on the SSH host
Expand Down Expand Up @@ -498,6 +509,20 @@ function getSafeTheme(rawTheme: unknown): DesktopTheme | null {
return null;
}

function getSafeWindowThemeColors(rawInput: unknown): DesktopWindowThemeColors | null {
if (typeof rawInput !== "object" || rawInput === null || Array.isArray(rawInput)) return null;
const input = rawInput as Record<string, unknown>;
if (typeof input.backgroundColor !== "string" || input.backgroundColor.trim().length === 0) {
return null;
}
return {
backgroundColor: input.backgroundColor,
titleBarColor: typeof input.titleBarColor === "string" ? input.titleBarColor : undefined,
titleBarSymbolColor:
typeof input.titleBarSymbolColor === "string" ? input.titleBarSymbolColor : undefined,
};
}

async function waitForBackendHttpReady(
baseUrl: string,
options?: Parameters<typeof waitForHttpReady>[1],
Expand Down Expand Up @@ -1818,6 +1843,26 @@ function registerIpcHandlers(): void {
nativeTheme.themeSource = theme;
});

ipcMain.removeHandler(DISCOVER_COLOR_THEMES_CHANNEL);
ipcMain.handle(DISCOVER_COLOR_THEMES_CHANNEL, async () => discoverEditorColorThemes());

ipcMain.removeHandler(LOAD_COLOR_THEME_CHANNEL);
ipcMain.handle(LOAD_COLOR_THEME_CHANNEL, async (_event, rawThemeId: unknown) => {
if (typeof rawThemeId !== "string") return null;
return loadEditorColorTheme(rawThemeId);
});

ipcMain.removeHandler(GET_EDITOR_THEME_PREFERENCES_CHANNEL);
ipcMain.handle(GET_EDITOR_THEME_PREFERENCES_CHANNEL, async () => getEditorThemePreferences());

ipcMain.removeHandler(SET_WINDOW_THEME_COLORS_CHANNEL);
ipcMain.handle(SET_WINDOW_THEME_COLORS_CHANNEL, async (_event, rawInput: unknown) => {
const input = getSafeWindowThemeColors(rawInput);
if (!input) return;
windowThemeColors = input;
syncAllWindowAppearance();
});

ipcMain.removeHandler(CONTEXT_MENU_CHANNEL);
ipcMain.handle(
CONTEXT_MENU_CHANNEL,
Expand Down Expand Up @@ -1989,7 +2034,9 @@ function getIconOption(): { icon: string } | Record<string, never> {
}

function getInitialWindowBackgroundColor(): string {
return nativeTheme.shouldUseDarkColors ? "#0a0a0a" : "#ffffff";
return (
windowThemeColors?.backgroundColor ?? (nativeTheme.shouldUseDarkColors ? "#0a0a0a" : "#ffffff")
);
}

function getWindowTitleBarOptions(): WindowTitleBarOptions {
Expand All @@ -2003,11 +2050,13 @@ function getWindowTitleBarOptions(): WindowTitleBarOptions {
return {
titleBarStyle: "hidden",
titleBarOverlay: {
color: TITLEBAR_COLOR,
color: windowThemeColors?.titleBarColor ?? TITLEBAR_COLOR,
height: TITLEBAR_HEIGHT,
symbolColor: nativeTheme.shouldUseDarkColors
? TITLEBAR_DARK_SYMBOL_COLOR
: TITLEBAR_LIGHT_SYMBOL_COLOR,
symbolColor:
windowThemeColors?.titleBarSymbolColor ??
(nativeTheme.shouldUseDarkColors
? TITLEBAR_DARK_SYMBOL_COLOR
: TITLEBAR_LIGHT_SYMBOL_COLOR),
},
};
}
Expand Down
8 changes: 8 additions & 0 deletions apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import type { DesktopBridge } from "@t3tools/contracts";
const PICK_FOLDER_CHANNEL = "desktop:pick-folder";
const CONFIRM_CHANNEL = "desktop:confirm";
const SET_THEME_CHANNEL = "desktop:set-theme";
const DISCOVER_COLOR_THEMES_CHANNEL = "desktop:discover-color-themes";
const LOAD_COLOR_THEME_CHANNEL = "desktop:load-color-theme";
const GET_EDITOR_THEME_PREFERENCES_CHANNEL = "desktop:get-editor-theme-preferences";
const SET_WINDOW_THEME_COLORS_CHANNEL = "desktop:set-window-theme-colors";
const CONTEXT_MENU_CHANNEL = "desktop:context-menu";
const OPEN_EXTERNAL_CHANNEL = "desktop:open-external";
const MENU_ACTION_CHANNEL = "desktop:menu-action";
Expand Down Expand Up @@ -114,7 +118,11 @@ contextBridge.exposeInMainWorld("desktopBridge", {
getAdvertisedEndpoints: () => ipcRenderer.invoke(GET_ADVERTISED_ENDPOINTS_CHANNEL),
pickFolder: (options) => ipcRenderer.invoke(PICK_FOLDER_CHANNEL, options),
confirm: (message) => ipcRenderer.invoke(CONFIRM_CHANNEL, message),
discoverColorThemes: () => ipcRenderer.invoke(DISCOVER_COLOR_THEMES_CHANNEL),
loadColorTheme: (themeId) => ipcRenderer.invoke(LOAD_COLOR_THEME_CHANNEL, themeId),
getEditorThemePreferences: () => ipcRenderer.invoke(GET_EDITOR_THEME_PREFERENCES_CHANNEL),
setTheme: (theme) => ipcRenderer.invoke(SET_THEME_CHANNEL, theme),
setWindowThemeColors: (input) => ipcRenderer.invoke(SET_WINDOW_THEME_COLORS_CHANNEL, input),
showContextMenu: (items, position) => ipcRenderer.invoke(CONTEXT_MENU_CHANNEL, items, position),
openExternal: (url: string) => ipcRenderer.invoke(OPEN_EXTERNAL_CHANNEL, url),
onMenuAction: (listener) => {
Expand Down
104 changes: 104 additions & 0 deletions apps/desktop/src/vscodeThemeDiscovery.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import * as FS from "node:fs";
import * as OS from "node:os";
import * as Path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
discoverEditorColorThemes,
getEditorThemePreferences,
loadEditorColorTheme,
resolveEditorRoots,
} from "./vscodeThemeDiscovery.ts";

let tempDir = "";

function writeJson(filePath: string, value: unknown) {
FS.mkdirSync(Path.dirname(filePath), { recursive: true });
FS.writeFileSync(filePath, JSON.stringify(value, null, 2));
}

describe("vscodeThemeDiscovery", () => {
beforeEach(() => {
tempDir = FS.mkdtempSync(Path.join(OS.tmpdir(), "t3-vscode-themes-"));
});

afterEach(() => {
FS.rmSync(tempDir, { recursive: true, force: true });
});

it("discovers contributed themes and ignores unsafe paths", () => {
const roots = resolveEditorRoots({ platform: "darwin", homedir: tempDir });
const vscodeRoot = roots.find((root) => root.source === "vscode");
expect(vscodeRoot).toBeDefined();
if (!vscodeRoot) return;

const extensionDir = Path.join(vscodeRoot.extensionsPath, "github.github-vscode-theme");
writeJson(Path.join(extensionDir, "package.json"), {
publisher: "GitHub",
displayName: "GitHub Theme",
contributes: {
themes: [
{ label: "GitHub Dark Default", uiTheme: "vs-dark", path: "./themes/dark.json" },
{ label: "Escaped", uiTheme: "vs", path: "../escaped.json" },
],
},
});
writeJson(Path.join(extensionDir, "themes", "dark.json"), {
colors: { "editor.background": "#0d1117" },
});

expect(discoverEditorColorThemes(roots)).toMatchObject([
{
source: "vscode",
label: "GitHub Dark Default",
kind: "dark",
publisher: "GitHub",
},
]);
});

it("loads JSONC settings overrides into the resolved app theme", () => {
const roots = resolveEditorRoots({ platform: "darwin", homedir: tempDir });
const vscodeRoot = roots.find((root) => root.source === "vscode");
expect(vscodeRoot).toBeDefined();
if (!vscodeRoot) return;

const extensionDir = Path.join(vscodeRoot.extensionsPath, "github.github-vscode-theme");
writeJson(Path.join(extensionDir, "package.json"), {
contributes: {
themes: [{ label: "GitHub Dark Default", uiTheme: "vs-dark", path: "./themes/dark.json" }],
},
});
writeJson(Path.join(extensionDir, "themes", "dark.json"), {
semanticHighlighting: true,
colors: {
"editor.background": "#0d1117",
"editor.foreground": "#e6edf3",
},
tokenColors: [],
});
FS.mkdirSync(Path.dirname(vscodeRoot.settingsPath), { recursive: true });
FS.writeFileSync(
vscodeRoot.settingsPath,
`{
"workbench.colorTheme": "GitHub Dark Default",
"workbench.colorCustomizations": {
"button.background": "#238636",
"[GitHub Dark Default]": {
"sideBar.background": "#010409"
}
}
}`,
);

const preferences = getEditorThemePreferences(roots);
expect(preferences[0]?.colorTheme).toBe("GitHub Dark Default");

const themeId = discoverEditorColorThemes(roots)[0]?.id;
expect(themeId).toBeTruthy();
const resolved = themeId ? loadEditorColorTheme(themeId, roots) : null;

expect(resolved?.colors["button.background"]).toBe("#238636");
expect(resolved?.colors["sideBar.background"]).toBe("#010409");
expect(resolved?.appVariables["--primary"]).toBe("#238636");
});
});
Loading
Loading