Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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