diff --git a/src/app/clientsettings/clientsettings.tsx b/src/app/clientsettings/clientsettings.tsx index d70f2848..92b32825 100644 --- a/src/app/clientsettings/clientsettings.tsx +++ b/src/app/clientsettings/clientsettings.tsx @@ -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 = 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 ( @@ -233,6 +249,19 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove /> + 0}> +
+
Terminal Theme
+
+ +
+
+
Client ID
{cdata.clientid}
diff --git a/src/app/common/modals/screensettings.tsx b/src/app/common/modals/screensettings.tsx index c5a05c73..04418b28 100644 --- a/src/app/common/modals/screensettings.tsx +++ b/src/app/common/modals/screensettings.tsx @@ -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 ( @@ -186,6 +210,19 @@ class ScreenSettingsModal extends React.Component<{}, {}> {
+ 0}> +
+
Terminal Theme
+
+ +
+
+
Actions
diff --git a/src/app/common/modals/sessionsettings.tsx b/src/app/common/modals/sessionsettings.tsx index 1f3839a3..a208353a 100644 --- a/src/app/common/modals/sessionsettings.tsx +++ b/src/app/common/modals/sessionsettings.tsx @@ -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 ( @@ -104,6 +119,19 @@ class SessionSettingsModal extends React.Component<{}, {}> { />
+ 0}> +
+
Terminal Theme
+
+ +
+
+
Archived
diff --git a/src/app/history/history.tsx b/src/app/history/history.tsx index 7dba1b62..1d1481d1 100644 --- a/src/app/history/history.tsx +++ b/src/app/history/history.tsx @@ -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 ( -
+
diff --git a/src/app/line/linesview.tsx b/src/app/line/linesview.tsx index a267e98b..9539c592 100644 --- a/src/app/line/linesview.tsx +++ b/src/app/line/linesview.tsx @@ -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); diff --git a/src/app/workspace/screen/tab.tsx b/src/app/workspace/screen/tab.tsx index 84f7c5be..4a6f61cf 100644 --- a/src/app/workspace/screen/tab.tsx +++ b/src/app/workspace/screen/tab.tsx @@ -21,11 +21,16 @@ class ScreenTab extends React.Component< tabRef = React.createRef(); dragEndTimeout = null; scrollIntoViewTimeout = null; + theme: string; + themeReactionDisposer: mobx.IReactionDisposer; componentWillUnmount() { if (this.scrollIntoViewTimeout) { clearTimeout(this.dragEndTimeout); } + if (this.themeReactionDisposer) { + this.themeReactionDisposer(); + } } @boundMethod diff --git a/src/app/workspace/workspaceview.tsx b/src/app/workspace/workspaceview.tsx index 84915def..eeb27201 100644 --- a/src/app/workspace/workspaceview.tsx +++ b/src/app/workspace/workspaceview.tsx @@ -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 (
@@ -156,6 +173,17 @@ class TabSettings extends React.Component<{ screen: Screen }, {}> {
+ 0}> +
+ +
+
+
@@ -180,6 +208,61 @@ class TabSettings extends React.Component<{ screen: Screen }, {}> { @mobxReact.observer class WorkspaceView extends React.Component<{}, {}> { + sessionRef = React.createRef(); + 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 (
{
- +
diff --git a/src/models/commandrunner.ts b/src/models/commandrunner.ts index d2fbdf7c..edd63f00 100644 --- a/src/models/commandrunner.ts +++ b/src/models/commandrunner.ts @@ -377,6 +377,33 @@ class CommandRunner { return GlobalModel.submitCommand("client", "set", null, kwargs, interactive); } + setGlobalTermTheme(theme: string, interactive: boolean): Promise { + let kwargs = { + nohist: "1", + termtheme: theme, + }; + return GlobalModel.submitCommand("client", "set", null, kwargs, interactive); + } + + setSessionTermTheme(sessionId: string, name: string, interactive: boolean): Promise { + 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 { + let kwargs = { + nohist: "1", + id: screenId, + name: name, + }; + return GlobalModel.submitCommand("screen", "termtheme", null, kwargs, interactive); + } + setClientOpenAISettings(opts: { model?: string; apitoken?: string; diff --git a/src/models/model.ts b/src/models/model.ts index 182dc24d..091f4ef7 100644 --- a/src/models/model.ts +++ b/src/models/model.ts @@ -139,6 +139,18 @@ class Model { name: "appUpdateStatus", }); + termThemes: OMap = mobx.observable.array([], { + name: "terminalThemes", + deep: false, + }); + termThemeSrcEl: OV = mobx.observable.box(null, { + name: "termThemeSrcEl", + }); + termRenderVersion: OV = 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 { diff --git a/src/models/screen.ts b/src/models/screen.ts index b1ab5452..3277224c 100644 --- a/src/models/screen.ts +++ b/src/models/screen.ts @@ -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, diff --git a/src/plugins/terminal/term.ts b/src/plugins/terminal/term.ts index 5ad64436..d9862a2a 100644 --- a/src/plugins/terminal/term.ts +++ b/src/plugins/terminal/term.ts @@ -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, diff --git a/src/plugins/terminal/terminal.tsx b/src/plugins/terminal/terminal.tsx index d0c0e087..09deab62 100644 --- a/src/plugins/terminal/terminal.tsx +++ b/src/plugins/terminal/terminal.tsx @@ -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; - onHeightChange: () => void; - collapsed: boolean; - }, - {} -> { +class TerminalRenderer extends React.Component<{ + screen: LineContainerType; + line: LineType; + width: number; + staticRender: boolean; + visible: OV; + onHeightChange: () => void; + collapsed: boolean; +}> { termLoaded: mobx.IObservableValue = mobx.observable.box(false, { name: "linecmd-term-loaded", }); diff --git a/src/types/custom.d.ts b/src/types/custom.d.ts index 9d1ebb56..7ca11a6e 100644 --- a/src/types/custom.d.ts +++ b/src/types/custom.d.ts @@ -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 = { diff --git a/src/util/themeutil.ts b/src/util/themeutil.ts new file mode 100644 index 00000000..ceb93ebe --- /dev/null +++ b/src/util/themeutil.ts @@ -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 }; diff --git a/wavesrv/pkg/cmdrunner/cmdrunner.go b/wavesrv/pkg/cmdrunner/cmdrunner.go index 896049d8..48f4fab9 100644 --- a/wavesrv/pkg/cmdrunner/cmdrunner.go +++ b/wavesrv/pkg/cmdrunner/cmdrunner.go @@ -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 { diff --git a/wavesrv/pkg/sstore/sstore.go b/wavesrv/pkg/sstore/sstore.go index 27771b78..1c8ff9d7 100644 --- a/wavesrv/pkg/sstore/sstore.go +++ b/wavesrv/pkg/sstore/sstore.go @@ -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 {