Support os-native theming (#495)

* save work

* Add native theme support

* update index

* update var name

* remove comment

* fix code setting

* bump render version on change

* remove themeutil
This commit is contained in:
Evan Simkowitz 2024-03-26 19:14:03 -07:00 committed by GitHub
parent 69b9ce33b3
commit 6065ee931f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 173 additions and 156 deletions

View File

@ -5,14 +5,14 @@
<base href="../" />
<script charset="UTF-8" src="dist-dev/waveterm.js"></script>
<link rel="stylesheet" href="public/bulma-0.9.4.min.css" />
<link rel="stylesheet" href="public/fontawesome/css/fontawesome.min.css">
<link rel="stylesheet" href="public/fontawesome/css/brands.min.css">
<link rel="stylesheet" href="public/fontawesome/css/solid.min.css">
<link rel="stylesheet" href="public/fontawesome/css/sharp-solid.min.css">
<link rel="stylesheet" href="public/fontawesome/css/sharp-regular.min.css">
<link rel="stylesheet" href="public/fontawesome/css/fontawesome.min.css" />
<link rel="stylesheet" href="public/fontawesome/css/brands.min.css" />
<link rel="stylesheet" href="public/fontawesome/css/solid.min.css" />
<link rel="stylesheet" href="public/fontawesome/css/sharp-solid.min.css" />
<link rel="stylesheet" href="public/fontawesome/css/sharp-regular.min.css" />
<link rel="stylesheet" href="dist-dev/waveterm.css" />
<link rel="stylesheet" id="theme-stylesheet" href="public/themes/default.css">
<link rel="stylesheet" id="theme-stylesheet" href="public/themes/themequery.css" />
</head>
<body>
<div id="measure"></div>

View File

@ -5,14 +5,14 @@
<base href="../" />
<script charset="UTF-8" src="dist/waveterm.js"></script>
<link rel="stylesheet" href="public/bulma-0.9.4.min.css" />
<link rel="stylesheet" href="public/fontawesome/css/fontawesome.min.css">
<link rel="stylesheet" href="public/fontawesome/css/brands.min.css">
<link rel="stylesheet" href="public/fontawesome/css/solid.min.css">
<link rel="stylesheet" href="public/fontawesome/css/sharp-solid.min.css">
<link rel="stylesheet" href="public/fontawesome/css/sharp-regular.min.css">
<link rel="stylesheet" href="public/fontawesome/css/fontawesome.min.css" />
<link rel="stylesheet" href="public/fontawesome/css/brands.min.css" />
<link rel="stylesheet" href="public/fontawesome/css/solid.min.css" />
<link rel="stylesheet" href="public/fontawesome/css/sharp-solid.min.css" />
<link rel="stylesheet" href="public/fontawesome/css/sharp-regular.min.css" />
<link rel="stylesheet" href="dist/waveterm.css" />
<link rel="stylesheet" id="theme-stylesheet" href="public/themes/default.css" />
<link rel="stylesheet" id="theme-stylesheet" href="public/themes/themequery.css" />
</head>
<body>
<div id="measure"></div>

View File

@ -0,0 +1,3 @@
@import url("./default.css") screen;
@import url("./light.css") screen and (prefers-color-scheme: light);

View File

@ -8,7 +8,7 @@ import { boundMethod } from "autobind-decorator";
import { If } from "tsx-control-statements/components";
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalCommandRunner, GlobalModel } from "@/models";
import { GlobalModel } from "@/models";
import { isBlank } from "@/util/util";
import { WorkspaceView } from "./workspace/workspaceview";
import { PluginsView } from "./pluginsview/pluginsview";
@ -123,7 +123,7 @@ class App extends React.Component<{}, {}> {
const mainSidebarCollapsed = GlobalModel.mainSidebarModel.getCollapsed();
const rightSidebarCollapsed = GlobalModel.rightSidebarModel.getCollapsed();
const activeMainView = GlobalModel.activeMainView.get();
const lightDarkClass = GlobalModel.isThemeDark() ? "is-dark" : "is-light";
const lightDarkClass = GlobalModel.isDarkTheme.get() ? "is-dark" : "is-light";
return (
<div
key={"version-" + renderVersion}

View File

@ -64,11 +64,12 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
}
@boundMethod
handleChangeTheme(theme: string): void {
if (GlobalModel.getTheme() == theme) {
handleChangeThemeSource(themeSource: NativeThemeSource): void {
if (GlobalModel.getThemeSource() == themeSource) {
return;
}
const prtn = GlobalCommandRunner.setTheme(theme, false);
const prtn = GlobalCommandRunner.setTheme(themeSource, false);
getApi().setNativeThemeSource(themeSource);
commandRtnHandler(prtn, this.errorMessage);
}
@ -111,11 +112,12 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
return availableFontFamilies;
}
getThemes(): DropdownItem[] {
const themes: DropdownItem[] = [];
themes.push({ label: "Dark", value: "dark" });
themes.push({ label: "Light", value: "light" });
return themes;
getThemeSources(): DropdownItem[] {
const themeSources: DropdownItem[] = [];
themeSources.push({ label: "Dark", value: "dark" });
themeSources.push({ label: "Light", value: "light" });
themeSources.push({ label: "System", value: "system" });
return themeSources;
}
@boundMethod
@ -190,7 +192,7 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
);
const curFontSize = GlobalModel.getTermFontSize();
const curFontFamily = GlobalModel.getTermFontFamily();
const curTheme = GlobalModel.getTheme();
const curTheme = GlobalModel.getThemeSource();
return (
<MainView className="clientsettings-view" title="Client Settings" onClose={this.handleClose}>
@ -225,9 +227,9 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
<div className="settings-input">
<Dropdown
className="theme-dropdown"
options={this.getThemes()}
options={this.getThemeSources()}
defaultValue={curTheme}
onChange={this.handleChangeTheme}
onChange={this.handleChangeThemeSource}
/>
</div>
</div>

View File

@ -539,6 +539,25 @@ electron.ipcMain.on("get-last-logs", (event, numberOfLines) => {
});
});
electron.ipcMain.on("get-shouldusedarkcolors", (event) => {
event.returnValue = electron.nativeTheme.shouldUseDarkColors;
});
electron.ipcMain.on("get-nativethemesource", (event) => {
event.returnValue = electron.nativeTheme.themeSource;
});
electron.ipcMain.on("set-nativethemesource", (event, themeSource: "system" | "light" | "dark") => {
electron.nativeTheme.themeSource = themeSource;
event.returnValue = true;
});
electron.nativeTheme.on("updated", () => {
if (MainWindow != null) {
MainWindow.webContents.send("nativetheme-updated");
}
});
function readLastLinesOfFile(filePath: string, lineCount: number) {
return new Promise((resolve, reject) => {
child_process.exec(`tail -n ${lineCount} "${filePath}"`, (err, stdout, stderr) => {

View File

@ -13,6 +13,10 @@ contextBridge.exposeInMainWorld("api", {
ipcRenderer.once("last-logs", (event, data) => callback(data));
},
getInitialTermFontFamily: () => ipcRenderer.sendSync("get-initial-termfontfamily"),
getShouldUseDarkColors: () => ipcRenderer.sendSync("get-shouldusedarkcolors"),
getNativeThemeSource: () => ipcRenderer.sendSync("get-nativethemesource"),
setNativeThemeSource: (source) => ipcRenderer.send("set-nativethemesource", source),
onNativeThemeUpdated: (callback) => ipcRenderer.on("nativetheme-updated", callback),
restartWaveSrv: () => ipcRenderer.sendSync("restart-server"),
reloadWindow: () => ipcRenderer.sendSync("reload-window"),
reregisterGlobalShortcut: (shortcut) => ipcRenderer.sendSync("reregister-global-shortcut", shortcut),

View File

@ -365,7 +365,7 @@ class CommandRunner {
return GlobalModel.submitCommand("client", "set", null, kwargs, interactive);
}
setTheme(theme: string, interactive: boolean): Promise<CommandRtnType> {
setTheme(theme: NativeThemeSource, interactive: boolean): Promise<CommandRtnType> {
let kwargs = {
nohist: "1",
theme: theme,

View File

@ -13,12 +13,11 @@ import {
isModKeyPress,
isBlank,
} from "@/util/util";
import { loadTheme } from "@/util/themeutil";
import { WSControl } from "./ws";
import { cmdStatusIsRunning } from "@/app/line/lineutil";
import * as appconst from "@/app/appconst";
import { remotePtrToString, cmdPacketString } from "@/util/modelutil";
import { KeybindManager, checkKeyPressed, adaptFromReactOrNativeKeyEvent, setKeyUtilPlatform } from "@/util/keyutil";
import { KeybindManager, adaptFromReactOrNativeKeyEvent, setKeyUtilPlatform } from "@/util/keyutil";
import { Session } from "./session";
import { ScreenLines } from "./screenlines";
import { InputModel } from "./input";
@ -118,6 +117,9 @@ class Model {
modalsModel: ModalsModel;
mainSidebarModel: MainSidebarModel;
rightSidebarModel: RightSidebarModel;
isDarkTheme: OV<boolean> = mobx.observable.box(getApi().getShouldUseDarkColors(), {
name: "isDarkTheme",
});
clientData: OV<ClientDataType> = mobx.observable.box(null, {
name: "clientData",
});
@ -181,6 +183,7 @@ class Model {
getApi().onMenuItemAbout(this.onMenuItemAbout.bind(this));
getApi().onWaveSrvStatusChange(this.onWaveSrvStatusChange.bind(this));
getApi().onAppUpdateStatus(this.onAppUpdateStatus.bind(this));
getApi().onNativeThemeUpdated(this.onNativeThemeUpdated.bind(this));
document.addEventListener("keydown", this.docKeyDownHandler.bind(this));
document.addEventListener("selectionchange", this.docSelectionChangeHandler.bind(this));
setTimeout(() => this.getClientDataLoop(1), 10);
@ -389,18 +392,19 @@ class Model {
return ff;
}
getTheme(): string {
let cdata = this.clientData.get();
let theme = cdata?.feopts?.theme;
if (theme == null) {
theme = appconst.DefaultTheme;
}
return theme;
getThemeSource(): NativeThemeSource {
return getApi().getNativeThemeSource();
}
isThemeDark(): boolean {
let cdata = this.clientData.get();
return cdata?.feopts?.theme != "light";
onNativeThemeUpdated(): void {
console.log("native theme updated");
const isDark = getApi().getShouldUseDarkColors();
if (isDark != this.isDarkTheme.get()) {
mobx.action(() => {
this.isDarkTheme.set(isDark);
this.bumpRenderVersion();
})();
}
}
getTermFontSize(): number {
@ -1197,7 +1201,7 @@ class Model {
if (newTheme == null) {
newTheme = appconst.DefaultTheme;
}
const themeUpdated = newTheme != this.getTheme();
const themeUpdated = newTheme != this.getThemeSource();
mobx.action(() => {
this.clientData.set(clientData);
})();
@ -1215,7 +1219,7 @@ class Model {
this.updateTermFontSizeVars();
}
if (themeUpdated) {
loadTheme(newTheme);
getApi().setNativeThemeSource(newTheme);
this.bumpRenderVersion();
}
}

View File

@ -7,17 +7,11 @@ import Editor, { Monaco } from "@monaco-editor/react";
import type * as MonacoTypes from "monaco-editor/esm/vs/editor/editor.api";
import cn from "classnames";
import { If } from "tsx-control-statements/components";
import { Markdown } from "@/elements";
import { Markdown, Button } from "@/elements";
import { GlobalModel, GlobalCommandRunner } from "@/models";
import Split from "react-split-it";
import loader from "@monaco-editor/loader";
import {
checkKeyPressed,
adaptFromReactOrNativeKeyEvent,
KeybindManager,
adaptFromElectronKeyEvent,
} from "@/util/keyutil";
import { Button, Dropdown } from "@/elements";
import { adaptFromReactOrNativeKeyEvent } from "@/util/keyutil";
import "./code.less";
@ -99,7 +93,7 @@ class SourceCodeRenderer extends React.Component<
* codeCache is a Hashmap with key=screenId:lineId:filepath and value=code
* Editor should never read the code directly from the filesystem. it should read from the cache.
*/
static codeCache = new Map();
static readonly codeCache = new Map();
// which languages have preview options
languagesWithPreviewer: string[] = ["markdown", "mdx"];
@ -109,7 +103,6 @@ class SourceCodeRenderer extends React.Component<
monacoEditor: MonacoTypes.editor.IStandaloneCodeEditor; // reference to mounted monaco editor. TODO need the correct type
markdownRef: React.RefObject<HTMLDivElement>;
syncing: boolean;
monacoOptions: MonacoTypes.editor.IEditorOptions & MonacoTypes.editor.IGlobalEditorOptions;
constructor(props) {
super(props);
@ -117,7 +110,7 @@ class SourceCodeRenderer extends React.Component<
const editorHeight = Math.max(props.savedHeight - this.getEditorHeightBuffer(), 0); // must subtract the padding/margin to get the real editorHeight
this.markdownRef = React.createRef();
this.syncing = false;
let isClosed = props.lineState["prompt:closed"];
const isClosed = props.lineState["prompt:closed"];
this.state = {
code: null,
languages: [],
@ -161,12 +154,12 @@ class SourceCodeRenderer extends React.Component<
}
}
saveLineState = (kvp) => {
saveLineState(kvp) {
const { screenId, lineId } = this.props.context;
GlobalCommandRunner.setLineState(screenId, lineId, { ...this.props.lineState, ...kvp }, false);
};
}
setInitialLanguage = (editor) => {
setInitialLanguage(editor) {
// set all languages
const languages = monaco.languages.getLanguages().map((lang) => lang.id);
this.setState({ languages });
@ -175,7 +168,7 @@ class SourceCodeRenderer extends React.Component<
// if not found, we try to grab the filename from with filePath (coming from lineState["prompt:file"]) or cmdstr
if (!detectedLanguage) {
const strForFilePath = this.filePath || this.props.cmdstr;
const extension = strForFilePath.match(/(?:[^\\\/:*?"<>|\r\n]+\.)([a-zA-Z0-9]+)\b/)?.[1] || "";
const extension = RegExp(/(?:[^\\/:*?"<>|\r\n]+\.)([a-zA-Z0-9]+)\b/).exec(strForFilePath)?.[1] || "";
const detectedLanguageObj = monaco.languages
.getLanguages()
.find((lang) => lang.extensions?.includes("." + extension));
@ -194,12 +187,12 @@ class SourceCodeRenderer extends React.Component<
});
}
}
};
}
registerKeybindings() {
const { lineId } = this.props.context;
let domain = "code-" + lineId;
let keybindManager = GlobalModel.keybindManager;
const domain = "code-" + lineId;
const keybindManager = GlobalModel.keybindManager;
keybindManager.registerKeybinding("plugin", domain, "codeedit:save", (waveEvent) => {
this.doSave();
return true;
@ -216,20 +209,20 @@ class SourceCodeRenderer extends React.Component<
unregisterKeybindings() {
const { lineId } = this.props.context;
let domain = "code-" + lineId;
const domain = "code-" + lineId;
GlobalModel.keybindManager.unregisterDomain(domain);
}
handleEditorDidMount = (editor: MonacoTypes.editor.IStandaloneCodeEditor, monaco: Monaco) => {
handleEditorDidMount(editor: MonacoTypes.editor.IStandaloneCodeEditor, monaco: Monaco) {
this.monacoEditor = editor;
this.setInitialLanguage(editor);
this.setEditorHeight();
setTimeout(() => {
let opts = this.getEditorOptions();
const opts = this.getEditorOptions();
editor.updateOptions(opts);
}, 2000);
editor.onKeyDown((e: MonacoTypes.IKeyboardEvent) => {
let waveEvent = adaptFromReactOrNativeKeyEvent(e.browserEvent);
const waveEvent = adaptFromReactOrNativeKeyEvent(e.browserEvent);
console.log("keydown?", waveEvent);
if (
GlobalModel.keybindManager.checkKeysPressed(waveEvent, [
@ -261,7 +254,7 @@ class SourceCodeRenderer extends React.Component<
});
}
if (!this.getAllowEditing()) this.setState({ showReadonly: true });
};
}
handleEditorScrollChange(e) {
if (!this.state.showPreview) return;
@ -291,7 +284,7 @@ class SourceCodeRenderer extends React.Component<
}
}
handleLanguageChange = (e: any) => {
handleLanguageChange(e: any) {
const selectedLanguage = e.target.value;
this.setState({
selectedLanguage,
@ -304,9 +297,9 @@ class SourceCodeRenderer extends React.Component<
this.saveLineState({ lang: selectedLanguage });
}
}
};
}
doSave = (onSave = () => {}) => {
doSave(onSave = () => {}) {
if (!this.state.isSave) return;
const { screenId, lineId } = this.props.context;
const encodedCode = new TextEncoder().encode(this.state.code);
@ -326,9 +319,9 @@ class SourceCodeRenderer extends React.Component<
this.setState({ message: { status: "error", text: e.message } });
setTimeout(() => this.setState({ message: null }), 3000);
});
};
}
doClose = () => {
doClose() {
// if there is unsaved data
if (this.state.isSave)
return GlobalModel.showAlert({
@ -363,15 +356,15 @@ class SourceCodeRenderer extends React.Component<
if (this.props.shouldFocus) {
GlobalCommandRunner.screenSetFocus("input");
}
};
}
handleEditorChange = (code) => {
handleEditorChange(code) {
SourceCodeRenderer.codeCache.set(this.cacheKey, code);
this.setState({ code }, () => {
this.setEditorHeight();
this.setState({ isSave: code !== this.originalCode });
});
};
}
getEditorHeightBuffer(): number {
const heightBuffer = GlobalModel.lineHeightEnv.lineHeight + 11;
@ -381,7 +374,7 @@ class SourceCodeRenderer extends React.Component<
setEditorHeight = () => {
const maxEditorHeight = this.props.opts.maxSize.height - this.getEditorHeightBuffer();
let _editorHeight = maxEditorHeight;
let allowEditing = this.getAllowEditing();
const allowEditing = this.getAllowEditing();
if (!allowEditing) {
const noOfLines = Math.max(this.state.code.split("\n").length, 5);
const lineHeight = Math.ceil(GlobalModel.lineHeightEnv.lineHeight);
@ -395,8 +388,8 @@ class SourceCodeRenderer extends React.Component<
};
getAllowEditing(): boolean {
let lineState = this.props.lineState;
let mode = lineState["mode"] || "view";
const lineState = this.props.lineState;
const mode = lineState["mode"] || "view";
if (mode == "view") {
return false;
}
@ -407,32 +400,25 @@ class SourceCodeRenderer extends React.Component<
if (!this.monacoEditor) {
return;
}
let opts = this.getEditorOptions();
const opts = this.getEditorOptions();
this.monacoEditor.updateOptions(opts);
}
getEditorOptions(): MonacoTypes.editor.IEditorOptions {
let opts: MonacoTypes.editor.IEditorOptions = {
const opts: MonacoTypes.editor.IEditorOptions = {
scrollBeyondLastLine: false,
fontSize: GlobalModel.getTermFontSize(),
fontFamily: GlobalModel.getTermFontFamily(),
readOnly: !this.getAllowEditing(),
};
let lineState = this.props.lineState;
let minimap = true;
if (this.state.showPreview) {
minimap = false;
} else if ("minimap" in lineState && !lineState["minimap"]) {
minimap = false;
}
if (!minimap) {
const lineState = this.props.lineState;
if (this.state.showPreview || ("minimap" in lineState && !lineState["minimap"])) {
opts.minimap = { enabled: false };
}
return opts;
}
getCodeEditor = () => {
let theme = GlobalModel.isThemeDark() ? "wave-theme-dark" : "wave-theme-light";
getCodeEditor(theme: string) {
return (
<div className="editor-wrap" style={{ maxHeight: this.state.editorHeight }}>
{this.state.showReadonly && <div className="readonly">{"read-only"}</div>}
@ -447,9 +433,9 @@ class SourceCodeRenderer extends React.Component<
/>
</div>
);
};
}
getPreviewer = () => {
getPreviewer() {
return (
<div
className="scroller"
@ -460,17 +446,20 @@ class SourceCodeRenderer extends React.Component<
<Markdown text={this.state.code} style={{ width: "100%", padding: "1rem" }} />
</div>
);
};
}
togglePreview = () => {
this.saveLineState({ showPreview: !this.state.showPreview });
this.setState({ showPreview: !this.state.showPreview });
togglePreview() {
this.setState((prevState) => {
const newPreviewState = { showPreview: !prevState.showPreview };
this.saveLineState(newPreviewState);
return newPreviewState;
});
setTimeout(() => this.updateEditorOpts(), 0);
};
}
getEditorControls = () => {
const { selectedLanguage, isSave, languages, isPreviewerAvailable, showPreview } = this.state;
let allowEditing = this.getAllowEditing();
getEditorControls() {
const { selectedLanguage, languages, isPreviewerAvailable, showPreview } = this.state;
const allowEditing = this.getAllowEditing();
return (
<>
<If condition={isPreviewerAvailable}>
@ -507,20 +496,22 @@ class SourceCodeRenderer extends React.Component<
</If>
</>
);
};
}
getMessage = () => (
<div className="messageContainer">
<div className={`message ${this.state.message.status === "error" ? "error" : ""}`}>
{this.state.message.text}
getMessage() {
return (
<div className="messageContainer">
<div className={`message ${this.state.message.status === "error" ? "error" : ""}`}>
{this.state.message.text}
</div>
</div>
</div>
);
);
}
setSizes = (sizes) => {
setSizes(sizes: number[]) {
this.setState({ editorFraction: sizes[0] });
this.saveLineState({ editorFraction: sizes[0] });
};
}
render() {
const { exitcode } = this.props;
@ -544,14 +535,16 @@ class SourceCodeRenderer extends React.Component<
</div>
);
}
let { lineNum } = this.props.context;
let screen = GlobalModel.getActiveScreen();
let lineIsSelected = mobx.computed(
const { lineNum } = this.props.context;
const screen = GlobalModel.getActiveScreen();
const lineIsSelected = mobx.computed(
() => screen.getSelectedLine() == lineNum && screen.getFocusType() == "cmd",
{
name: "code-lineisselected",
}
);
const theme = `wave-theme-${GlobalModel.isDarkTheme.get() ? "dark" : "light"}`;
console.log("lineis selected:", lineIsSelected.get());
return (
<div className="code-renderer">
@ -559,7 +552,7 @@ class SourceCodeRenderer extends React.Component<
<CodeKeybindings codeObject={this}></CodeKeybindings>
</If>
<Split sizes={[editorFraction, 1 - editorFraction]} onSetSizes={this.setSizes}>
{this.getCodeEditor()}
{this.getCodeEditor(theme)}
{isPreviewerAvailable && showPreview && this.getPreviewer()}
</Split>
<div className="flex-spacer" />

View File

@ -23,17 +23,17 @@ class TerminalKeybindings extends React.Component<{ termWrap: any; lineid: strin
}
registerKeybindings() {
let keybindManager = GlobalModel.keybindManager;
let domain = "line-" + this.props.lineid;
let termWrap = this.props.termWrap;
const keybindManager = GlobalModel.keybindManager;
const domain = "line-" + this.props.lineid;
const termWrap = this.props.termWrap;
keybindManager.registerKeybinding("plugin", domain, "terminal:copy", (waveEvent) => {
let termWrap = this.props.termWrap;
let sel = termWrap.terminal.getSelection();
const termWrap = this.props.termWrap;
const sel = termWrap.terminal.getSelection();
navigator.clipboard.writeText(sel);
return true;
});
keybindManager.registerKeybinding("plugin", domain, "terminal:paste", (waveEvent) => {
let p = navigator.clipboard.readText();
const p = navigator.clipboard.readText();
p.then((text) => {
termWrap.dataHandler?.(text, termWrap);
});
@ -105,7 +105,7 @@ class TerminalRenderer extends React.Component<
}
getSnapshotBeforeUpdate(prevProps, prevState): { height: number } {
let elem = this.elemRef.current;
const elem = this.elemRef.current;
if (elem == null) {
return { height: 0 };
}
@ -116,9 +116,8 @@ class TerminalRenderer extends React.Component<
if (this.props.onHeightChange == null) {
return;
}
let { line } = this.props;
let curHeight = 0;
let elem = this.elemRef.current;
const elem = this.elemRef.current;
if (elem != null) {
curHeight = elem.offsetHeight;
}
@ -133,12 +132,12 @@ class TerminalRenderer extends React.Component<
}
checkLoad(): void {
let { line, staticRender, visible, collapsed } = this.props;
let { staticRender, visible, collapsed } = this.props;
if (staticRender) {
return;
}
let vis = visible && visible.get() && !collapsed;
let curVis = this.termLoaded.get();
const vis = visible?.get() && !collapsed;
const curVis = this.termLoaded.get();
if (vis && !curVis) {
this.loadTerminal();
} else if (!vis && curVis) {
@ -147,13 +146,12 @@ class TerminalRenderer extends React.Component<
}
loadTerminal(): void {
let { screen, line } = this.props;
let model = GlobalModel;
let cmd = screen.getCmd(line);
const { screen, line } = this.props;
const cmd = screen.getCmd(line);
if (cmd == null) {
return;
}
let termElem = this.termRef.current;
const termElem = this.termRef.current;
if (termElem == null) {
console.log("cannot load terminal, no term elem found", line);
return;
@ -163,11 +161,11 @@ class TerminalRenderer extends React.Component<
}
unloadTerminal(unmount: boolean): void {
let { screen, line } = this.props;
const { screen, line } = this.props;
screen.unloadRenderer(line.lineid);
if (!unmount) {
mobx.action(() => this.termLoaded.set(false))();
let termElem = this.termRef.current;
const termElem = this.termRef.current;
if (termElem != null) {
termElem.replaceChildren();
}
@ -176,22 +174,21 @@ class TerminalRenderer extends React.Component<
@boundMethod
clickTermBlock(e: any) {
let { screen, line } = this.props;
let model = GlobalModel;
let termWrap = screen.getTermWrap(line.lineid);
const { screen, line } = this.props;
const termWrap = screen.getTermWrap(line.lineid);
if (termWrap != null) {
termWrap.giveFocus();
}
}
render() {
let { screen, line, width, staticRender, visible, collapsed } = this.props;
let isPhysicalFocused = mobx
const { screen, line, width, collapsed } = this.props;
const isPhysicalFocused = mobx
.computed(() => screen.getIsFocused(line.linenum), {
name: "computed-getIsFocused",
})
.get();
let isFocused = mobx
const isFocused = mobx
.computed(
() => {
let screenFocusType = screen.getFocusType();
@ -200,15 +197,15 @@ class TerminalRenderer extends React.Component<
{ name: "computed-isFocused" }
)
.get();
let cmd = screen.getCmd(line); // will not be null
let usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width);
const cmd = screen.getCmd(line); // will not be null
const usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width);
let termHeight = termHeightFromRows(usedRows, GlobalModel.getTermFontSize(), cmd.getTermMaxRows());
if (usedRows === 0) {
termHeight = 0;
}
let termLoaded = this.termLoaded.get();
let lineid = line.lineid;
let termWrap = screen.getTermWrap(lineid);
const termLoaded = this.termLoaded.get();
const lineid = line.lineid;
const termWrap = screen.getTermWrap(lineid);
return (
<div
ref={this.elemRef}

View File

@ -12,6 +12,7 @@ declare global {
type RemoteStatusTypeStrs = "connected" | "connecting" | "disconnected" | "error";
type LineContainerStrs = "main" | "sidebar" | "history";
type AppUpdateStatusType = "unavailable" | "ready";
type NativeThemeSource = "system" | "light" | "dark";
type OV<V> = mobx.IObservableValue<V>;
type OArr<V> = mobx.IObservableArray<V>;
@ -563,7 +564,7 @@ declare global {
type FeOptsType = {
termfontsize: number;
termfontfamily: string;
theme: string;
theme: NativeThemeSource;
};
type ConfirmFlagsType = {
@ -898,6 +899,10 @@ declare global {
getAuthKey: () => string;
getWaveSrvStatus: () => boolean;
getInitialTermFontFamily: () => string;
getShouldUseDarkColors: () => boolean;
getNativeThemeSource: () => NativeThemeSource;
setNativeThemeSource: (source: NativeThemeSource) => void;
onNativeThemeUpdated: (callback: () => void) => void;
restartWaveSrv: () => boolean;
reloadWindow: () => void;
openExternalLink: (url: string) => void;

View File

@ -1,10 +0,0 @@
function loadTheme(theme: string) {
const linkTag: any = document.getElementById("theme-stylesheet");
if (theme === "dark") {
linkTag.href = "public/themes/default.css";
} else {
linkTag.href = `public/themes/${theme}.css`;
}
}
export { loadTheme };

View File

@ -104,7 +104,7 @@ var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cy
var RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoinstall", "color"}
var ConfirmFlags = []string{"hideshellprompt"}
var SidebarNames = []string{"main"}
var ThemeNames = []string{"light", "dark"}
var ThemeSources = []string{"light", "dark", "system"}
var ScreenCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "reset", "signal", "chat"}
var NoHistCmds = []string{"_compgen", "line", "history", "_killserver"}
@ -5529,20 +5529,20 @@ func ClientSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sc
}
varsUpdated = append(varsUpdated, "termfontfamily")
}
if themeStr, found := pk.Kwargs["theme"]; found {
newTheme := themeStr
if themeSourceStr, found := pk.Kwargs["theme"]; found {
newThemeSource := themeSourceStr
found := false
for _, theme := range ThemeNames {
if newTheme == theme {
for _, theme := range ThemeSources {
if newThemeSource == theme {
found = true
break
}
}
if !found {
return nil, fmt.Errorf("invalid theme name")
return nil, fmt.Errorf("invalid theme source")
}
feOpts := clientData.FeOpts
feOpts.Theme = newTheme
feOpts.Theme = newThemeSource
err = sstore.UpdateClientFeOpts(ctx, feOpts)
if err != nil {
return nil, fmt.Errorf("error updating client feopts: %v", err)