diff --git a/src/common/user-store/preferences-helpers.ts b/src/common/user-store/preferences-helpers.ts index 1ded307f0d..e20e259556 100644 --- a/src/common/user-store/preferences-helpers.ts +++ b/src/common/user-store/preferences-helpers.ts @@ -83,6 +83,15 @@ const colorTheme: PreferenceDescription = { }, }; +const terminalTheme: PreferenceDescription = { + fromStore(val) { + return val || ""; + }, + toStore(val) { + return val || undefined; + }, +}; + const localeTimezone: PreferenceDescription = { fromStore(val) { return val || moment.tz.guess(true) || "UTC"; @@ -351,6 +360,7 @@ export const DESCRIPTORS = { httpsProxy, shell, colorTheme, + terminalTheme, localeTimezone, allowUntrustedCAs, allowTelemetry, diff --git a/src/common/user-store/user-store.ts b/src/common/user-store/user-store.ts index 31238f0ded..952ca32ecc 100644 --- a/src/common/user-store/user-store.ts +++ b/src/common/user-store/user-store.ts @@ -67,6 +67,7 @@ export class UserStore extends BaseStore /* implements UserStore @observable allowErrorReporting: boolean; @observable allowUntrustedCAs: boolean; @observable colorTheme: string; + @observable terminalTheme: string; @observable localeTimezone: string; @observable downloadMirror: string; @observable httpsProxy?: string; @@ -188,6 +189,7 @@ export class UserStore extends BaseStore /* implements UserStore this.httpsProxy = DESCRIPTORS.httpsProxy.fromStore(preferences?.httpsProxy); this.shell = DESCRIPTORS.shell.fromStore(preferences?.shell); this.colorTheme = DESCRIPTORS.colorTheme.fromStore(preferences?.colorTheme); + this.terminalTheme = DESCRIPTORS.terminalTheme.fromStore(preferences?.terminalTheme); this.localeTimezone = DESCRIPTORS.localeTimezone.fromStore(preferences?.localeTimezone); this.allowUntrustedCAs = DESCRIPTORS.allowUntrustedCAs.fromStore(preferences?.allowUntrustedCAs); this.allowTelemetry = DESCRIPTORS.allowTelemetry.fromStore(preferences?.allowTelemetry); @@ -212,6 +214,7 @@ export class UserStore extends BaseStore /* implements UserStore httpsProxy: DESCRIPTORS.httpsProxy.toStore(this.httpsProxy), shell: DESCRIPTORS.shell.toStore(this.shell), colorTheme: DESCRIPTORS.colorTheme.toStore(this.colorTheme), + terminalTheme: DESCRIPTORS.terminalTheme.toStore(this.terminalTheme), localeTimezone: DESCRIPTORS.localeTimezone.toStore(this.localeTimezone), allowUntrustedCAs: DESCRIPTORS.allowUntrustedCAs.toStore(this.allowUntrustedCAs), allowTelemetry: DESCRIPTORS.allowTelemetry.toStore(this.allowTelemetry), diff --git a/src/renderer/components/+preferences/application.tsx b/src/renderer/components/+preferences/application.tsx index 1e3c75b68f..dd2d1bee90 100644 --- a/src/renderer/components/+preferences/application.tsx +++ b/src/renderer/components/+preferences/application.tsx @@ -57,24 +57,38 @@ export const Application = observer(() => { const [customUrl, setCustomUrl] = React.useState(userStore.extensionRegistryUrl.customUrl || ""); const [shell, setShell] = React.useState(userStore.shell || ""); const extensionSettings = AppPreferenceRegistry.getInstance().getItems().filter((preference) => preference.showInPreferencesTab === "application"); + const themeStore = ThemeStore.getInstance(); return (

Application

- + userStore.terminalTheme = value} + /> +
- + {
-
+
diff --git a/src/renderer/components/dock/dock.scss b/src/renderer/components/dock/dock.scss index d473a089fa..49bbcdf6b3 100644 --- a/src/renderer/components/dock/dock.scss +++ b/src/renderer/components/dock/dock.scss @@ -76,11 +76,14 @@ .tab-content { position: relative; - background: var(--terminalBackground); flex: 1; overflow: hidden; transition: flex-basis 25ms ease-in; + &.terminal { + background: var(--terminalBackground); + } + > *:not(.Spinner) { position: absolute; left: 0; diff --git a/src/renderer/components/dock/dock.tsx b/src/renderer/components/dock/dock.tsx index 747fd37a5c..90dc49c478 100644 --- a/src/renderer/components/dock/dock.tsx +++ b/src/renderer/components/dock/dock.tsx @@ -105,7 +105,7 @@ export class Dock extends React.Component { if (!isOpen || !selectedTab) return null; return ( -
+
{this.renderTab(selectedTab)}
); diff --git a/src/renderer/components/dock/terminal.ts b/src/renderer/components/dock/terminal.ts index 383accab20..7028081784 100644 --- a/src/renderer/components/dock/terminal.ts +++ b/src/renderer/components/dock/terminal.ts @@ -26,9 +26,9 @@ import { FitAddon } from "xterm-addon-fit"; import { dockStore, TabId } from "./dock.store"; import { TerminalApi, TerminalChannels } from "../../api/terminal-api"; import { ThemeStore } from "../../theme.store"; -import { boundMethod, disposer } from "../../utils"; +import { disposer } from "../../utils"; import { isMac } from "../../../common/vars"; -import { camelCase, once } from "lodash"; +import { once } from "lodash"; import { UserStore } from "../../../common/user-store"; import { clipboard } from "electron"; import logger from "../../../common/logger"; @@ -56,23 +56,6 @@ export class Terminal { private scrollPos = 0; private disposer = disposer(); - @boundMethod - protected setTheme(colors: Record) { - if (!this.xterm) { - return; - } - - // Replacing keys stored in styles to format accepted by terminal - // E.g. terminalBrightBlack -> brightBlack - const colorPrefix = "terminal"; - const terminalColorEntries = Object.entries(colors) - .filter(([name]) => name.startsWith(colorPrefix)) - .map(([name, color]) => [camelCase(name.slice(colorPrefix.length)), color]); - const terminalColors = Object.fromEntries(terminalColorEntries); - - this.xterm.setOption("theme", terminalColors); - } - get elem() { return this.xterm?.element; } @@ -121,7 +104,9 @@ export class Terminal { window.addEventListener("resize", this.onResize); this.disposer.push( - reaction(() => ThemeStore.getInstance().activeTheme.colors, this.setTheme, { + reaction(() => ThemeStore.getInstance().xtermColors, colors => { + this.xterm?.setOption("theme", colors); + }, { fireImmediately: true, }), dockStore.onResize(this.onResize), diff --git a/src/renderer/theme.store.ts b/src/renderer/theme.store.ts index 15aa055f92..f39a500c71 100644 --- a/src/renderer/theme.store.ts +++ b/src/renderer/theme.store.ts @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { computed, makeObservable, observable, reaction } from "mobx"; +import { comparer, computed, makeObservable, observable, reaction } from "mobx"; import { autoBind, Singleton } from "./utils"; import { UserStore } from "../common/user-store"; import logger from "../main/logger"; @@ -28,6 +28,7 @@ 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"; export type ThemeId = string; @@ -41,7 +42,7 @@ export interface Theme { } export class ThemeStore extends Singleton { - protected styles: HTMLStyleElement; + private terminalColorPrefix = "terminal"; // bundled themes from `themes/${themeId}.json` private themes = observable.map({ @@ -49,14 +50,37 @@ export class ThemeStore extends Singleton { "lens-light": lensLightThemeJson as Theme, }); - @computed get activeThemeId(): string { + @computed get activeThemeId(): ThemeId { return UserStore.getInstance().colorTheme; } + @computed get terminalThemeId(): ThemeId { + return UserStore.getInstance().terminalTheme; + } + @computed get activeTheme(): Theme { return 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, @@ -71,15 +95,19 @@ export class ThemeStore extends Singleton { autoBind(this); // auto-apply active theme - reaction(() => this.activeThemeId, themeId => { + reaction(() => ({ + themeId: this.activeThemeId, + terminalThemeId: this.terminalThemeId, + }), ({ themeId }) => { try { - this.applyTheme(this.getThemeById(themeId)); + this.applyTheme(themeId); } catch (err) { logger.error(err); UserStore.getInstance().resetTheme(); } }, { fireImmediately: true, + equals: comparer.shallow, }); } @@ -87,20 +115,18 @@ export class ThemeStore extends Singleton { return this.themes.get(themeId); } - protected applyTheme(theme: Theme) { - if (!this.styles) { - this.styles = document.createElement("style"); - this.styles.id = "lens-theme"; - document.head.append(this.styles); - } - const cssVars = Object.entries(theme.colors).map(([cssName, color]) => { - return `--${cssName}: ${color};`; + protected applyTheme(themeId: ThemeId) { + const theme = this.getThemeById(themeId); + const colors = Object.entries({ + ...theme.colors, + ...Object.fromEntries(this.terminalColors), }); - this.styles.textContent = `:root {\n${cssVars.join("\n")}}`; - // Adding universal theme flag which can be used in component styles - const body = document.querySelector("body"); + colors.forEach(([name, value]) => { + document.documentElement.style.setProperty(`--${name}`, value); + }); - body.classList.toggle("theme-light", theme.type === "light"); + // 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.json index 023f9f6ccf..b48ed84cc7 100644 --- a/src/renderer/themes/lens-dark.json +++ b/src/renderer/themes/lens-dark.json @@ -1,5 +1,5 @@ { - "name": "Dark (Lens)", + "name": "Dark", "type": "dark", "description": "Original Lens dark theme", "author": "Mirantis", diff --git a/src/renderer/themes/lens-light.json b/src/renderer/themes/lens-light.json index 8525d31a56..7891f53321 100644 --- a/src/renderer/themes/lens-light.json +++ b/src/renderer/themes/lens-light.json @@ -1,5 +1,5 @@ { - "name": "Light (Lens)", + "name": "Light", "type": "light", "description": "Original Lens light theme", "author": "Mirantis", @@ -76,26 +76,26 @@ "logsBackground": "#24292e", "logsForeground": "#ffffff", "logRowHoverBackground": "#35373a", - "terminalBackground": "#24292e", - "terminalForeground": "#ffffff", - "terminalCursor": "#ffffff", - "terminalCursorAccent": "#000000", - "terminalSelection": "#ffffff77", - "terminalBlack": "#2e3436", - "terminalRed": "#cc0000", - "terminalGreen": "#4e9a06", - "terminalYellow": "#c4a000", - "terminalBlue": "#3465a4", - "terminalMagenta": "#75507b", - "terminalCyan": "#06989a", + "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": "#555753", - "terminalBrightRed": "#ef2929", - "terminalBrightGreen": "#8ae234", - "terminalBrightYellow": "#fce94f", - "terminalBrightBlue": "#729fcf", - "terminalBrightMagenta": "#ad7fa8", - "terminalBrightCyan": "#34e2e2", + "terminalBrightBlack": "#a8a8a8", + "terminalBrightRed": "#ff6259", + "terminalBrightGreen": "#5cdb59", + "terminalBrightYellow": "#f8c000", + "terminalBrightBlue": "#008db6", + "terminalBrightMagenta": "#ee55f8", + "terminalBrightCyan": "#50e8df", "terminalBrightWhite": "#eeeeec", "dialogTextColor": "#87909c", "dialogBackground": "#ffffff",