diff --git a/src/common/ipc/native-theme.ts b/src/common/ipc/native-theme.ts new file mode 100644 index 0000000000..4708a3c9b3 --- /dev/null +++ b/src/common/ipc/native-theme.ts @@ -0,0 +1,8 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + + +export const setNativeThemeChannel = "theme:set-native-theme"; +export const getNativeThemeChannel = "theme:get-native-theme"; diff --git a/src/extensions/registries/__tests__/page-registry.test.ts b/src/extensions/registries/__tests__/page-registry.test.ts index 1f8d1578eb..34c921cef5 100644 --- a/src/extensions/registries/__tests__/page-registry.test.ts +++ b/src/extensions/registries/__tests__/page-registry.test.ts @@ -29,6 +29,10 @@ jest.mock("electron", () => ({ on: jest.fn(), handle: jest.fn(), }, + ipcRenderer: { + on: jest.fn(), + invoke: jest.fn(), + }, })); console = new Console(stdout, stderr); diff --git a/src/main/index.ts b/src/main/index.ts index cf7dc47c65..2366a8036a 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -56,6 +56,7 @@ import routerInjectable from "./router/router.injectable"; import shellApiRequestInjectable from "./proxy-functions/shell-api-request/shell-api-request.injectable"; import userStoreInjectable from "../common/user-store/user-store.injectable"; import trayMenuItemsInjectable from "./tray/tray-menu-items.injectable"; +import { broadcastNativeThemeOnUpdate } from "./native-theme"; const di = getDi(); @@ -109,6 +110,8 @@ di.runSetups().then(() => { } } + broadcastNativeThemeOnUpdate(); + app.on("second-instance", (event, argv) => { logger.debug("second-instance message"); diff --git a/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.ts b/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.ts index c5b2d1f975..0899052f00 100644 --- a/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.ts +++ b/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.ts @@ -24,6 +24,8 @@ import { onLocationChange, handleWindowAction } from "../../ipc/window"; import { openFilePickingDialogChannel } from "../../../common/ipc/dialog"; import { showOpenDialog } from "../../ipc/dialog"; import { windowActionHandleChannel, windowLocationChangedChannel, windowOpenAppMenuAsContextMenuChannel } from "../../../common/ipc/window"; +import { getNativeColorTheme } from "../../native-theme"; +import { getNativeThemeChannel } from "../../../common/ipc/native-theme"; interface Dependencies { electronMenuItems: IComputedValue; @@ -158,4 +160,8 @@ export const initIpcMainHandlers = ({ electronMenuItems, directoryForLensLocalSt y: 20, }); }); + + ipcMainHandle(getNativeThemeChannel, () => { + return getNativeColorTheme(); + }); }; diff --git a/src/main/native-theme.ts b/src/main/native-theme.ts new file mode 100644 index 0000000000..e729245666 --- /dev/null +++ b/src/main/native-theme.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { nativeTheme } from "electron"; +import { broadcastMessage } from "../common/ipc"; +import { setNativeThemeChannel } from "../common/ipc/native-theme"; + +export function broadcastNativeThemeOnUpdate() { + nativeTheme.on("updated", () => { + broadcastMessage(setNativeThemeChannel, getNativeColorTheme()); + }); +} + +export function getNativeColorTheme() { + return nativeTheme.shouldUseDarkColors ? "dark" : "light"; +} diff --git a/src/renderer/components/+catalog/catalog.test.tsx b/src/renderer/components/+catalog/catalog.test.tsx index cac9143e54..218bb60feb 100644 --- a/src/renderer/components/+catalog/catalog.test.tsx +++ b/src/renderer/components/+catalog/catalog.test.tsx @@ -41,6 +41,10 @@ jest.mock("electron", () => ({ on: jest.fn(), handle: jest.fn(), }, + ipcRenderer: { + on: jest.fn(), + invoke: jest.fn(), + }, })); jest.mock("./hotbar-toggle-menu-item", () => ({ diff --git a/src/renderer/components/+preferences/application.tsx b/src/renderer/components/+preferences/application.tsx index d73982a468..b85d3e9665 100644 --- a/src/renderer/components/+preferences/application.tsx +++ b/src/renderer/components/+preferences/application.tsx @@ -45,7 +45,10 @@ const NonInjectedApplication: React.FC = ({ appPreferenceItems })
", () => { let di: DiContainer; diff --git a/src/renderer/theme.store.ts b/src/renderer/theme.store.ts index 19180285dd..e76b47dc38 100644 --- a/src/renderer/theme.store.ts +++ b/src/renderer/theme.store.ts @@ -13,6 +13,8 @@ import type { SelectOption } from "./components/select"; import type { MonacoEditorProps } from "./components/monaco-editor"; import { defaultTheme } from "../common/vars"; import { camelCase } from "lodash"; +import { ipcRenderer } from "electron"; +import { getNativeThemeChannel, setNativeThemeChannel } from "../common/ipc/native-theme"; export type ThemeId = string; @@ -34,6 +36,8 @@ export class ThemeStore extends Singleton { "lens-light": lensLightThemeJson as Theme, }); + @observable osNativeTheme: "dark" | "light" | undefined; + @computed get activeThemeId(): ThemeId { return UserStore.getInstance().colorTheme; } @@ -43,7 +47,7 @@ export class ThemeStore extends Singleton { } @computed get activeTheme(): Theme { - return this.themes.get(this.activeThemeId) ?? this.themes.get(defaultTheme); + return this.systemTheme ?? this.themes.get(this.activeThemeId) ?? this.themes.get(defaultTheme); } @computed get terminalColors(): [string, string][] { @@ -72,11 +76,25 @@ export class ThemeStore extends Singleton { })); } + @computed get systemTheme() { + if (this.activeThemeId == "system" && this.osNativeTheme) { + return this.themes.get(`lens-${this.osNativeTheme}`); + } + + return null; + } + constructor() { super(); makeObservable(this); autoBind(this); + this.init(); + } + + async init() { + await this.setNativeTheme(); + this.bindNativeThemeUpdateEvent(); // auto-apply active theme reaction(() => ({ @@ -95,12 +113,26 @@ export class ThemeStore extends Singleton { }); } + bindNativeThemeUpdateEvent() { + ipcRenderer.on(setNativeThemeChannel, (event, theme: "dark" | "light") => { + this.osNativeTheme = theme; + this.applyTheme(theme); + }); + } + + async setNativeTheme() { + const theme: "dark" | "light" = await ipcRenderer.invoke(getNativeThemeChannel); + + this.osNativeTheme = theme; + } + getThemeById(themeId: ThemeId): Theme { return this.themes.get(themeId); } protected applyTheme(themeId: ThemeId) { - const theme = this.getThemeById(themeId); + const theme = this.systemTheme ?? this.getThemeById(themeId); + const colors = Object.entries({ ...theme.colors, ...Object.fromEntries(this.terminalColors),