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:
Red J Adaya 2024-04-02 14:41:24 +08:00 committed by GitHub
parent f41ac1d5e3
commit ca5117cda0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 431 additions and 37 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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