diff --git a/package.json b/package.json index 008bac0a32..894666dedf 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "productName": "OpenLens", "description": "OpenLens - Open Source IDE for Kubernetes", "homepage": "https://github.com/lensapp/lens", - "version": "5.5.0-alpha.0", + "version": "5.5.0-alpha.1", "main": "static/build/main.js", "copyright": "© 2021 OpenLens Authors", "license": "MIT", diff --git a/src/common/__tests__/user-store.test.ts b/src/common/__tests__/user-store.test.ts index 9fb3713be1..a9069e1ec1 100644 --- a/src/common/__tests__/user-store.test.ts +++ b/src/common/__tests__/user-store.test.ts @@ -22,23 +22,21 @@ jest.mock("electron", () => ({ })); import { UserStore } from "../user-store"; -import { Console } from "console"; import { SemVer } from "semver"; import electron from "electron"; -import { stdout, stderr } from "process"; import userStoreInjectable from "../user-store/user-store.injectable"; import type { DiContainer } from "@ogre-tools/injectable"; import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; import type { ClusterStoreModel } from "../cluster-store/cluster-store"; -import { defaultTheme } from "../vars"; import writeFileInjectable from "../fs/write-file.injectable"; import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable"; import appVersionInjectable from "../get-configuration-file-model/app-version/app-version.injectable"; +import { defaultColorThemeConf, defaultTerminalThemeConf } from "../user-store/preferences-helpers"; -console = new Console(stdout, stderr); +console.log("This is a reminder to get rid of mockFs because it breaks console.log"); describe("user store tests", () => { let userStore: UserStore; @@ -76,20 +74,16 @@ describe("user store tests", () => { expect(userStore.lastSeenAppVersion).toBe("1.2.3"); }); - it("allows setting and getting preferences", () => { + it("allows setting preferences", () => { userStore.httpsProxy = "abcd://defg"; expect(userStore.httpsProxy).toBe("abcd://defg"); - expect(userStore.colorTheme).toBe(defaultTheme); - - userStore.colorTheme = "light"; - expect(userStore.colorTheme).toBe("light"); }); it("correctly resets theme to default value", async () => { - userStore.colorTheme = "some other theme"; - userStore.resetTheme(); - expect(userStore.colorTheme).toBe(defaultTheme); + (userStore.colorTheme as any) = "some other theme"; + userStore.resetThemeSettings(); + expect(userStore.colorTheme).toEqual(defaultColorThemeConf); }); it("correctly calculates if the last seen version is an old release", () => { @@ -107,7 +101,6 @@ describe("user store tests", () => { "config.json": JSON.stringify({ user: { username: "foobar" }, preferences: { colorTheme: "light" }, - lastSeenAppVersion: "1.2.3", }), "lens-cluster-store.json": JSON.stringify({ clusters: [ @@ -137,9 +130,148 @@ describe("user store tests", () => { expect(userStore.lastSeenAppVersion).toBe("0.0.0"); }); - it.only("skips clusters for adding to kube-sync with files under extension_data/", () => { + it("skips clusters for adding to kube-sync with files under extension_data/", () => { expect(userStore.syncKubeconfigEntries.has("some-directory-for-user-data/extension_data/foo/bar")).toBe(false); expect(userStore.syncKubeconfigEntries.has("some/other/path")).toBe(true); }); }); + + describe("5.5.0-alpha.1 migrations", () => { + it("given the invalid colorTheme setting of 'light', this migration resets value to default", () => { + mockFs({ + "some-directory-for-user-data": { + "lens-user-store.json": JSON.stringify({ + __internal__: { + migrations: { + version: "5.5.0-alpha.0", + }, + }, + preferences: { colorTheme: "light" }, + }), + }, + }); + + userStore = di.inject(userStoreInjectable); + expect(userStore.colorTheme).toEqual(defaultColorThemeConf); + }); + it("given the invalid colorTheme setting of false, this migration resets value to default", () => { + mockFs({ + "some-directory-for-user-data": { + "lens-user-store.json": JSON.stringify({ + __internal__: { + migrations: { + version: "5.5.0-alpha.0", + }, + }, + preferences: { colorTheme: false }, + }), + }, + }); + + userStore = di.inject(userStoreInjectable); + expect(userStore.colorTheme).toEqual(defaultColorThemeConf); + }); + + it("given the colorTheme setting of 'system', this migration sets to tracking the system color theme type", () => { + mockFs({ + "some-directory-for-user-data": { + "lens-user-store.json": JSON.stringify({ + __internal__: { + migrations: { + version: "5.5.0-alpha.0", + }, + }, + preferences: { colorTheme: "system" }, + }), + }, + }); + + userStore = di.inject(userStoreInjectable); + expect(userStore.colorTheme).toEqual({ + followSystemThemeType: true, + name: "lens", + }); + }); + + it("given the colorTheme setting of 'lens-light', this migration sets to the new format without loosing data", () => { + mockFs({ + "some-directory-for-user-data": { + "lens-user-store.json": JSON.stringify({ + __internal__: { + migrations: { + version: "5.5.0-alpha.0", + }, + }, + preferences: { colorTheme: "lens-light" }, + }), + }, + }); + + userStore = di.inject(userStoreInjectable); + expect(userStore.colorTheme).toEqual({ + followSystemThemeType: false, + name: "lens", + type: "light", + }); + }); + + it("given the terminalTheme setting of '', this migration sets to the new format of following lens theme", () => { + mockFs({ + "some-directory-for-user-data": { + "lens-user-store.json": JSON.stringify({ + __internal__: { + migrations: { + version: "5.5.0-alpha.0", + }, + }, + preferences: { terminalTheme: "" }, + }), + }, + }); + + userStore = di.inject(userStoreInjectable); + expect(userStore.terminalTheme).toEqual(defaultTerminalThemeConf); + }); + + it("given the invalid terminalTheme setting of 10, this migration sets to default", () => { + mockFs({ + "some-directory-for-user-data": { + "lens-user-store.json": JSON.stringify({ + __internal__: { + migrations: { + version: "5.5.0-alpha.0", + }, + }, + preferences: { terminalTheme: 10 }, + }), + }, + }); + + userStore = di.inject(userStoreInjectable); + expect(userStore.terminalTheme).toEqual(defaultTerminalThemeConf); + }); + + it("given the valid terminalTheme setting of 'lens-dark', this migration sets to the new format", () => { + mockFs({ + "some-directory-for-user-data": { + "lens-user-store.json": JSON.stringify({ + __internal__: { + migrations: { + version: "5.5.0-alpha.0", + }, + }, + preferences: { terminalTheme: "lens-dark" }, + }), + }, + }); + + userStore = di.inject(userStoreInjectable); + expect(userStore.terminalTheme).toEqual({ + isGlobalTheme: false, + isGlobalThemeType: false, + name: "lens", + type: "dark", + }); + }); + }); }); diff --git a/src/common/user-store/preferences-helpers.ts b/src/common/user-store/preferences-helpers.ts index 5e1fc597f3..1a551b7637 100644 --- a/src/common/user-store/preferences-helpers.ts +++ b/src/common/user-store/preferences-helpers.ts @@ -10,7 +10,9 @@ import { getAppVersion } from "../utils"; import type { editor } from "monaco-editor"; import merge from "lodash/merge"; import { SemVer } from "semver"; -import { defaultTheme, defaultEditorFontFamily, defaultFontSize, defaultTerminalFontFamily } from "../vars"; +import { defaultEditorFontFamily, defaultFontSize, defaultTerminalFontFamily, defaultThemeName, defaultThemeType } from "../vars"; +import type { ThemeType } from "../../renderer/themes/types"; +import { equals } from "lodash/fp"; export interface KubeconfigSyncEntry extends KubeconfigSyncValue { filePath: string; @@ -43,7 +45,7 @@ export const defaultEditorConfig: EditorConfiguration = { }; interface PreferenceDescription { fromStore(val: T | undefined): R; - toStore(val: R): T | undefined; + toStore(val: R | undefined): T | undefined; } const httpsProxy: PreferenceDescription = { @@ -64,12 +66,28 @@ const shell: PreferenceDescription = { }, }; -const colorTheme: PreferenceDescription = { +export type ColorThemeConf = { + name: string; +} & ({ + followSystemThemeType: true; + type?: undefined; +} | { + followSystemThemeType: false; + type: ThemeType; +}); + +export const defaultColorThemeConf: ColorThemeConf = { + name: defaultThemeName, + followSystemThemeType: false, + type: defaultThemeType, +}; + +const colorTheme: PreferenceDescription = { fromStore(val) { - return val || defaultTheme; + return val || defaultColorThemeConf; }, toStore(val) { - if (!val || val === defaultTheme) { + if (!val || equals(val, defaultColorThemeConf)) { return undefined; } @@ -77,12 +95,35 @@ const colorTheme: PreferenceDescription = { }, }; -const terminalTheme: PreferenceDescription = { +export type TerminalThemeConf = ({ + isGlobalTheme: true; + name?: undefined; +} | { + isGlobalTheme: false; + name: string; +}) & ({ + isGlobalThemeType: true; + type?: undefined; +} | { + isGlobalThemeType: false; + type: ThemeType; +}); + +export const defaultTerminalThemeConf: TerminalThemeConf = { + isGlobalTheme: true, + isGlobalThemeType: true, +}; + +const terminalTheme: PreferenceDescription = { fromStore(val) { - return val || ""; + return val || defaultTerminalThemeConf; }, toStore(val) { - return val || undefined; + if (!val || equals(val, defaultTerminalThemeConf)) { + return undefined; + } + + return defaultTerminalThemeConf; }, }; diff --git a/src/common/user-store/user-store.ts b/src/common/user-store/user-store.ts index a948def9d8..aa9a5192a4 100644 --- a/src/common/user-store/user-store.ts +++ b/src/common/user-store/user-store.ts @@ -12,7 +12,7 @@ import { getAppVersion } from "../utils/app-version"; import { kubeConfigDefaultPath } from "../kube-helpers"; import { appEventBus } from "../app-event-bus/event-bus"; import { getOrInsertSet, toggle, toJS, entries, fromEntries } from "../../renderer/utils"; -import { DESCRIPTORS } from "./preferences-helpers"; +import { ColorThemeConf, DESCRIPTORS, TerminalThemeConf } from "./preferences-helpers"; import type { EditorConfiguration, ExtensionRegistry, KubeconfigSyncValue, UserPreferencesModel, TerminalConfig } from "./preferences-helpers"; import logger from "../../main/logger"; @@ -49,8 +49,8 @@ export class UserStore extends BaseStore /* implements UserStore @observable allowTelemetry: boolean; @observable allowErrorReporting: boolean; @observable allowUntrustedCAs: boolean; - @observable colorTheme: string; - @observable terminalTheme: string; + @observable colorTheme: ColorThemeConf; + @observable terminalTheme: TerminalThemeConf; @observable localeTimezone: string; @observable downloadMirror: string; @observable httpsProxy?: string; @@ -142,8 +142,9 @@ export class UserStore extends BaseStore /* implements UserStore } @action - resetTheme() { + resetThemeSettings() { this.colorTheme = DESCRIPTORS.colorTheme.fromStore(undefined); + this.terminalTheme = DESCRIPTORS.terminalTheme.fromStore(undefined); } @action diff --git a/src/common/utils/type-narrowing.ts b/src/common/utils/type-narrowing.ts index d03dfa6b9a..1618477f9c 100644 --- a/src/common/utils/type-narrowing.ts +++ b/src/common/utils/type-narrowing.ts @@ -3,6 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ + /** * Narrows `val` to include the property `key` (if true is returned) * @param val The object to be tested @@ -73,6 +74,20 @@ export function isString(val: unknown): val is string { return typeof val === "string"; } +/** + * A function to validate (into the typesystem) that a certain string ends with a specific suffix. + */ +export function endsWith(val: string, suffix: T): val is `${string}${T}` { + return val.endsWith(suffix); +} + +/** + * A predicate for determining (in the type system) that a given string has a certain prefix + */ +export function startsWith(val: string, prefix: T): val is `${T}${string}`{ + return val.startsWith(prefix) as never; +} + /** * checks if val is of type object and isn't null * @param val the value to be checked diff --git a/src/common/utils/types.ts b/src/common/utils/types.ts new file mode 100644 index 0000000000..2b997122e3 --- /dev/null +++ b/src/common/utils/types.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +export type SafeCapitalize = S extends string + ? Capitalize + : never; + +export type PrefixCamelCasedProperties = Value extends Function + ? Value + : Value extends Array + ? Value + : { + [K in keyof Value as `${Prefix}${SafeCapitalize}`]: Value[K]; + }; diff --git a/src/common/vars.ts b/src/common/vars.ts index 63e441ed1a..cd4278fdf7 100644 --- a/src/common/vars.ts +++ b/src/common/vars.ts @@ -7,6 +7,7 @@ import path from "path"; import { SemVer } from "semver"; import packageInfo from "../../package.json"; +import type { ThemeType } from "../renderer/themes/types"; import { defineGlobal } from "./utils/defineGlobal"; import { lazyInitialized } from "./utils/lazy-initialized"; @@ -26,7 +27,10 @@ export const isIntegrationTesting = process.argv.includes(integrationTestingArg) export const productName = packageInfo.productName; export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`; export const publicPath = "/build/" as string; -export const defaultTheme = "lens-dark" as string; + +export const defaultThemeName = "lens"; +export const defaultThemeType: ThemeType = "dark"; + export const defaultFontSize = 12; export const defaultTerminalFontFamily = "RobotoMono"; export const defaultEditorFontFamily = "RobotoMono"; diff --git a/src/extensions/renderer-api/theming.ts b/src/extensions/renderer-api/theming.ts index 76796ab7bd..4ae3c9c1fb 100644 --- a/src/extensions/renderer-api/theming.ts +++ b/src/extensions/renderer-api/theming.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { ThemeStore } from "../../renderer/theme.store"; +import { ThemeStore } from "../../renderer/themes/store"; export function getActiveTheme() { return ThemeStore.getInstance().activeTheme; diff --git a/src/migrations/user-store/5.5.0-alpha.1.ts b/src/migrations/user-store/5.5.0-alpha.1.ts new file mode 100644 index 0000000000..b507fb63c8 --- /dev/null +++ b/src/migrations/user-store/5.5.0-alpha.1.ts @@ -0,0 +1,69 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { TerminalThemeConf, ColorThemeConf } from "../../common/user-store/preferences-helpers"; +import type { ThemeType } from "../../renderer/themes/types"; +import type { MigrationDeclaration } from "../helpers"; + +type Pre550Alpha1ColorTheme = string; // theme-id or "system" +type Pre550TerminalTheme = string; // theme-id or "" (for match) + +const themeIdMatcher = /^(?[a-z0-9]+)-(?dark|light)$/i; + +const v550alpha1Migration: MigrationDeclaration = { + version: "5.5.0-alpha.1", + run(store) { + const colorTheme = store.get("preferences.colorTheme") as Pre550Alpha1ColorTheme | undefined; + const terminalTheme = store.get("preferences.terminalTheme") as Pre550TerminalTheme | undefined; + + if (typeof colorTheme !== "string") { + store.delete("preferences.colorTheme"); // use default + } else { + if (colorTheme === "system") { + const newSetting: ColorThemeConf = { + followSystemThemeType: true, + name: "lens", + }; + + store.set("preferences.colorTheme", newSetting); + } else { + const match = colorTheme.match(themeIdMatcher); + + if (!match?.groups) { + store.delete("preferences.colorTheme"); // use default + } else { + const newSetting: ColorThemeConf = { + followSystemThemeType: false, + name: match.groups.name, + type: match.groups.type as ThemeType, + }; + + store.set("preferences.colorTheme", newSetting); + } + } + } + + if (typeof terminalTheme !== "string") { + store.delete("preferences.terminalTheme"); // use default + } else { + const match = terminalTheme.match(themeIdMatcher); + + if (!match?.groups) { + store.delete("preferences.terminalTheme"); // use default + } else { + const newSetting: TerminalThemeConf = { + isGlobalTheme: false, + isGlobalThemeType: false, + name: match.groups.name, + type: match.groups.type as ThemeType, + }; + + store.set("preferences.terminalTheme", newSetting); + } + } + }, +}; + +export default v550alpha1Migration; diff --git a/src/migrations/user-store/index.ts b/src/migrations/user-store/index.ts index 797868954b..53ffb40695 100644 --- a/src/migrations/user-store/index.ts +++ b/src/migrations/user-store/index.ts @@ -10,6 +10,7 @@ import { joinMigrations } from "../helpers"; import version210Beta4 from "./2.1.0-beta.4"; import version500Alpha3 from "./5.0.0-alpha.3"; import version503Beta1 from "./5.0.3-beta.1"; +import v550alpha1Migration from "./5.5.0-alpha.1"; import { fileNameMigration } from "./file-name-migration"; export { @@ -20,4 +21,5 @@ export default joinMigrations( version210Beta4, version500Alpha3, version503Beta1, + v550alpha1Migration, ); diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 3d1f9abf3d..38f0c54b97 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -36,7 +36,7 @@ import initClusterFrameInjectable from "./frames/cluster-frame/init-cluster-fram import commandOverlayInjectable from "./components/command-palette/command-overlay.injectable"; import { Router } from "react-router"; import historyInjectable from "./navigation/history.injectable"; -import themeStoreInjectable from "./theme-store.injectable"; +import themeStoreInjectable from "./themes/store.injectable"; import navigateToAddClusterInjectable from "../common/front-end-routing/routes/add-cluster/navigate-to-add-cluster.injectable"; import addSyncEntriesInjectable from "./initializers/add-sync-entries.injectable"; import hotbarStoreInjectable from "../common/hotbar-store.injectable"; diff --git a/src/renderer/components/+catalog/catalog.test.tsx b/src/renderer/components/+catalog/catalog.test.tsx index 9422adf055..f97329dad0 100644 --- a/src/renderer/components/+catalog/catalog.test.tsx +++ b/src/renderer/components/+catalog/catalog.test.tsx @@ -18,7 +18,7 @@ import catalogEntityStoreInjectable from "./catalog-entity-store/catalog-entity- import catalogEntityRegistryInjectable from "../../api/catalog-entity-registry/catalog-entity-registry.injectable"; import type { DiRender } from "../test-utils/renderFor"; import { renderFor } from "../test-utils/renderFor"; -import { ThemeStore } from "../../theme.store"; +import { ThemeStore } from "../../themes/store"; import { UserStore } from "../../../common/user-store"; import mockFs from "mock-fs"; import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; diff --git a/src/renderer/components/+cluster/cluster-issues.tsx b/src/renderer/components/+cluster/cluster-issues.tsx index de69b1b1da..ffae2ca1df 100644 --- a/src/renderer/components/+cluster/cluster-issues.tsx +++ b/src/renderer/components/+cluster/cluster-issues.tsx @@ -16,7 +16,7 @@ import { eventStore } from "../+events/event.store"; import { boundMethod, cssNames, prevDefault } from "../../utils"; import type { ItemObject } from "../../../common/item.store"; import { Spinner } from "../spinner"; -import { ThemeStore } from "../../theme.store"; +import { ThemeStore } from "../../themes/store"; import { kubeSelectedUrlParam, toggleDetails } from "../kube-detail-params"; import { apiManager } from "../../../common/k8s-api/api-manager"; import { KubeObjectAge } from "../kube-object/age"; diff --git a/src/renderer/components/+cluster/cluster-pie-charts.tsx b/src/renderer/components/+cluster/cluster-pie-charts.tsx index f955ece37b..0172f385cd 100644 --- a/src/renderer/components/+cluster/cluster-pie-charts.tsx +++ b/src/renderer/components/+cluster/cluster-pie-charts.tsx @@ -14,7 +14,7 @@ import { nodesStore } from "../+nodes/nodes.store"; import { ChartData, PieChart } from "../chart"; import { ClusterNoMetrics } from "./cluster-no-metrics"; import { bytesToUnits, cssNames } from "../../utils"; -import { ThemeStore } from "../../theme.store"; +import { ThemeStore } from "../../themes/store"; import { getMetricLastPoints } from "../../../common/k8s-api/endpoints/metrics.api"; import { withInjectables } from "@ogre-tools/injectable-react"; import clusterOverviewStoreInjectable from "./cluster-overview-store/cluster-overview-store.injectable"; diff --git a/src/renderer/components/+helm-releases/release-details/release-details.tsx b/src/renderer/components/+helm-releases/release-details/release-details.tsx index 5f4168ed57..371c4dd56b 100644 --- a/src/renderer/components/+helm-releases/release-details/release-details.tsx +++ b/src/renderer/components/+helm-releases/release-details/release-details.tsx @@ -20,7 +20,7 @@ import { Spinner } from "../../spinner"; import { Table, TableCell, TableHead, TableRow } from "../../table"; import { Button } from "../../button"; import { Notifications } from "../../notifications"; -import { ThemeStore } from "../../../theme.store"; +import { ThemeStore } from "../../../themes/store"; import { apiManager } from "../../../../common/k8s-api/api-manager"; import { SubTitle } from "../../layout/sub-title"; import { getDetailsUrl } from "../../kube-detail-params"; diff --git a/src/renderer/components/+nodes/node-charts.tsx b/src/renderer/components/+nodes/node-charts.tsx index 434dba1146..0a2a9a8961 100644 --- a/src/renderer/components/+nodes/node-charts.tsx +++ b/src/renderer/components/+nodes/node-charts.tsx @@ -11,7 +11,7 @@ import { NoMetrics } from "../resource-metrics/no-metrics"; import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics"; import { observer } from "mobx-react"; import type { ChartOptions, ChartPoint } from "chart.js"; -import { ThemeStore } from "../../theme.store"; +import { ThemeStore } from "../../themes/store"; import { mapValues } from "lodash"; type IContext = IResourceMetricsValue; diff --git a/src/renderer/components/+preferences/application.tsx b/src/renderer/components/+preferences/application.tsx index 4809590b9f..72eab58d45 100644 --- a/src/renderer/components/+preferences/application.tsx +++ b/src/renderer/components/+preferences/application.tsx @@ -7,7 +7,7 @@ import React from "react"; import { observer } from "mobx-react"; import { SubTitle } from "../layout/sub-title"; import { Select, SelectOption } from "../select"; -import type { ThemeStore } from "../../theme.store"; +import { themeTypeOptions } from "../../themes/store"; import type { UserStore } from "../../../common/user-store"; import { Input } from "../input"; import { Switch } from "../switch"; @@ -21,7 +21,6 @@ import { withInjectables } from "@ogre-tools/injectable-react"; import appPreferencesInjectable from "./app-preferences/app-preferences.injectable"; import { Preferences } from "./preferences"; import userStoreInjectable from "../../../common/user-store/user-store.injectable"; -import themeStoreInjectable from "../../theme-store.injectable"; const timezoneOptions: SelectOption[] = moment.tz.names().map(zone => ({ label: zone, @@ -35,10 +34,9 @@ const updateChannelOptions: SelectOption[] = Array.from( interface Dependencies { appPreferenceItems: IComputedValue; userStore: UserStore; - themeStore: ThemeStore; } -const NonInjectedApplication: React.FC = ({ appPreferenceItems, userStore, themeStore }) => { +const NonInjectedApplication = observer(({ appPreferenceItems, userStore }: Dependencies) => { const [customUrl, setCustomUrl] = React.useState(userStore.extensionRegistryUrl.customUrl || ""); const extensionSettings = appPreferenceItems.get().filter((preference) => preference.showInPreferencesTab === "application"); @@ -48,16 +46,27 @@ const NonInjectedApplication: React.FC = ({ appPreferenceItems, us

Application

- userStore.colorTheme.type = value} + themeName="lens" + /> + + )}

@@ -135,16 +144,11 @@ const NonInjectedApplication: React.FC = ({ appPreferenceItems, us ); -}; +}); -export const Application = withInjectables( - observer(NonInjectedApplication), - - { - getProps: (di) => ({ - appPreferenceItems: di.inject(appPreferencesInjectable), - userStore: di.inject(userStoreInjectable), - themeStore: di.inject(themeStoreInjectable), - }), - }, -); +export const Application = withInjectables(NonInjectedApplication, { + getProps: (di) => ({ + appPreferenceItems: di.inject(appPreferencesInjectable), + userStore: di.inject(userStoreInjectable), + }), +}); diff --git a/src/renderer/components/+preferences/terminal.tsx b/src/renderer/components/+preferences/terminal.tsx index 08adb55ee3..c01bc91141 100644 --- a/src/renderer/components/+preferences/terminal.tsx +++ b/src/renderer/components/+preferences/terminal.tsx @@ -10,20 +10,18 @@ import { SubTitle } from "../layout/sub-title"; import { Input, InputValidators } from "../input"; import { Switch } from "../switch"; import { Select } from "../select"; -import type { ThemeStore } from "../../theme.store"; +import { themeTypeOptions } from "../../themes/store"; import { Preferences } from "./preferences"; import { withInjectables } from "@ogre-tools/injectable-react"; import userStoreInjectable from "../../../common/user-store/user-store.injectable"; -import themeStoreInjectable from "../../theme-store.injectable"; import defaultShellInjectable from "./default-shell.injectable"; interface Dependencies { userStore: UserStore; - themeStore: ThemeStore; defaultShell: string; } -const NonInjectedTerminal = observer(({ userStore, themeStore, defaultShell }: Dependencies) => { +const NonInjectedTerminal = observer(({ userStore, defaultShell }: Dependencies) => { return (
@@ -51,18 +49,28 @@ const NonInjectedTerminal = observer(({ userStore, themeStore, defaultShell }: D
- userStore.terminalTheme.type = value} + /> + ) + }
+ {/* Once other themes are supported add an option to sync those as well */} +
( - NonInjectedTerminal, - - { - getProps: (di) => ({ - userStore: di.inject(userStoreInjectable), - themeStore: di.inject(themeStoreInjectable), - defaultShell: di.inject(defaultShellInjectable), - }), - }, -); +export const Terminal = withInjectables(NonInjectedTerminal, { + getProps: (di) => ({ + userStore: di.inject(userStoreInjectable), + defaultShell: di.inject(defaultShellInjectable), + }), +}); diff --git a/src/renderer/components/+storage-volume-claims/volume-claim-disk-chart.tsx b/src/renderer/components/+storage-volume-claims/volume-claim-disk-chart.tsx index eebd712232..d3ac83ae49 100644 --- a/src/renderer/components/+storage-volume-claims/volume-claim-disk-chart.tsx +++ b/src/renderer/components/+storage-volume-claims/volume-claim-disk-chart.tsx @@ -10,7 +10,7 @@ import { BarChart, ChartDataSets, memoryOptions } from "../chart"; import { isMetricsEmpty, normalizeMetrics } from "../../../common/k8s-api/endpoints/metrics.api"; import { NoMetrics } from "../resource-metrics/no-metrics"; import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics"; -import { ThemeStore } from "../../theme.store"; +import { ThemeStore } from "../../themes/store"; type IContext = IResourceMetricsValue; diff --git a/src/renderer/components/+workloads-overview/overview-workload-status.tsx b/src/renderer/components/+workloads-overview/overview-workload-status.tsx index fea3955b6e..54942d2427 100644 --- a/src/renderer/components/+workloads-overview/overview-workload-status.tsx +++ b/src/renderer/components/+workloads-overview/overview-workload-status.tsx @@ -11,7 +11,7 @@ import { observer } from "mobx-react"; import { PieChart } from "../chart"; import { cssVar } from "../../utils"; import type { ChartData } from "chart.js"; -import { ThemeStore } from "../../theme.store"; +import { ThemeStore } from "../../themes/store"; export interface OverviewWorkloadStatusProps { status: Record; diff --git a/src/renderer/components/+workloads-pods/container-charts.tsx b/src/renderer/components/+workloads-pods/container-charts.tsx index 85182ed516..243e08c7d2 100644 --- a/src/renderer/components/+workloads-pods/container-charts.tsx +++ b/src/renderer/components/+workloads-pods/container-charts.tsx @@ -10,7 +10,7 @@ import { BarChart, cpuOptions, memoryOptions } from "../chart"; import { isMetricsEmpty, normalizeMetrics } from "../../../common/k8s-api/endpoints/metrics.api"; import { NoMetrics } from "../resource-metrics/no-metrics"; import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics"; -import { ThemeStore } from "../../theme.store"; +import { ThemeStore } from "../../themes/store"; import { mapValues } from "lodash"; type IContext = IResourceMetricsValue; diff --git a/src/renderer/components/chart/bar-chart.tsx b/src/renderer/components/chart/bar-chart.tsx index f53bed6e51..88c8c33328 100644 --- a/src/renderer/components/chart/bar-chart.tsx +++ b/src/renderer/components/chart/bar-chart.tsx @@ -12,7 +12,7 @@ import type { ChartData, ChartOptions, ChartPoint, ChartTooltipItem, Scriptable import { Chart, ChartKind, ChartProps } from "./chart"; import { bytesToUnits, cssNames } from "../../utils"; import { ZebraStripes } from "./zebra-stripes.plugin"; -import { ThemeStore } from "../../theme.store"; +import { ThemeStore } from "../../themes/store"; import { NoMetrics } from "../resource-metrics/no-metrics"; export interface BarChartProps extends ChartProps { diff --git a/src/renderer/components/chart/pie-chart.tsx b/src/renderer/components/chart/pie-chart.tsx index bc6cd318f3..acbf223cb7 100644 --- a/src/renderer/components/chart/pie-chart.tsx +++ b/src/renderer/components/chart/pie-chart.tsx @@ -9,7 +9,7 @@ import { observer } from "mobx-react"; import ChartJS, { ChartOptions } from "chart.js"; import { Chart, ChartProps } from "./chart"; import { cssNames } from "../../utils"; -import { ThemeStore } from "../../theme.store"; +import { ThemeStore } from "../../themes/store"; export interface PieChartProps extends ChartProps { } diff --git a/src/renderer/components/dock/__test__/dock-tabs.test.tsx b/src/renderer/components/dock/__test__/dock-tabs.test.tsx index 3bc1f048d7..70852e49cb 100644 --- a/src/renderer/components/dock/__test__/dock-tabs.test.tsx +++ b/src/renderer/components/dock/__test__/dock-tabs.test.tsx @@ -10,7 +10,7 @@ import fse from "fs-extra"; import { DockTabs } from "../dock-tabs"; import { DockStore, DockTab, TabKind } from "../dock/store"; import { noop } from "../../../utils"; -import { ThemeStore } from "../../../theme.store"; +import { ThemeStore } from "../../../themes/store"; import { UserStore } from "../../../../common/user-store"; import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; import dockStoreInjectable from "../dock/store.injectable"; diff --git a/src/renderer/components/dock/logs/__test__/log-resource-selector.test.tsx b/src/renderer/components/dock/logs/__test__/log-resource-selector.test.tsx index f9d92aab06..f187ff6e04 100644 --- a/src/renderer/components/dock/logs/__test__/log-resource-selector.test.tsx +++ b/src/renderer/components/dock/logs/__test__/log-resource-selector.test.tsx @@ -9,7 +9,7 @@ import * as selectEvent from "react-select-event"; import { Pod } from "../../../../../common/k8s-api/endpoints"; import { LogResourceSelector } from "../resource-selector"; import { dockerPod, deploymentPod1, deploymentPod2 } from "./pod.mock"; -import { ThemeStore } from "../../../../theme.store"; +import { ThemeStore } from "../../../../themes/store"; import { UserStore } from "../../../../../common/user-store"; import mockFs from "mock-fs"; import { getDiForUnitTesting } from "../../../../getDiForUnitTesting"; diff --git a/src/renderer/components/dock/terminal/terminal.ts b/src/renderer/components/dock/terminal/terminal.ts index c119c071c6..6ca8160544 100644 --- a/src/renderer/components/dock/terminal/terminal.ts +++ b/src/renderer/components/dock/terminal/terminal.ts @@ -9,7 +9,7 @@ import { Terminal as XTerm } from "xterm"; import { FitAddon } from "xterm-addon-fit"; import type { TabId } from "../dock/store"; import { TerminalApi, TerminalChannels } from "../../../api/terminal-api"; -import { ThemeStore } from "../../../theme.store"; +import { ThemeStore } from "../../../themes/store"; import { disposer } from "../../../utils"; import { isMac } from "../../../../common/vars"; import { once } from "lodash"; diff --git a/src/renderer/components/dock/terminal/view.tsx b/src/renderer/components/dock/terminal/view.tsx index ec58a2a83b..1df269cf49 100644 --- a/src/renderer/components/dock/terminal/view.tsx +++ b/src/renderer/components/dock/terminal/view.tsx @@ -10,7 +10,7 @@ import { disposeOnUnmount, observer } from "mobx-react"; import { cssNames } from "../../../utils"; import type { Terminal } from "./terminal"; import type { TerminalStore } from "./store"; -import { ThemeStore } from "../../../theme.store"; +import { ThemeStore } from "../../../themes/store"; import type { DockTab, DockStore } from "../dock/store"; import { withInjectables } from "@ogre-tools/injectable-react"; import dockStoreInjectable from "../dock/store.injectable"; diff --git a/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx b/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx index f51dc1e8b2..3b53967017 100644 --- a/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx +++ b/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx @@ -11,7 +11,7 @@ import type { DiContainer } from "@ogre-tools/injectable"; import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; import { DiRender, renderFor } from "../../test-utils/renderFor"; import hotbarStoreInjectable from "../../../../common/hotbar-store.injectable"; -import { ThemeStore } from "../../../theme.store"; +import { ThemeStore } from "../../../themes/store"; import { ConfirmDialog } from "../../confirm-dialog"; import { UserStore } from "../../../../common/user-store"; import mockFs from "mock-fs"; diff --git a/src/renderer/components/item-object-list/content.tsx b/src/renderer/components/item-object-list/content.tsx index eb7b5dedb2..07aad6746a 100644 --- a/src/renderer/components/item-object-list/content.tsx +++ b/src/renderer/components/item-object-list/content.tsx @@ -16,7 +16,7 @@ import { NoItems } from "../no-items"; import { Spinner } from "../spinner"; import type { ItemObject, ItemStore } from "../../../common/item.store"; import { Filter, pageFilters } from "./page-filters.store"; -import { ThemeStore } from "../../theme.store"; +import { ThemeStore } from "../../themes/store"; import { MenuActions } from "../menu/menu-actions"; import { MenuItem } from "../menu"; import { Checkbox } from "../checkbox"; diff --git a/src/renderer/components/monaco-editor/monaco-editor.tsx b/src/renderer/components/monaco-editor/monaco-editor.tsx index b4931bdaea..f9cd8e4436 100644 --- a/src/renderer/components/monaco-editor/monaco-editor.tsx +++ b/src/renderer/components/monaco-editor/monaco-editor.tsx @@ -13,7 +13,7 @@ import { MonacoValidator, monacoValidators } from "./monaco-validators"; import { debounce, merge } from "lodash"; import { cssNames, disposer } from "../../utils"; import { UserStore } from "../../../common/user-store"; -import { ThemeStore } from "../../theme.store"; +import { ThemeStore } from "../../themes/store"; export type MonacoEditorId = string; diff --git a/src/renderer/components/select/select.test.tsx b/src/renderer/components/select/select.test.tsx index 6149f1072d..1495ca7eda 100644 --- a/src/renderer/components/select/select.test.tsx +++ b/src/renderer/components/select/select.test.tsx @@ -6,7 +6,7 @@ import React from "react"; import "@testing-library/jest-dom/extend-expect"; import { Select } from "./select"; import { UserStore } from "../../../common/user-store"; -import { ThemeStore } from "../../theme.store"; +import { ThemeStore } from "../../themes/store"; import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import type { DiContainer } from "@ogre-tools/injectable"; import { DiRender, renderFor } from "../test-utils/renderFor"; diff --git a/src/renderer/components/select/select.tsx b/src/renderer/components/select/select.tsx index 9de1944408..13d067f0d5 100644 --- a/src/renderer/components/select/select.tsx +++ b/src/renderer/components/select/select.tsx @@ -15,7 +15,7 @@ import ReactSelectCreatable from "react-select/creatable"; import type { ActionMeta, OptionTypeBase, Props as ReactSelectProps, Styles } from "react-select"; import type { CreatableProps } from "react-select/creatable"; -import { ThemeStore } from "../../theme.store"; +import { ThemeStore } from "../../themes/store"; import { boundMethod, cssNames } from "../../utils"; const { Menu } = components; diff --git a/src/renderer/theme.store.ts b/src/renderer/theme.store.ts deleted file mode 100644 index e76b47dc38..0000000000 --- a/src/renderer/theme.store.ts +++ /dev/null @@ -1,148 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { comparer, computed, makeObservable, observable, reaction } from "mobx"; -import { autoBind, Singleton } from "./utils"; -import { UserStore } from "../common/user-store"; -import logger from "../main/logger"; -import lensDarkThemeJson from "./themes/lens-dark.json"; -import lensLightThemeJson from "./themes/lens-light.json"; -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; - -export interface Theme { - name: string; - type: "dark" | "light"; - colors: Record; - description: string; - author: string; - monacoTheme: MonacoEditorProps["theme"]; -} - -export class ThemeStore extends Singleton { - private terminalColorPrefix = "terminal"; - - // bundled themes from `themes/${themeId}.json` - private themes = observable.map({ - "lens-dark": lensDarkThemeJson as Theme, - "lens-light": lensLightThemeJson as Theme, - }); - - @observable osNativeTheme: "dark" | "light" | undefined; - - @computed get activeThemeId(): ThemeId { - return UserStore.getInstance().colorTheme; - } - - @computed get terminalThemeId(): ThemeId { - return UserStore.getInstance().terminalTheme; - } - - @computed get activeTheme(): Theme { - return this.systemTheme ?? this.themes.get(this.activeThemeId) ?? this.themes.get(defaultTheme); - } - - @computed get terminalColors(): [string, string][] { - const theme = this.themes.get(this.terminalThemeId) ?? this.activeTheme; - - return Object - .entries(theme.colors) - .filter(([name]) => name.startsWith(this.terminalColorPrefix)); - } - - // Replacing keys stored in styles to format accepted by terminal - // E.g. terminalBrightBlack -> brightBlack - @computed get xtermColors(): Record { - return Object.fromEntries( - this.terminalColors.map(([name, color]) => [ - camelCase(name.replace(this.terminalColorPrefix, "")), - color, - ]), - ); - } - - @computed get themeOptions(): SelectOption[] { - return Array.from(this.themes).map(([themeId, theme]) => ({ - label: theme.name, - value: themeId, - })); - } - - @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(() => ({ - themeId: this.activeThemeId, - terminalThemeId: this.terminalThemeId, - }), ({ themeId }) => { - try { - this.applyTheme(themeId); - } catch (err) { - logger.error(err); - UserStore.getInstance().resetTheme(); - } - }, { - fireImmediately: true, - equals: comparer.shallow, - }); - } - - 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.systemTheme ?? this.getThemeById(themeId); - - const colors = Object.entries({ - ...theme.colors, - ...Object.fromEntries(this.terminalColors), - }); - - colors.forEach(([name, value]) => { - document.documentElement.style.setProperty(`--${name}`, value); - }); - - // Adding universal theme flag which can be used in component styles - document.body.classList.toggle("theme-light", theme.type === "light"); - } -} diff --git a/src/renderer/themes/lens-dark.json b/src/renderer/themes/lens-dark.ts similarity index 94% rename from src/renderer/themes/lens-dark.json rename to src/renderer/themes/lens-dark.ts index f30d48197e..f31baa606c 100644 --- a/src/renderer/themes/lens-dark.json +++ b/src/renderer/themes/lens-dark.ts @@ -1,4 +1,11 @@ -{ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { Theme } from "./types"; + +const lensDarkTheme: Theme = { "name": "Dark", "type": "dark", "description": "Original Lens dark theme", @@ -136,6 +143,8 @@ "navSelectedBackground": "#262b2e", "navHoverColor": "#dcddde", "hrColor": "#ffffff0f", - "tooltipBackground": "#18191c" - } -} + "tooltipBackground": "#18191c", + }, +}; + +export default lensDarkTheme; diff --git a/src/renderer/themes/lens-light.json b/src/renderer/themes/lens-light.json deleted file mode 100644 index 06ef6d79b2..0000000000 --- a/src/renderer/themes/lens-light.json +++ /dev/null @@ -1,141 +0,0 @@ -{ - "name": "Light", - "type": "light", - "description": "Original Lens light theme", - "author": "Mirantis", - "monacoTheme": "vs", - "colors": { - "blue": "#3d90ce", - "magenta": "#c93dce", - "golden": "#ffc63d", - "halfGray": "#87909c80", - "primary": "#3d90ce", - "textColorPrimary": "#555555", - "textColorSecondary": "#51575d", - "textColorTertiary": "#555555", - "textColorAccent": "#222222", - "textColorDimmed": "#5557598c", - "borderColor": "#c9cfd3", - "borderFaintColor": "#dfdfdf", - "mainBackground": "#f1f1f1", - "secondaryBackground": "#f2f3f5", - "contentColor": "#ffffff", - "layoutBackground": "#e8e8e8", - "layoutTabsBackground": "#f8f8f8", - "layoutTabsActiveColor": "#333333", - "layoutTabsLineColor": "#87909c80", - "sidebarLogoBackground": "#f1f1f1", - "sidebarActiveColor": "#ffffff", - "sidebarSubmenuActiveColor": "#3d90ce", - "sidebarBackground": "#e8e8e8", - "sidebarItemHoverBackground": "#f0f2f5", - "buttonPrimaryBackground": "#3d90ce", - "buttonDefaultBackground": "#414448", - "buttonLightBackground": "#f1f1f1", - "buttonAccentBackground": "#e85555", - "buttonDisabledBackground": "#808080", - "tableBgcStripe": "#f8f8f8", - "tableBgcSelected": "#f4f5f5", - "tableHeaderBackground": "#f1f1f1", - "tableHeaderBorderWidth": "2px", - "tableHeaderBorderColor": "#3d90ce", - "tableHeaderColor": "#555555", - "tableSelectedRowColor": "#222222", - "helmLogoBackground": "#ffffff", - "helmImgBackground": "#e8e8e8", - "helmStableRepo": "#3d90ce", - "helmIncubatorRepo": "#ff7043", - "helmDescriptionHr": "#dddddd", - "helmDescriptionBlockquoteColor": "#555555", - "helmDescriptionBlockquoteBorder": "#8a8f93", - "helmDescriptionBlockquoteBackground": "#eeeeee", - "helmDescriptionHeaders": "#3e4147", - "helmDescriptionH6": "#6a737d", - "helmDescriptionTdBorder": "#c6c6c6", - "helmDescriptionTrBackground": "#1c2125", - "helmDescriptionCodeBackground": "#ffffff1a", - "helmDescriptionPreBackground": "#eeeeee", - "helmDescriptionPreColor": "#555555", - "colorSuccess": "#206923", - "colorOk": "#399c3d", - "colorInfo": "#2d71a4", - "colorError": "#ce3933", - "colorSoftError": "#e85555", - "colorWarning": "#ff9800", - "colorVague": "#ededed", - "colorTerminated": "#9dabb5", - "dockHeadBackground": "#e8e8e8", - "dockInfoBackground": "#f3f3f3", - "dockInfoBorderColor": "#c9cfd3", - "dockEditorBackground": "#24292e", - "dockEditorTag": "#8e97a3", - "dockEditorKeyword": "#ffffff", - "dockEditorComment": "#808080", - "dockEditorActiveLineBackground": "#3a3d41", - "dockBadgeBackground": "#dedede", - "dockTabBorderColor": "#d5d4de", - "dockTabActiveBackground": "#ffffff", - "logsBackground": "#24292e", - "logsForeground": "#ffffff", - "logRowHoverBackground": "#35373a", - "terminalBackground": "#ffffff", - "terminalForeground": "#2d2d2d", - "terminalCursor": "#2d2d2d", - "terminalCursorAccent": "#ffffff", - "terminalSelection": "#bfbfbf", - "terminalBlack": "#2d2d2d", - "terminalRed": "#cd3734 ", - "terminalGreen": "#18cf12", - "terminalYellow": "#acb300", - "terminalBlue": "#3d90ce", - "terminalMagenta": "#c100cd", - "terminalCyan": "#07c4b9", - "terminalWhite": "#d3d7cf", - "terminalBrightBlack": "#a8a8a8", - "terminalBrightRed": "#ff6259", - "terminalBrightGreen": "#5cdb59", - "terminalBrightYellow": "#f8c000", - "terminalBrightBlue": "#008db6", - "terminalBrightMagenta": "#ee55f8", - "terminalBrightCyan": "#50e8df", - "terminalBrightWhite": "#eeeeec", - "dialogTextColor": "#87909c", - "dialogBackground": "#ffffff", - "dialogHeaderBackground": "#36393e", - "dialogFooterBackground": "#f4f4f4", - "drawerTogglerBackground": "#eaeced", - "drawerTitleText": "#ffffff", - "drawerSubtitleBackground": "#f1f1f1", - "drawerItemNameColor": "#727272", - "drawerItemValueColor": "#555555", - "clusterMenuBackground": "#d7d8da", - "clusterMenuBorderColor": "#c9cfd3", - "clusterMenuCellBackground": "#bbbbbb", - "clusterSettingsBackground": "#ffffff", - "addClusterIconColor": "#8d8d8d", - "boxShadow": "#0000003a", - "iconActiveColor": "#ffffff", - "iconActiveBackground": "#a6a6a694", - "filterAreaBackground": "#f7f7f7", - "chartLiveBarBackground": "#00000033", - "chartStripesColor": "#00000009", - "chartCapacityColor": "#cccccc", - "pieChartDefaultColor": "#efefef", - "inputOptionHoverColor": "#ffffff", - "inputControlBackground": "#f6f6f7", - "inputControlBorder": "#cccdcf", - "inputControlHoverBorder": "#b9bbbe", - "lineProgressBackground": "#e8e8e8", - "radioActiveBackground": "#f1f1f1", - "menuActiveBackground": "#3d90ce", - "menuSelectedOptionBgc": "#e8e8e8", - "canvasBackground": "#24292e", - "scrollBarColor": "#bbbbbb", - "settingsBackground": "#ffffff", - "settingsColor": "#555555", - "navSelectedBackground": "#ffffff", - "navHoverColor": "#2e3135", - "hrColor": "#06060714", - "tooltipBackground": "#ffffff" - } -} diff --git a/src/renderer/themes/lens-light.ts b/src/renderer/themes/lens-light.ts new file mode 100644 index 0000000000..a19a188d4d --- /dev/null +++ b/src/renderer/themes/lens-light.ts @@ -0,0 +1,150 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { Theme } from "./types"; + +const lensLightTheme: Theme = { + name: "Light", + type: "light", + description: "Original Lens light theme", + author: "Mirantis", + monacoTheme: "vs", + colors: { + blue: "#3d90ce", + magenta: "#c93dce", + golden: "#ffc63d", + halfGray: "#87909c80", + primary: "#3d90ce", + textColorPrimary: "#555555", + textColorSecondary: "#51575d", + textColorTertiary: "#555555", + textColorAccent: "#222222", + textColorDimmed: "#5557598c", + borderColor: "#c9cfd3", + borderFaintColor: "#dfdfdf", + mainBackground: "#f1f1f1", + secondaryBackground: "#f2f3f5", + contentColor: "#ffffff", + layoutBackground: "#e8e8e8", + layoutTabsBackground: "#f8f8f8", + layoutTabsActiveColor: "#333333", + layoutTabsLineColor: "#87909c80", + sidebarLogoBackground: "#f1f1f1", + sidebarActiveColor: "#ffffff", + sidebarSubmenuActiveColor: "#3d90ce", + sidebarBackground: "#e8e8e8", + sidebarItemHoverBackground: "#f0f2f5", + buttonPrimaryBackground: "#3d90ce", + buttonDefaultBackground: "#414448", + buttonLightBackground: "#f1f1f1", + buttonAccentBackground: "#e85555", + buttonDisabledBackground: "#808080", + tableBgcStripe: "#f8f8f8", + tableBgcSelected: "#f4f5f5", + tableHeaderBackground: "#f1f1f1", + tableHeaderBorderWidth: "2px", + tableHeaderBorderColor: "#3d90ce", + tableHeaderColor: "#555555", + tableSelectedRowColor: "#222222", + helmLogoBackground: "#ffffff", + helmImgBackground: "#e8e8e8", + helmStableRepo: "#3d90ce", + helmIncubatorRepo: "#ff7043", + helmDescriptionHr: "#dddddd", + helmDescriptionBlockquoteColor: "#555555", + helmDescriptionBlockquoteBorder: "#8a8f93", + helmDescriptionBlockquoteBackground: "#eeeeee", + helmDescriptionHeaders: "#3e4147", + helmDescriptionH6: "#6a737d", + helmDescriptionTdBorder: "#c6c6c6", + helmDescriptionTrBackground: "#1c2125", + helmDescriptionCodeBackground: "#ffffff1a", + helmDescriptionPreBackground: "#eeeeee", + helmDescriptionPreColor: "#555555", + colorSuccess: "#206923", + colorOk: "#399c3d", + colorInfo: "#2d71a4", + colorError: "#ce3933", + colorSoftError: "#e85555", + colorWarning: "#ff9800", + colorVague: "#ededed", + colorTerminated: "#9dabb5", + dockHeadBackground: "#e8e8e8", + dockInfoBackground: "#f3f3f3", + dockInfoBorderColor: "#c9cfd3", + dockEditorBackground: "#24292e", + dockEditorTag: "#8e97a3", + dockEditorKeyword: "#ffffff", + dockEditorComment: "#808080", + dockEditorActiveLineBackground: "#3a3d41", + dockBadgeBackground: "#dedede", + dockTabBorderColor: "#d5d4de", + dockTabActiveBackground: "#ffffff", + logsBackground: "#24292e", + logsForeground: "#ffffff", + logRowHoverBackground: "#35373a", + terminalBackground: "#ffffff", + terminalForeground: "#2d2d2d", + terminalCursor: "#2d2d2d", + terminalCursorAccent: "#ffffff", + terminalSelection: "#bfbfbf", + terminalBlack: "#2d2d2d", + terminalRed: "#cd3734 ", + terminalGreen: "#18cf12", + terminalYellow: "#acb300", + terminalBlue: "#3d90ce", + terminalMagenta: "#c100cd", + terminalCyan: "#07c4b9", + terminalWhite: "#d3d7cf", + terminalBrightBlack: "#a8a8a8", + terminalBrightRed: "#ff6259", + terminalBrightGreen: "#5cdb59", + terminalBrightYellow: "#f8c000", + terminalBrightBlue: "#008db6", + terminalBrightMagenta: "#ee55f8", + terminalBrightCyan: "#50e8df", + terminalBrightWhite: "#eeeeec", + dialogTextColor: "#87909c", + dialogBackground: "#ffffff", + dialogHeaderBackground: "#36393e", + dialogFooterBackground: "#f4f4f4", + drawerTogglerBackground: "#eaeced", + drawerTitleText: "#ffffff", + drawerSubtitleBackground: "#f1f1f1", + drawerItemNameColor: "#727272", + drawerItemValueColor: "#555555", + clusterMenuBackground: "#d7d8da", + clusterMenuBorderColor: "#c9cfd3", + clusterMenuCellBackground: "#bbbbbb", + clusterSettingsBackground: "#ffffff", + addClusterIconColor: "#8d8d8d", + boxShadow: "#0000003a", + iconActiveColor: "#ffffff", + iconActiveBackground: "#a6a6a694", + filterAreaBackground: "#f7f7f7", + chartLiveBarBackground: "#00000033", + chartStripesColor: "#00000009", + chartCapacityColor: "#cccccc", + pieChartDefaultColor: "#efefef", + inputOptionHoverColor: "#ffffff", + inputControlBackground: "#f6f6f7", + inputControlBorder: "#cccdcf", + inputControlHoverBorder: "#b9bbbe", + lineProgressBackground: "#e8e8e8", + radioActiveBackground: "#f1f1f1", + menuActiveBackground: "#3d90ce", + menuSelectedOptionBgc: "#e8e8e8", + canvasBackground: "#24292e", + scrollBarColor: "#bbbbbb", + settingsBackground: "#ffffff", + settingsColor: "#555555", + navSelectedBackground: "#ffffff", + navHoverColor: "#2e3135", + hrColor: "#06060714", + tooltipBackground: "#ffffff", + }, +}; + +export default lensLightTheme; diff --git a/src/renderer/theme-store.injectable.ts b/src/renderer/themes/store.injectable.ts similarity index 90% rename from src/renderer/theme-store.injectable.ts rename to src/renderer/themes/store.injectable.ts index 40e8a6fc59..923241eb77 100644 --- a/src/renderer/theme-store.injectable.ts +++ b/src/renderer/themes/store.injectable.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { ThemeStore } from "./theme.store"; +import { ThemeStore } from "./store"; const themeStoreInjectable = getInjectable({ id: "theme-store", diff --git a/src/renderer/themes/store.ts b/src/renderer/themes/store.ts new file mode 100644 index 0000000000..739f013b43 --- /dev/null +++ b/src/renderer/themes/store.ts @@ -0,0 +1,177 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { comparer, computed, makeObservable, observable, reaction } from "mobx"; +import { autoBind, entries, fromEntries, Singleton, startsWith } from "../utils"; +import { UserStore } from "../../common/user-store"; +import logger from "../../main/logger"; +import lensDarkThemeJson from "./lens-dark"; +import lensLightThemeJson from "./lens-light"; +import { ipcRenderer } from "electron"; +import { getNativeThemeChannel, setNativeThemeChannel } from "../../common/ipc/native-theme"; +import { Theme, ThemeId, TerminalColors, ThemeType, terminalColorPrefix, PrefixedTerminalColorPair } from "./types"; +import { camelCase } from "lodash/fp"; +import assert from "assert"; + +const themeNameExtractor = /$(?.+)-(dark|light)^/; + +export const themeTypeOptions = [ + { + value: "dark", + label: "Dark", + }, + { + value: "light", + label: "Light", + }, +]; + +export class ThemeStore extends Singleton { + private themes = observable.map({ + "lens-dark": lensDarkThemeJson, + "lens-light": lensLightThemeJson, + }); + + @computed get themeNames(): string[] { + const names = new Set(); + + for (const themeId of this.themes.keys()) { + const match = themeId.match(themeNameExtractor); + + assert(match, "All ThemeId's MUST have a suffix of '-dark' or '-light'"); + + names.add(match.groups.name); + } + + return [...names]; + } + + @observable osNativeTheme: ThemeType = "dark"; + + @computed get activeThemeType(): ThemeType { + const { colorTheme } = UserStore.getInstance(); + + if (colorTheme.followSystemThemeType === true) { + return this.osNativeTheme; + } + + return colorTheme.type; + } + + @computed get activeThemeName(): string { + const { colorTheme } = UserStore.getInstance(); + + return colorTheme.name; + } + + @computed get activeThemeId(): ThemeId { + return `${this.activeThemeName}-${this.activeThemeType}`; + } + + @computed get activeTheme(): Theme { + return this.themes.get(this.activeThemeId) ?? lensDarkThemeJson; + } + + @computed get activeTerminalThemeType(): ThemeType { + const { terminalTheme } = UserStore.getInstance(); + + if (terminalTheme.isGlobalThemeType === true) { + return this.activeThemeType; + } + + return terminalTheme.type; + } + + @computed get activeTerminalThemeName(): string { + const { terminalTheme } = UserStore.getInstance(); + + if (terminalTheme.isGlobalTheme === true) { + return this.activeThemeName; + } + + return terminalTheme.name; + } + + @computed get terminalThemeId(): ThemeId { + return `${this.activeTerminalThemeName}-${this.activeTerminalThemeType}`; + } + + @computed get activeTerminalTheme(): Theme { + return this.themes.get(this.terminalThemeId) ?? lensDarkThemeJson; + } + + @computed private get terminalThemeColors(): PrefixedTerminalColorPair[] { + const theme = this.activeTerminalTheme; + + return entries(theme.colors) + .filter((value): value is PrefixedTerminalColorPair => ( + startsWith(value[0], terminalColorPrefix) + )); + } + + @computed get xtermColors(): TerminalColors { + return fromEntries( + this.terminalThemeColors + .map(([name, color]) => [ + camelCase(name.slice(terminalColorPrefix.length)) as keyof TerminalColors, + color, + ]), + ); + } + + constructor() { + super(); + makeObservable(this); + autoBind(this); + this.init(); + } + + async init() { + await this.setNativeTheme(); + this.bindNativeThemeUpdateEvent(); + + // auto-apply active theme + reaction(() => ({ + theme: this.activeTheme, + terminalTheme: this.terminalThemeColors, + }), (themes) => { + try { + this.applyTheme(themes); + } catch (err) { + logger.error(err); + UserStore.getInstance().resetThemeSettings(); + } + }, { + fireImmediately: true, + equals: comparer.shallow, + }); + } + + bindNativeThemeUpdateEvent() { + ipcRenderer.on(setNativeThemeChannel, (event, theme: ThemeType) => { + this.osNativeTheme = theme; + }); + } + + async setNativeTheme() { + const theme: "dark" | "light" = await ipcRenderer.invoke(getNativeThemeChannel); + + this.osNativeTheme = theme; + } + + protected applyTheme({ theme, terminalTheme }: { theme: Theme; terminalTheme: PrefixedTerminalColorPair[] }) { + const colors = Object.entries({ + ...theme.colors, + ...Object.fromEntries(terminalTheme), + }); + + colors.forEach(([name, value]) => { + document.documentElement.style.setProperty(`--${name}`, value); + }); + + // Adding universal theme flag which can be used in component styles + document.body.classList.toggle("theme-light", theme.type === "light"); + } +} diff --git a/src/renderer/themes/types.ts b/src/renderer/themes/types.ts new file mode 100644 index 0000000000..b96d0d7b08 --- /dev/null +++ b/src/renderer/themes/types.ts @@ -0,0 +1,164 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { PrefixCamelCasedProperties } from "../../common/utils/types"; +import type { MonacoTheme } from "../components/monaco-editor"; + +export type ThemeType = "dark" | "light"; +export type ThemeId = `${string}-${ThemeType}`; +export type ThemeColor = `#${string}`; +export type ThemeLengthUnit = "px" | "ch" | "em" | "ex" | "rem" | "vh" | "vw" | "vmin" | "vmax" | "cm" | "mm" | "in" | "pc" | "pt"; +export type ThemeLength = `${string}${ThemeLengthUnit}`; + +export interface TerminalColors { + background: ThemeColor; + foreground: ThemeColor; + cursor: ThemeColor; + cursorAccent: ThemeColor; + selection: ThemeColor; + black: ThemeColor; + red: ThemeColor; + green: ThemeColor; + yellow: ThemeColor; + blue: ThemeColor; + magenta: ThemeColor; + cyan: ThemeColor; + white: ThemeColor; + brightBlack: ThemeColor; + brightRed: ThemeColor; + brightGreen: ThemeColor; + brightYellow: ThemeColor; + brightBlue: ThemeColor; + brightMagenta: ThemeColor; + brightCyan: ThemeColor; + brightWhite: ThemeColor; +} + +export const terminalColorPrefix = "terminal"; +export type PrefixedTerminalColors = PrefixCamelCasedProperties; +export type PrefixedTerminalColorPair = [keyof PrefixedTerminalColors, ThemeColor]; + +export interface ThemeColors extends PrefixedTerminalColors { + blue: ThemeColor; + magenta: ThemeColor; + golden: ThemeColor; + halfGray: ThemeColor; + primary: ThemeColor; + textColorPrimary: ThemeColor; + textColorSecondary: ThemeColor; + textColorTertiary: ThemeColor; + textColorAccent: ThemeColor; + textColorDimmed: ThemeColor; + borderColor: ThemeColor; + borderFaintColor: ThemeColor; + mainBackground: ThemeColor; + secondaryBackground: ThemeColor; + contentColor: ThemeColor; + layoutBackground: ThemeColor; + layoutTabsBackground: ThemeColor; + layoutTabsActiveColor: ThemeColor; + layoutTabsLineColor: ThemeColor; + sidebarLogoBackground: ThemeColor; + sidebarActiveColor: ThemeColor; + sidebarSubmenuActiveColor: ThemeColor; + sidebarBackground: ThemeColor; + sidebarItemHoverBackground: ThemeColor; + buttonPrimaryBackground: ThemeColor; + buttonDefaultBackground: ThemeColor; + buttonLightBackground: ThemeColor; + buttonAccentBackground: ThemeColor; + buttonDisabledBackground: ThemeColor; + tableBgcStripe: ThemeColor; + tableBgcSelected: ThemeColor; + tableHeaderBackground: ThemeColor; + tableHeaderBorderWidth: string; + tableHeaderBorderColor: ThemeColor; + tableHeaderColor: ThemeColor; + tableSelectedRowColor: ThemeColor; + helmLogoBackground: ThemeColor; + helmImgBackground: ThemeColor; + helmStableRepo: ThemeColor; + helmIncubatorRepo: ThemeColor; + helmDescriptionHr: ThemeColor; + helmDescriptionBlockquoteColor: ThemeColor; + helmDescriptionBlockquoteBorder: ThemeColor; + helmDescriptionBlockquoteBackground: ThemeColor; + helmDescriptionHeaders: ThemeColor; + helmDescriptionH6: ThemeColor; + helmDescriptionTdBorder: ThemeColor; + helmDescriptionTrBackground: ThemeColor; + helmDescriptionCodeBackground: ThemeColor; + helmDescriptionPreBackground: ThemeColor; + helmDescriptionPreColor: ThemeColor; + colorSuccess: ThemeColor; + colorOk: ThemeColor; + colorInfo: ThemeColor; + colorError: ThemeColor; + colorSoftError: ThemeColor; + colorWarning: ThemeColor; + colorVague: ThemeColor; + colorTerminated: ThemeColor; + dockHeadBackground: ThemeColor; + dockInfoBackground: ThemeColor; + dockInfoBorderColor: ThemeColor; + dockEditorBackground: ThemeColor; + dockEditorTag: ThemeColor; + dockEditorKeyword: ThemeColor; + dockEditorComment: ThemeColor; + dockEditorActiveLineBackground: ThemeColor; + dockBadgeBackground: ThemeColor; + dockTabBorderColor: ThemeColor; + dockTabActiveBackground: ThemeColor; + logsBackground: ThemeColor; + logsForeground: ThemeColor; + logRowHoverBackground: ThemeColor; + dialogTextColor: ThemeColor; + dialogBackground: ThemeColor; + dialogHeaderBackground: ThemeColor; + dialogFooterBackground: ThemeColor; + drawerTogglerBackground: ThemeColor; + drawerTitleText: ThemeColor; + drawerSubtitleBackground: ThemeColor; + drawerItemNameColor: ThemeColor; + drawerItemValueColor: ThemeColor; + clusterMenuBackground: ThemeColor; + clusterMenuBorderColor: ThemeColor; + clusterMenuCellBackground: ThemeColor; + clusterSettingsBackground: ThemeColor; + addClusterIconColor: ThemeColor; + boxShadow: ThemeColor; + iconActiveColor: ThemeColor; + iconActiveBackground: ThemeColor; + filterAreaBackground: ThemeColor; + chartLiveBarBackground: ThemeColor; + chartStripesColor: ThemeColor; + chartCapacityColor: ThemeColor; + pieChartDefaultColor: ThemeColor; + inputOptionHoverColor: ThemeColor; + inputControlBackground: ThemeColor; + inputControlBorder: ThemeColor; + inputControlHoverBorder: ThemeColor; + lineProgressBackground: ThemeColor; + radioActiveBackground: ThemeColor; + menuActiveBackground: ThemeColor; + menuSelectedOptionBgc: ThemeColor; + canvasBackground: ThemeColor; + scrollBarColor: ThemeColor; + settingsBackground: ThemeColor; + settingsColor: ThemeColor; + navSelectedBackground: ThemeColor; + navHoverColor: ThemeColor; + hrColor: ThemeColor; + tooltipBackground: ThemeColor; +} + +export interface Theme { + name: string; + type: ThemeType; + colors: ThemeColors; + description: string; + author: string; + monacoTheme: MonacoTheme; +}