1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Fix crash when switching OS theme type

- Changed the representation of theme preferences to no longer use
  sentinal values and instead use flags

- This new format is forward looking to when support for user provided
  themes is added, which is now quite easy.

- Added unit tests for the migration code

- Made Theme's more type safe my having the lens themes be .ts files

- Moved ThemeStore into renderer/themes as it is a more fitting place

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2022-04-01 15:13:56 -04:00
parent cfc702f89f
commit 8b19da5359
39 changed files with 892 additions and 394 deletions

View File

@ -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",

View File

@ -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",
});
});
});
});

View File

@ -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<T, R = T> {
fromStore(val: T | undefined): R;
toStore(val: R): T | undefined;
toStore(val: R | undefined): T | undefined;
}
const httpsProxy: PreferenceDescription<string | undefined> = {
@ -64,12 +66,28 @@ const shell: PreferenceDescription<string | undefined> = {
},
};
const colorTheme: PreferenceDescription<string> = {
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<ColorThemeConf> = {
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<string> = {
},
};
const terminalTheme: PreferenceDescription<string | undefined> = {
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<TerminalThemeConf> = {
fromStore(val) {
return val || "";
return val || defaultTerminalThemeConf;
},
toStore(val) {
return val || undefined;
if (!val || equals(val, defaultTerminalThemeConf)) {
return undefined;
}
return defaultTerminalThemeConf;
},
};

View File

@ -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<UserStoreModel> /* 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<UserStoreModel> /* implements UserStore
}
@action
resetTheme() {
resetThemeSettings() {
this.colorTheme = DESCRIPTORS.colorTheme.fromStore(undefined);
this.terminalTheme = DESCRIPTORS.terminalTheme.fromStore(undefined);
}
@action

View File

@ -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<T extends string>(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<T extends string>(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

16
src/common/utils/types.ts Normal file
View File

@ -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> = S extends string
? Capitalize<S>
: never;
export type PrefixCamelCasedProperties<Value, Prefix extends string> = Value extends Function
? Value
: Value extends Array<any>
? Value
: {
[K in keyof Value as `${Prefix}${SafeCapitalize<K>}`]: Value[K];
};

View File

@ -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";

View File

@ -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;

View File

@ -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 = /^(?<name>[a-z0-9]+)-(?<type>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;

View File

@ -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,
);

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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<Node, { metrics: IClusterMetrics }>;

View File

@ -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<string>[] = moment.tz.names().map(zone => ({
label: zone,
@ -35,10 +34,9 @@ const updateChannelOptions: SelectOption<string>[] = Array.from(
interface Dependencies {
appPreferenceItems: IComputedValue<RegisteredAppPreference[]>;
userStore: UserStore;
themeStore: ThemeStore;
}
const NonInjectedApplication: React.FC<Dependencies> = ({ 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<Dependencies> = ({ appPreferenceItems, us
<h2 data-testid="application-header">Application</h2>
<section id="appearance">
<SubTitle title="Theme" />
<Select
id="theme-input"
options={[
{ label: "Sync with computer", value: "system" },
...themeStore.themeOptions,
]}
value={userStore.colorTheme}
onChange={({ value }) => userStore.colorTheme = value}
themeName="lens"
/>
<Switch
checked={userStore.colorTheme.followSystemThemeType}
onChange={() => userStore.colorTheme.followSystemThemeType = !userStore.colorTheme.followSystemThemeType}
>
Sync theme type with Operating System settings
</Switch>
{!userStore.colorTheme.followSystemThemeType && (
<>
<p className="mt-4 mb-5 leading-relaxed">
Choose whether you want dark or light theming for Lens.
</p>
<Select
id="theme-type-input"
options={themeTypeOptions}
value={userStore.colorTheme.type}
onChange={({ value }) => userStore.colorTheme.type = value}
themeName="lens"
/>
</>
)}
</section>
<hr />
@ -135,16 +144,11 @@ const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems, us
</section>
</Preferences>
);
};
});
export const Application = withInjectables<Dependencies>(
observer(NonInjectedApplication),
{
getProps: (di) => ({
appPreferenceItems: di.inject(appPreferencesInjectable),
userStore: di.inject(userStoreInjectable),
themeStore: di.inject(themeStoreInjectable),
}),
},
);
export const Application = withInjectables<Dependencies>(NonInjectedApplication, {
getProps: (di) => ({
appPreferenceItems: di.inject(appPreferencesInjectable),
userStore: di.inject(userStoreInjectable),
}),
});

View File

@ -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 (
<Preferences data-testid="terminal-preferences-page">
<section>
@ -51,18 +49,28 @@ const NonInjectedTerminal = observer(({ userStore, themeStore, defaultShell }: D
<section id="terminalTheme">
<SubTitle title="Terminal theme" />
<Select
id="terminal-theme-input"
themeName="lens"
options={[
{ label: "Match theme", value: "" },
...themeStore.themeOptions,
]}
value={userStore.terminalTheme}
onChange={({ value }) => userStore.terminalTheme = value}
/>
<Switch
checked={userStore.terminalTheme.isGlobalThemeType}
onChange={() => userStore.terminalTheme.isGlobalThemeType = !userStore.terminalTheme.isGlobalThemeType}
>
Sync terminal theme type with that of Lens
</Switch>
{
!userStore.terminalTheme.isGlobalThemeType && (
<Select
id="terminal-theme-input"
themeName="lens"
options={themeTypeOptions}
value={userStore.terminalTheme.type}
onChange={({ value }) => userStore.terminalTheme.type = value}
/>
)
}
</section>
{/* Once other themes are supported add an option to sync those as well */}
<section>
<SubTitle title="Font size" />
<Input
@ -88,15 +96,10 @@ const NonInjectedTerminal = observer(({ userStore, themeStore, defaultShell }: D
);
});
export const Terminal = withInjectables<Dependencies>(
NonInjectedTerminal,
{
getProps: (di) => ({
userStore: di.inject(userStoreInjectable),
themeStore: di.inject(themeStoreInjectable),
defaultShell: di.inject(defaultShellInjectable),
}),
},
);
export const Terminal = withInjectables<Dependencies>(NonInjectedTerminal, {
getProps: (di) => ({
userStore: di.inject(userStoreInjectable),
defaultShell: di.inject(defaultShellInjectable),
}),
});

View File

@ -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<PersistentVolumeClaim, { metrics: IPvcMetrics }>;

View File

@ -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<string, number>;

View File

@ -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<any, { metrics: IPodMetrics }>;

View File

@ -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 {

View File

@ -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 {
}

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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;

View File

@ -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";

View File

@ -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;

View File

@ -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<string, string>;
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<ThemeId, Theme>({
"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<string, string> {
return Object.fromEntries(
this.terminalColors.map(([name, color]) => [
camelCase(name.replace(this.terminalColorPrefix, "")),
color,
]),
);
}
@computed get themeOptions(): SelectOption<string>[] {
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");
}
}

View File

@ -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;

View File

@ -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"
}
}

View File

@ -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;

View File

@ -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",

View File

@ -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 = /$(?<name>.+)-(dark|light)^/;
export const themeTypeOptions = [
{
value: "dark",
label: "Dark",
},
{
value: "light",
label: "Light",
},
];
export class ThemeStore extends Singleton {
private themes = observable.map<ThemeId, Theme>({
"lens-dark": lensDarkThemeJson,
"lens-light": lensLightThemeJson,
});
@computed get themeNames(): string[] {
const names = new Set<string>();
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");
}
}

View File

@ -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<TerminalColors, typeof terminalColorPrefix>;
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;
}