mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-03-11 13:23:06 +01:00
Terminal theming (#485)
* init * use setStyleVar * backend implementation. scrope level terminal theming. * only invoke this.applyTermTheme for keys that are updated. command runner for global term theme * invoke applyTermTheme for global terminal themes as well * fix nil error * fix issue were theme can't be found * fix issue where selected termtheme is not set as default value * term theme switcher for session * do not force reload after setting css vars * fix issues. screenview terminal theme switcher * remove debugging code * move getTermThemes to util * fix global theme reset * fix workspace theme reset * fix screenview terminal theme reset issue * cleanup * do not apply theme if theme hasn't changed * do not apply theme if theme hasn't changed in workspace view * cleanup * cleanup * force reload terminal * fix inconsistency * fix reset issue * add a mobx reaction so that theming working when switching sessions * workig reset * simplify and cleanup * refactor * working global and session terminal theming * add check * perf improvement * more perf improvements * put reaction componentDidUpdate to make sure ref is already associated to the element * cleanup * fix issue where session theme is overriden by global theme on reload * reduce flickering on reload * more on reducing flickering on reload * cleanup * more cleanup * fix file not found when no global theme is set * screen level terminal theming * update comment * re-render terminal in history view. cleanup. * cleanup * merge main
This commit is contained in:
parent
f41ac1d5e3
commit
ca5117cda0
@ -9,6 +9,7 @@ import { If } from "tsx-control-statements/components";
|
||||
import { GlobalModel, GlobalCommandRunner, RemotesModel, getApi } from "@/models";
|
||||
import { Toggle, InlineSettingsTextEdit, SettingsError, Dropdown } from "@/common/elements";
|
||||
import { commandRtnHandler, isBlank } from "@/util/util";
|
||||
import { getTermThemes } from "@/util/themeutil";
|
||||
import * as appconst from "@/app/appconst";
|
||||
|
||||
import "./clientsettings.less";
|
||||
@ -73,6 +74,19 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
|
||||
commandRtnHandler(prtn, this.errorMessage);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleChangeTermTheme(theme: string): void {
|
||||
// For global terminal theme, the key is global, otherwise it's either
|
||||
// sessionId or screenId.
|
||||
const currTheme = GlobalModel.getTermTheme()["global"];
|
||||
if (currTheme == theme) {
|
||||
return;
|
||||
}
|
||||
|
||||
const prtn = GlobalCommandRunner.setGlobalTermTheme(theme, false);
|
||||
commandRtnHandler(prtn, this.errorMessage);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleChangeTelemetry(val: boolean): void {
|
||||
let prtn: Promise<CommandRtnType> = null;
|
||||
@ -193,6 +207,8 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
|
||||
const curFontSize = GlobalModel.getTermFontSize();
|
||||
const curFontFamily = GlobalModel.getTermFontFamily();
|
||||
const curTheme = GlobalModel.getThemeSource();
|
||||
const termThemes = getTermThemes(GlobalModel.termThemes, "Wave Default");
|
||||
const currTermTheme = GlobalModel.getTermTheme()["global"] ?? termThemes[0].label;
|
||||
|
||||
return (
|
||||
<MainView className="clientsettings-view" title="Client Settings" onClose={this.handleClose}>
|
||||
@ -233,6 +249,19 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<If condition={termThemes.length > 0}>
|
||||
<div className="settings-field">
|
||||
<div className="settings-label">Terminal Theme</div>
|
||||
<div className="settings-input">
|
||||
<Dropdown
|
||||
className="terminal-theme-dropdown"
|
||||
options={termThemes}
|
||||
defaultValue={currTermTheme}
|
||||
onChange={this.handleChangeTermTheme}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</If>
|
||||
<div className="settings-field">
|
||||
<div className="settings-label">Client ID</div>
|
||||
<div className="settings-input">{cdata.clientid}</div>
|
||||
|
@ -11,8 +11,9 @@ import { GlobalModel, GlobalCommandRunner, Screen } from "@/models";
|
||||
import { SettingsError, Modal, Dropdown, Tooltip } from "@/elements";
|
||||
import * as util from "@/util/util";
|
||||
import { Button } from "@/elements";
|
||||
import { ReactComponent as GlobeIcon } from "@/assets/icons/globe.svg";
|
||||
import { ReactComponent as StatusCircleIcon } from "@/assets/icons/statuscircle.svg";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
import { commandRtnHandler } from "@/util/util";
|
||||
import { getTermThemes } from "@/util/themeutil";
|
||||
import {
|
||||
TabColorSelector,
|
||||
TabIconSelector,
|
||||
@ -153,11 +154,34 @@ class ScreenSettingsModal extends React.Component<{}, {}> {
|
||||
});
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleChangeTermTheme(theme: string): void {
|
||||
const currTheme = GlobalModel.getTermTheme()[this.screenId];
|
||||
if (currTheme == theme) {
|
||||
return;
|
||||
}
|
||||
const prtn = GlobalCommandRunner.setScreenTermTheme(this.screenId, theme, false);
|
||||
commandRtnHandler(prtn, this.errorMessage);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
selectRemote(cname: string): void {
|
||||
let prtn = GlobalCommandRunner.screenSetRemote(cname, true, false);
|
||||
util.commandRtnHandler(prtn, this.errorMessage);
|
||||
}
|
||||
|
||||
render() {
|
||||
const screen = this.screen;
|
||||
if (screen == null) {
|
||||
return null;
|
||||
}
|
||||
let color: string = null;
|
||||
let icon: string = null;
|
||||
let index: number = 0;
|
||||
const curRemote = GlobalModel.getRemote(GlobalModel.getActiveScreen().getCurRemoteInstance().remoteid);
|
||||
const termThemes = getTermThemes(GlobalModel.termThemes);
|
||||
const currTermTheme = GlobalModel.getTermTheme()[this.screenId] ?? termThemes[0].label;
|
||||
|
||||
return (
|
||||
<Modal className="screen-settings-modal">
|
||||
<Modal.Header onClose={this.closeModal} title={`Tab Settings (${screen.name.get()})`} />
|
||||
@ -186,6 +210,19 @@ class ScreenSettingsModal extends React.Component<{}, {}> {
|
||||
<TabIconSelector screen={screen} errorMessage={this.errorMessage} />
|
||||
</div>
|
||||
</div>
|
||||
<If condition={termThemes.length > 0}>
|
||||
<div className="settings-field">
|
||||
<div className="settings-label">Terminal Theme</div>
|
||||
<div className="settings-input">
|
||||
<Dropdown
|
||||
className="terminal-theme-dropdown"
|
||||
options={termThemes}
|
||||
defaultValue={currTermTheme}
|
||||
onChange={this.handleChangeTermTheme}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</If>
|
||||
<div className="settings-field">
|
||||
<div className="settings-label actions-label">
|
||||
<div>Actions</div>
|
||||
|
@ -6,8 +6,10 @@ import * as mobxReact from "mobx-react";
|
||||
import * as mobx from "mobx";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { GlobalModel, GlobalCommandRunner, Session } from "@/models";
|
||||
import { Button } from "@/elements";
|
||||
import { Toggle, InlineSettingsTextEdit, SettingsError, Modal, Tooltip } from "@/elements";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
import { Toggle, InlineSettingsTextEdit, SettingsError, Modal, Tooltip, Button, Dropdown } from "@/elements";
|
||||
import { commandRtnHandler } from "@/util/util";
|
||||
import { getTermThemes } from "@/util/themeutil";
|
||||
import * as util from "@/util/util";
|
||||
|
||||
import "./sessionsettings.less";
|
||||
@ -76,6 +78,16 @@ class SessionSettingsModal extends React.Component<{}, {}> {
|
||||
});
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleChangeTermTheme(theme: string): void {
|
||||
const currTheme = GlobalModel.getTermTheme()[this.sessionId];
|
||||
if (currTheme == theme) {
|
||||
return;
|
||||
}
|
||||
const prtn = GlobalCommandRunner.setSessionTermTheme(this.sessionId, theme, false);
|
||||
commandRtnHandler(prtn, this.errorMessage);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
dismissError(): void {
|
||||
mobx.action(() => {
|
||||
@ -87,6 +99,9 @@ class SessionSettingsModal extends React.Component<{}, {}> {
|
||||
if (this.session == null) {
|
||||
return null;
|
||||
}
|
||||
const termThemes = getTermThemes(GlobalModel.termThemes);
|
||||
const currTermTheme = GlobalModel.getTermTheme()[this.sessionId] ?? termThemes[0].label;
|
||||
|
||||
return (
|
||||
<Modal className="session-settings-modal">
|
||||
<Modal.Header onClose={this.closeModal} title={`Workspace Settings (${this.session.name.get()})`} />
|
||||
@ -104,6 +119,19 @@ class SessionSettingsModal extends React.Component<{}, {}> {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<If condition={termThemes.length > 0}>
|
||||
<div className="settings-field">
|
||||
<div className="settings-label">Terminal Theme</div>
|
||||
<div className="settings-input">
|
||||
<Dropdown
|
||||
className="terminal-theme-dropdown"
|
||||
options={termThemes}
|
||||
defaultValue={currTermTheme}
|
||||
onChange={this.handleChangeTermTheme}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</If>
|
||||
<div className="settings-field">
|
||||
<div className="settings-label">
|
||||
<div>Archived</div>
|
||||
|
@ -308,7 +308,6 @@ class HistoryView extends React.Component<{}, {}> {
|
||||
hvm.setFromDate(null);
|
||||
return;
|
||||
}
|
||||
console.log;
|
||||
hvm.setFromDate(newDate);
|
||||
}
|
||||
|
||||
@ -670,6 +669,11 @@ class LineContainer extends React.Component<{ historyId: string; width: number }
|
||||
this.line = hvm.getLineById(this.historyItem.lineid);
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
GlobalModel.bumpTermRenderVersion();
|
||||
GlobalModel.termThemeSrcEl.set(null);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleHeightChange(lineNum: number, newHeight: number, oldHeight: number): void {
|
||||
return;
|
||||
@ -710,8 +714,9 @@ class LineContainer extends React.Component<{ historyId: string; width: number }
|
||||
ssStr = sprintf("#%s[%s]", session.name.get(), screen.name.get());
|
||||
canViewInContext = true;
|
||||
}
|
||||
const termRenderVersion = GlobalModel.termRenderVersion.get();
|
||||
return (
|
||||
<div className="line-container">
|
||||
<div className="line-container" key={termRenderVersion}>
|
||||
<If condition={canViewInContext}>
|
||||
<div className="line-context">
|
||||
<div title="View in Context" className="vic-btn" onClick={this.viewInContext}>
|
||||
|
@ -15,6 +15,7 @@ import * as util from "@/util/util";
|
||||
import * as lineutil from "./lineutil";
|
||||
|
||||
import "./lines.less";
|
||||
import { GlobalModel } from "@/models";
|
||||
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
|
@ -21,11 +21,16 @@ class ScreenTab extends React.Component<
|
||||
tabRef = React.createRef<HTMLUListElement>();
|
||||
dragEndTimeout = null;
|
||||
scrollIntoViewTimeout = null;
|
||||
theme: string;
|
||||
themeReactionDisposer: mobx.IReactionDisposer;
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.scrollIntoViewTimeout) {
|
||||
clearTimeout(this.dragEndTimeout);
|
||||
}
|
||||
if (this.themeReactionDisposer) {
|
||||
this.themeReactionDisposer();
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
|
@ -14,14 +14,18 @@ import { ScreenView } from "./screen/screenview";
|
||||
import { ScreenTabs } from "./screen/tabs";
|
||||
import { ErrorBoundary } from "@/common/error/errorboundary";
|
||||
import * as textmeasure from "@/util/textmeasure";
|
||||
import "./workspace.less";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import type { Screen } from "@/models";
|
||||
import { Button } from "@/elements";
|
||||
import { getRemoteStr, getRemoteStrWithAlias } from "@/common/prompt/prompt";
|
||||
import { commandRtnHandler } from "@/util/util";
|
||||
import { getTermThemes } from "@/util/themeutil";
|
||||
import { Dropdown } from "@/elements/dropdown";
|
||||
import { getRemoteStrWithAlias } from "@/common/prompt/prompt";
|
||||
import { TabColorSelector, TabIconSelector, TabNameTextField, TabRemoteSelector } from "./screen/newtabsettings";
|
||||
import * as util from "@/util/util";
|
||||
|
||||
import "./workspace.less";
|
||||
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
const ScreenDeleteMessage = `
|
||||
@ -135,9 +139,22 @@ class TabSettings extends React.Component<{ screen: Screen }, {}> {
|
||||
});
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleChangeTermTheme(theme: string): void {
|
||||
const { screenId } = this.props.screen;
|
||||
const currTheme = GlobalModel.getTermTheme()[screenId];
|
||||
if (currTheme == theme) {
|
||||
return;
|
||||
}
|
||||
const prtn = GlobalCommandRunner.setScreenTermTheme(screenId, theme, false);
|
||||
commandRtnHandler(prtn, this.errorMessage);
|
||||
}
|
||||
|
||||
render() {
|
||||
let { screen } = this.props;
|
||||
let rptr = screen.curRemote.get();
|
||||
const termThemes = getTermThemes(GlobalModel.termThemes);
|
||||
const currTermTheme = GlobalModel.getTermTheme()[screen.screenId] ?? termThemes[0].label;
|
||||
return (
|
||||
<div className="newtab-container">
|
||||
<div className="newtab-section name-section">
|
||||
@ -156,6 +173,17 @@ class TabSettings extends React.Component<{ screen: Screen }, {}> {
|
||||
</div>
|
||||
</div>
|
||||
<div className="newtab-spacer" />
|
||||
<If condition={termThemes.length > 0}>
|
||||
<div className="newtab-section">
|
||||
<Dropdown
|
||||
className="terminal-theme-dropdown"
|
||||
options={termThemes}
|
||||
defaultValue={currTermTheme}
|
||||
onChange={this.handleChangeTermTheme}
|
||||
/>
|
||||
</div>
|
||||
</If>
|
||||
<div className="newtab-spacer" />
|
||||
<div className="newtab-section">
|
||||
<TabIconSelector screen={screen} errorMessage={this.errorMessage} />
|
||||
</div>
|
||||
@ -180,6 +208,61 @@ class TabSettings extends React.Component<{ screen: Screen }, {}> {
|
||||
|
||||
@mobxReact.observer
|
||||
class WorkspaceView extends React.Component<{}, {}> {
|
||||
sessionRef = React.createRef<HTMLDivElement>();
|
||||
theme: string;
|
||||
themeReactionDisposer: mobx.IReactionDisposer;
|
||||
|
||||
componentDidMount() {
|
||||
this.setupThemeReaction();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.setupThemeReaction();
|
||||
}
|
||||
|
||||
setupThemeReaction() {
|
||||
if (this.themeReactionDisposer) {
|
||||
this.themeReactionDisposer();
|
||||
}
|
||||
|
||||
// This handles session and screen-level terminal theming.
|
||||
// Ideally, screen-level theming should be handled in the inner-level component, but
|
||||
// the frequent mounting and unmounting of the screen view make it really difficult to work.
|
||||
this.themeReactionDisposer = mobx.reaction(
|
||||
() => {
|
||||
return {
|
||||
termTheme: GlobalModel.getTermTheme(),
|
||||
session: GlobalModel.getActiveSession(),
|
||||
screen: GlobalModel.getActiveScreen(),
|
||||
};
|
||||
},
|
||||
({ termTheme, session, screen }) => {
|
||||
let currTheme = termTheme[session.sessionId];
|
||||
if (termTheme[screen.screenId]) {
|
||||
currTheme = termTheme[screen.screenId];
|
||||
}
|
||||
if (session && currTheme !== this.theme && this.sessionRef.current) {
|
||||
const reset = currTheme == null;
|
||||
const theme = currTheme ?? this.theme;
|
||||
const themeSrcEl = reset ? null : this.sessionRef.current;
|
||||
const rtn = GlobalModel.updateTermTheme(this.sessionRef.current, theme, reset);
|
||||
rtn.then(() => {
|
||||
GlobalModel.termThemeSrcEl.set(themeSrcEl);
|
||||
}).then(() => {
|
||||
GlobalModel.bumpTermRenderVersion();
|
||||
});
|
||||
this.theme = currTheme;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.themeReactionDisposer) {
|
||||
this.themeReactionDisposer();
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
toggleTabSettings() {
|
||||
mobx.action(() => {
|
||||
@ -200,11 +283,14 @@ class WorkspaceView extends React.Component<{}, {}> {
|
||||
if (cmdInputHeight == 0) {
|
||||
cmdInputHeight = textmeasure.baseCmdInputHeight(GlobalModel.lineHeightEnv); // this is the base size of cmdInput (measured using devtools)
|
||||
}
|
||||
|
||||
const isHidden = GlobalModel.activeMainView.get() != "session";
|
||||
const mainSidebarModel = GlobalModel.mainSidebarModel;
|
||||
const termRenderVersion = GlobalModel.termRenderVersion.get();
|
||||
const showTabSettings = GlobalModel.tabSettingsOpen.get();
|
||||
return (
|
||||
<div
|
||||
ref={this.sessionRef}
|
||||
className={cn("mainview", "session-view", { "is-hidden": isHidden })}
|
||||
data-sessionid={sessionId}
|
||||
style={{
|
||||
@ -227,7 +313,11 @@ class WorkspaceView extends React.Component<{}, {}> {
|
||||
</div>
|
||||
</If>
|
||||
<ErrorBoundary key="eb">
|
||||
<ScreenView key={"screenview-" + sessionId} session={session} screen={activeScreen} />
|
||||
<ScreenView
|
||||
key={`screenview-${sessionId}-${termRenderVersion}`}
|
||||
session={session}
|
||||
screen={activeScreen}
|
||||
/>
|
||||
<div className="cmdinput-height-placeholder" style={{ height: cmdInputHeight }}></div>
|
||||
<If condition={activeScreen != null}>
|
||||
<CmdInput key={"cmdinput-" + sessionId} />
|
||||
|
@ -377,6 +377,33 @@ class CommandRunner {
|
||||
return GlobalModel.submitCommand("client", "set", null, kwargs, interactive);
|
||||
}
|
||||
|
||||
setGlobalTermTheme(theme: string, interactive: boolean): Promise<CommandRtnType> {
|
||||
let kwargs = {
|
||||
nohist: "1",
|
||||
termtheme: theme,
|
||||
};
|
||||
return GlobalModel.submitCommand("client", "set", null, kwargs, interactive);
|
||||
}
|
||||
|
||||
setSessionTermTheme(sessionId: string, name: string, interactive: boolean): Promise<CommandRtnType> {
|
||||
console.log("setSessionTermTheme-------");
|
||||
let kwargs = {
|
||||
nohist: "1",
|
||||
id: sessionId,
|
||||
name: name,
|
||||
};
|
||||
return GlobalModel.submitCommand("session", "termtheme", null, kwargs, interactive);
|
||||
}
|
||||
|
||||
setScreenTermTheme(screenId: string, name: string, interactive: boolean): Promise<CommandRtnType> {
|
||||
let kwargs = {
|
||||
nohist: "1",
|
||||
id: screenId,
|
||||
name: name,
|
||||
};
|
||||
return GlobalModel.submitCommand("screen", "termtheme", null, kwargs, interactive);
|
||||
}
|
||||
|
||||
setClientOpenAISettings(opts: {
|
||||
model?: string;
|
||||
apitoken?: string;
|
||||
|
@ -139,6 +139,18 @@ class Model {
|
||||
name: "appUpdateStatus",
|
||||
});
|
||||
|
||||
termThemes: OMap<string, string> = mobx.observable.array([], {
|
||||
name: "terminalThemes",
|
||||
deep: false,
|
||||
});
|
||||
termThemeSrcEl: OV<HTMLElement> = mobx.observable.box(null, {
|
||||
name: "termThemeSrcEl",
|
||||
});
|
||||
termRenderVersion: OV<number> = mobx.observable.box(0, {
|
||||
name: "termRenderVersion",
|
||||
});
|
||||
currGlobalTermTheme: string;
|
||||
|
||||
private constructor() {
|
||||
this.clientId = getApi().getId();
|
||||
this.isDev = getApi().getIsDev();
|
||||
@ -151,6 +163,7 @@ class Model {
|
||||
this.ws.reconnect();
|
||||
this.keybindManager = new KeybindManager(this);
|
||||
this.readConfigKeybindings();
|
||||
this.fetchTerminalThemes();
|
||||
this.initSystemKeybindings();
|
||||
this.initAppKeybindings();
|
||||
this.inputModel = new InputModel(this);
|
||||
@ -215,6 +228,48 @@ class Model {
|
||||
});
|
||||
}
|
||||
|
||||
fetchTerminalThemes() {
|
||||
const url = new URL(this.getBaseHostPort() + "/config/terminal-themes");
|
||||
fetch(url, { method: "get", body: null, headers: this.getFetchHeaders() })
|
||||
.then((resp) => {
|
||||
if (resp.status == 404) {
|
||||
return [];
|
||||
} else if (!resp.ok) {
|
||||
util.handleNotOkResp(resp, url);
|
||||
}
|
||||
return resp.json();
|
||||
})
|
||||
.then((themes) => {
|
||||
const tt = themes.map((theme) => theme.name.split(".")[0]);
|
||||
this.termThemes.replace(tt);
|
||||
});
|
||||
}
|
||||
|
||||
updateTermTheme(element: HTMLElement, themeFileName: string, reset: boolean) {
|
||||
const url = new URL(this.getBaseHostPort() + `/config/terminal-themes/${themeFileName}.json`);
|
||||
return fetch(url, { method: "get", body: null, headers: this.getFetchHeaders() })
|
||||
.then((resp) => resp.json())
|
||||
.then((themeVars: TermThemeType) => {
|
||||
Object.keys(themeVars).forEach((key) => {
|
||||
if (reset) {
|
||||
this.resetStyleVar(element, `--term-${key}`);
|
||||
} else {
|
||||
this.resetStyleVar(element, `--term-${key}`);
|
||||
this.setStyleVar(element, `--term-${key}`, themeVars[key]);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`error applying theme: ${themeFileName}`, error);
|
||||
});
|
||||
}
|
||||
|
||||
bumpTermRenderVersion() {
|
||||
mobx.action(() => {
|
||||
this.termRenderVersion.set(this.termRenderVersion.get() + 1);
|
||||
})();
|
||||
}
|
||||
|
||||
initSystemKeybindings() {
|
||||
this.keybindManager.registerKeybinding("system", "electron", "system:toggleDeveloperTools", (waveEvent) => {
|
||||
getApi().toggleDeveloperTools();
|
||||
@ -417,6 +472,14 @@ class Model {
|
||||
}
|
||||
}
|
||||
|
||||
getTermTheme(): TermThemeType {
|
||||
let cdata = this.clientData.get();
|
||||
if (cdata?.feopts?.termtheme) {
|
||||
return mobx.toJS(cdata.feopts.termtheme);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
getTermFontSize(): number {
|
||||
return this.termFontSize.get();
|
||||
}
|
||||
@ -425,11 +488,11 @@ class Model {
|
||||
let lhe = this.recomputeLineHeightEnv();
|
||||
mobx.action(() => {
|
||||
this.bumpRenderVersion();
|
||||
this.setStyleVar("--termfontsize", lhe.fontSize + "px");
|
||||
this.setStyleVar("--termlineheight", lhe.lineHeight + "px");
|
||||
this.setStyleVar("--termpad", lhe.pad + "px");
|
||||
this.setStyleVar("--termfontsize-sm", lhe.fontSizeSm + "px");
|
||||
this.setStyleVar("--termlineheight-sm", lhe.lineHeightSm + "px");
|
||||
this.setStyleVar(document.documentElement, "--termfontsize", lhe.fontSize + "px");
|
||||
this.setStyleVar(document.documentElement, "--termlineheight", lhe.lineHeight + "px");
|
||||
this.setStyleVar(document.documentElement, "--termpad", lhe.pad + "px");
|
||||
this.setStyleVar(document.documentElement, "--termfontsize-sm", lhe.fontSizeSm + "px");
|
||||
this.setStyleVar(document.documentElement, "--termlineheight-sm", lhe.lineHeightSm + "px");
|
||||
})();
|
||||
}
|
||||
|
||||
@ -448,8 +511,12 @@ class Model {
|
||||
return this.lineHeightEnv;
|
||||
}
|
||||
|
||||
setStyleVar(name: string, value: string) {
|
||||
document.documentElement.style.setProperty(name, value);
|
||||
setStyleVar(element: HTMLElement, name: string, value: string): void {
|
||||
element.style.setProperty(name, value);
|
||||
}
|
||||
|
||||
resetStyleVar(element: HTMLElement, name: string): void {
|
||||
element.style.removeProperty(name);
|
||||
}
|
||||
|
||||
getBaseWsHostPort(): string {
|
||||
@ -1225,6 +1292,9 @@ class Model {
|
||||
newTheme = appconst.DefaultTheme;
|
||||
}
|
||||
const themeUpdated = newTheme != this.getThemeSource();
|
||||
const oldTermTheme = this.getTermTheme();
|
||||
const newTermTheme = clientData?.feopts?.termtheme;
|
||||
const ttUpdated = this.termThemeUpdated(newTermTheme, oldTermTheme);
|
||||
mobx.action(() => {
|
||||
this.clientData.set(clientData);
|
||||
})();
|
||||
@ -1245,6 +1315,36 @@ class Model {
|
||||
getApi().setNativeThemeSource(newTheme);
|
||||
this.bumpRenderVersion();
|
||||
}
|
||||
// Only for global terminal theme. For session and screen terminal theme,
|
||||
// they are handled in workspace view.
|
||||
if (newTermTheme) {
|
||||
const el = document.documentElement;
|
||||
const globaltt = newTermTheme["global"] ?? this.currGlobalTermTheme;
|
||||
const reset = newTermTheme["global"] == null;
|
||||
if (globaltt) {
|
||||
const rtn = this.updateTermTheme(el, globaltt, reset);
|
||||
rtn.then(() => {
|
||||
if (ttUpdated) {
|
||||
this.bumpTermRenderVersion();
|
||||
}
|
||||
});
|
||||
this.currGlobalTermTheme = globaltt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
termThemeUpdated(newTermTheme, oldTermTheme) {
|
||||
for (const key in oldTermTheme) {
|
||||
if (!(key in newTermTheme)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (const key in newTermTheme) {
|
||||
if (!oldTermTheme[key] || oldTermTheme[key] !== newTermTheme[key]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
submitCommandPacket(cmdPk: FeCmdPacketType, interactive: boolean): Promise<CommandRtnType> {
|
||||
|
@ -521,6 +521,7 @@ class Screen {
|
||||
lineId: line.lineid,
|
||||
lineNum: line.linenum,
|
||||
};
|
||||
// console.log("globalmodel)))))))))))))", this.globalModel.termThemeSrcEl.get());
|
||||
termWrap = new TermWrap(elem, {
|
||||
termContext: termContext,
|
||||
usedRows: usedRows,
|
||||
|
@ -51,9 +51,10 @@ type TermWrapOpts = {
|
||||
onUpdateContentHeight: (termContext: RendererContext, height: number) => void;
|
||||
};
|
||||
|
||||
function getThemeFromCSSVars(): ITheme {
|
||||
let theme: ITheme = {};
|
||||
let rootStyle = getComputedStyle(document.documentElement);
|
||||
function getThemeFromCSSVars(themeSrcEl: HTMLElement): ITheme {
|
||||
const theme: ITheme = {};
|
||||
const tse = themeSrcEl ?? document.documentElement;
|
||||
let rootStyle = getComputedStyle(tse);
|
||||
theme.foreground = rootStyle.getPropertyValue("--term-foreground");
|
||||
theme.background = rootStyle.getPropertyValue("--term-background");
|
||||
theme.black = rootStyle.getPropertyValue("--term-black");
|
||||
@ -130,7 +131,8 @@ class TermWrap {
|
||||
let cols = windowWidthToCols(opts.winSize.width, opts.fontSize);
|
||||
this.termSize = { rows: opts.termOpts.rows, cols: cols };
|
||||
}
|
||||
let theme = getThemeFromCSSVars();
|
||||
const themeSrcEl = GlobalModel.termThemeSrcEl.get();
|
||||
let theme = getThemeFromCSSVars(themeSrcEl);
|
||||
this.terminal = new Terminal({
|
||||
rows: this.termSize.rows,
|
||||
cols: this.termSize.cols,
|
||||
|
@ -72,18 +72,15 @@ class TerminalKeybindings extends React.Component<{ termWrap: any; lineid: strin
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class TerminalRenderer extends React.Component<
|
||||
{
|
||||
screen: LineContainerType;
|
||||
line: LineType;
|
||||
width: number;
|
||||
staticRender: boolean;
|
||||
visible: OV<boolean>;
|
||||
onHeightChange: () => void;
|
||||
collapsed: boolean;
|
||||
},
|
||||
{}
|
||||
> {
|
||||
class TerminalRenderer extends React.Component<{
|
||||
screen: LineContainerType;
|
||||
line: LineType;
|
||||
width: number;
|
||||
staticRender: boolean;
|
||||
visible: OV<boolean>;
|
||||
onHeightChange: () => void;
|
||||
collapsed: boolean;
|
||||
}> {
|
||||
termLoaded: mobx.IObservableValue<boolean> = mobx.observable.box(false, {
|
||||
name: "linecmd-term-loaded",
|
||||
});
|
||||
|
5
src/types/custom.d.ts
vendored
5
src/types/custom.d.ts
vendored
@ -563,10 +563,15 @@ declare global {
|
||||
data: Uint8Array;
|
||||
};
|
||||
|
||||
type TermThemeType = {
|
||||
[k: string]: string | null;
|
||||
};
|
||||
|
||||
type FeOptsType = {
|
||||
termfontsize: number;
|
||||
termfontfamily: string;
|
||||
theme: NativeThemeSource;
|
||||
termtheme: TermThemeType;
|
||||
};
|
||||
|
||||
type ConfirmFlagsType = {
|
||||
|
16
src/util/themeutil.ts
Normal file
16
src/util/themeutil.ts
Normal file
@ -0,0 +1,16 @@
|
||||
function getTermThemes(termThemes: string[], noneLabel = "Inherit"): DropdownItem[] {
|
||||
const tt: DropdownItem[] = [];
|
||||
tt.push({
|
||||
label: noneLabel,
|
||||
value: null,
|
||||
});
|
||||
for (const themeName of termThemes) {
|
||||
tt.push({
|
||||
label: themeName,
|
||||
value: themeName,
|
||||
});
|
||||
}
|
||||
return tt;
|
||||
}
|
||||
|
||||
export { getTermThemes };
|
@ -123,8 +123,8 @@ var SetVarNameMap map[string]string = map[string]string{
|
||||
var SetVarScopes = []SetVarScope{
|
||||
{ScopeName: "global", VarNames: []string{}},
|
||||
{ScopeName: "client", VarNames: []string{"telemetry"}},
|
||||
{ScopeName: "session", VarNames: []string{"name", "pos"}},
|
||||
{ScopeName: "screen", VarNames: []string{"name", "tabcolor", "tabicon", "pos", "pterm", "anchor", "focus", "line", "index"}},
|
||||
{ScopeName: "session", VarNames: []string{"name", "pos", "theme"}},
|
||||
{ScopeName: "screen", VarNames: []string{"name", "tabcolor", "tabicon", "pos", "pterm", "anchor", "focus", "line", "index", "theme"}},
|
||||
{ScopeName: "line", VarNames: []string{}},
|
||||
// connection = remote, remote = remoteinstance
|
||||
{ScopeName: "connection", VarNames: []string{"alias", "connectmode", "key", "password", "autoinstall", "color"}},
|
||||
@ -190,6 +190,7 @@ func init() {
|
||||
registerCmdFn("session:showall", SessionShowAllCommand)
|
||||
registerCmdFn("session:show", SessionShowCommand)
|
||||
registerCmdFn("session:openshared", SessionOpenSharedCommand)
|
||||
registerCmdFn("session:termtheme", TermSetThemeCommand)
|
||||
registerCmdFn("session:ensureone", SessionEnsureOneCommand)
|
||||
|
||||
registerCmdFn("screen", ScreenCommand)
|
||||
@ -203,6 +204,7 @@ func init() {
|
||||
registerCmdFn("screen:webshare", ScreenWebShareCommand)
|
||||
registerCmdFn("screen:reorder", ScreenReorderCommand)
|
||||
registerCmdFn("screen:show", ScreenShowCommand)
|
||||
registerCmdFn("screen:termtheme", TermSetThemeCommand)
|
||||
|
||||
registerCmdAlias("remote", RemoteCommand)
|
||||
registerCmdFn("remote:show", RemoteShowCommand)
|
||||
@ -3604,6 +3606,38 @@ func ScreenShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s
|
||||
return update, nil
|
||||
}
|
||||
|
||||
func TermSetThemeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) {
|
||||
clientData, err := sstore.EnsureClientData(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot retrieve client data: %v", err)
|
||||
}
|
||||
id, ok := pk.Kwargs["id"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("id key not provided")
|
||||
}
|
||||
themeName, themeNameOk := pk.Kwargs["name"]
|
||||
feOpts := clientData.FeOpts
|
||||
if feOpts.TermTheme == nil {
|
||||
feOpts.TermTheme = make(map[string]string)
|
||||
}
|
||||
if themeNameOk && themeName != "" {
|
||||
feOpts.TermTheme[id] = themeName
|
||||
} else {
|
||||
delete(feOpts.TermTheme, id)
|
||||
}
|
||||
err = sstore.UpdateClientFeOpts(ctx, feOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error updating client feopts: %v", err)
|
||||
}
|
||||
clientData, err = sstore.EnsureClientData(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot retrieve updated client data: %v", err)
|
||||
}
|
||||
update := scbus.MakeUpdatePacket()
|
||||
update.AddUpdate(*clientData)
|
||||
return update, nil
|
||||
}
|
||||
|
||||
func SessionShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) {
|
||||
ids, err := resolveUiIds(ctx, pk, R_Session)
|
||||
if err != nil {
|
||||
@ -5762,6 +5796,22 @@ func ClientSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sc
|
||||
}
|
||||
varsUpdated = append(varsUpdated, "theme")
|
||||
}
|
||||
if termthemeStr, found := pk.Kwargs["termtheme"]; found {
|
||||
feOpts := clientData.FeOpts
|
||||
if feOpts.TermTheme == nil {
|
||||
feOpts.TermTheme = make(map[string]string)
|
||||
}
|
||||
if termthemeStr == "" {
|
||||
delete(feOpts.TermTheme, "global")
|
||||
} else {
|
||||
feOpts.TermTheme["global"] = termthemeStr
|
||||
}
|
||||
err = sstore.UpdateClientFeOpts(ctx, feOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error updating client feopts: %v", err)
|
||||
}
|
||||
varsUpdated = append(varsUpdated, "termtheme")
|
||||
}
|
||||
if apiToken, found := pk.Kwargs["openaiapitoken"]; found {
|
||||
err = validateOpenAIAPIToken(apiToken)
|
||||
if err != nil {
|
||||
|
@ -250,9 +250,10 @@ type ClientOptsType struct {
|
||||
}
|
||||
|
||||
type FeOptsType struct {
|
||||
TermFontSize int `json:"termfontsize,omitempty"`
|
||||
TermFontFamily string `json:"termfontfamily,omitempty"`
|
||||
Theme string `json:"theme,omitempty"`
|
||||
TermFontSize int `json:"termfontsize,omitempty"`
|
||||
TermFontFamily string `json:"termfontfamily,omitempty"`
|
||||
Theme string `json:"theme,omitempty"`
|
||||
TermTheme map[string]string `json:"termtheme"`
|
||||
}
|
||||
|
||||
type ReleaseInfoType struct {
|
||||
|
Loading…
Reference in New Issue
Block a user