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="../" /> <base href="../" />
<script charset="UTF-8" src="dist-dev/waveterm.js"></script> <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/bulma-0.9.4.min.css" />
<link rel="stylesheet" href="public/fontawesome/css/fontawesome.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/brands.min.css" />
<link rel="stylesheet" href="public/fontawesome/css/solid.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-solid.min.css" />
<link rel="stylesheet" href="public/fontawesome/css/sharp-regular.min.css"> <link rel="stylesheet" href="public/fontawesome/css/sharp-regular.min.css" />
<link rel="stylesheet" href="dist-dev/waveterm.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> </head>
<body> <body>
<div id="measure"></div> <div id="measure"></div>

View File

@ -5,14 +5,14 @@
<base href="../" /> <base href="../" />
<script charset="UTF-8" src="dist/waveterm.js"></script> <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/bulma-0.9.4.min.css" />
<link rel="stylesheet" href="public/fontawesome/css/fontawesome.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/brands.min.css" />
<link rel="stylesheet" href="public/fontawesome/css/solid.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-solid.min.css" />
<link rel="stylesheet" href="public/fontawesome/css/sharp-regular.min.css"> <link rel="stylesheet" href="public/fontawesome/css/sharp-regular.min.css" />
<link rel="stylesheet" href="dist/waveterm.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> </head>
<body> <body>
<div id="measure"></div> <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 { If } from "tsx-control-statements/components";
import dayjs from "dayjs"; import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat"; import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalCommandRunner, GlobalModel } from "@/models"; import { GlobalModel } from "@/models";
import { isBlank } from "@/util/util"; import { isBlank } from "@/util/util";
import { WorkspaceView } from "./workspace/workspaceview"; import { WorkspaceView } from "./workspace/workspaceview";
import { PluginsView } from "./pluginsview/pluginsview"; import { PluginsView } from "./pluginsview/pluginsview";
@ -123,7 +123,7 @@ class App extends React.Component<{}, {}> {
const mainSidebarCollapsed = GlobalModel.mainSidebarModel.getCollapsed(); const mainSidebarCollapsed = GlobalModel.mainSidebarModel.getCollapsed();
const rightSidebarCollapsed = GlobalModel.rightSidebarModel.getCollapsed(); const rightSidebarCollapsed = GlobalModel.rightSidebarModel.getCollapsed();
const activeMainView = GlobalModel.activeMainView.get(); const activeMainView = GlobalModel.activeMainView.get();
const lightDarkClass = GlobalModel.isThemeDark() ? "is-dark" : "is-light"; const lightDarkClass = GlobalModel.isDarkTheme.get() ? "is-dark" : "is-light";
return ( return (
<div <div
key={"version-" + renderVersion} key={"version-" + renderVersion}

View File

@ -64,11 +64,12 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
} }
@boundMethod @boundMethod
handleChangeTheme(theme: string): void { handleChangeThemeSource(themeSource: NativeThemeSource): void {
if (GlobalModel.getTheme() == theme) { if (GlobalModel.getThemeSource() == themeSource) {
return; return;
} }
const prtn = GlobalCommandRunner.setTheme(theme, false); const prtn = GlobalCommandRunner.setTheme(themeSource, false);
getApi().setNativeThemeSource(themeSource);
commandRtnHandler(prtn, this.errorMessage); commandRtnHandler(prtn, this.errorMessage);
} }
@ -111,11 +112,12 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
return availableFontFamilies; return availableFontFamilies;
} }
getThemes(): DropdownItem[] { getThemeSources(): DropdownItem[] {
const themes: DropdownItem[] = []; const themeSources: DropdownItem[] = [];
themes.push({ label: "Dark", value: "dark" }); themeSources.push({ label: "Dark", value: "dark" });
themes.push({ label: "Light", value: "light" }); themeSources.push({ label: "Light", value: "light" });
return themes; themeSources.push({ label: "System", value: "system" });
return themeSources;
} }
@boundMethod @boundMethod
@ -190,7 +192,7 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
); );
const curFontSize = GlobalModel.getTermFontSize(); const curFontSize = GlobalModel.getTermFontSize();
const curFontFamily = GlobalModel.getTermFontFamily(); const curFontFamily = GlobalModel.getTermFontFamily();
const curTheme = GlobalModel.getTheme(); const curTheme = GlobalModel.getThemeSource();
return ( return (
<MainView className="clientsettings-view" title="Client Settings" onClose={this.handleClose}> <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"> <div className="settings-input">
<Dropdown <Dropdown
className="theme-dropdown" className="theme-dropdown"
options={this.getThemes()} options={this.getThemeSources()}
defaultValue={curTheme} defaultValue={curTheme}
onChange={this.handleChangeTheme} onChange={this.handleChangeThemeSource}
/> />
</div> </div>
</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) { function readLastLinesOfFile(filePath: string, lineCount: number) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
child_process.exec(`tail -n ${lineCount} "${filePath}"`, (err, stdout, stderr) => { 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)); ipcRenderer.once("last-logs", (event, data) => callback(data));
}, },
getInitialTermFontFamily: () => ipcRenderer.sendSync("get-initial-termfontfamily"), 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"), restartWaveSrv: () => ipcRenderer.sendSync("restart-server"),
reloadWindow: () => ipcRenderer.sendSync("reload-window"), reloadWindow: () => ipcRenderer.sendSync("reload-window"),
reregisterGlobalShortcut: (shortcut) => ipcRenderer.sendSync("reregister-global-shortcut", shortcut), reregisterGlobalShortcut: (shortcut) => ipcRenderer.sendSync("reregister-global-shortcut", shortcut),

View File

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

View File

@ -13,12 +13,11 @@ import {
isModKeyPress, isModKeyPress,
isBlank, isBlank,
} from "@/util/util"; } from "@/util/util";
import { loadTheme } from "@/util/themeutil";
import { WSControl } from "./ws"; import { WSControl } from "./ws";
import { cmdStatusIsRunning } from "@/app/line/lineutil"; import { cmdStatusIsRunning } from "@/app/line/lineutil";
import * as appconst from "@/app/appconst"; import * as appconst from "@/app/appconst";
import { remotePtrToString, cmdPacketString } from "@/util/modelutil"; 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 { Session } from "./session";
import { ScreenLines } from "./screenlines"; import { ScreenLines } from "./screenlines";
import { InputModel } from "./input"; import { InputModel } from "./input";
@ -118,6 +117,9 @@ class Model {
modalsModel: ModalsModel; modalsModel: ModalsModel;
mainSidebarModel: MainSidebarModel; mainSidebarModel: MainSidebarModel;
rightSidebarModel: RightSidebarModel; rightSidebarModel: RightSidebarModel;
isDarkTheme: OV<boolean> = mobx.observable.box(getApi().getShouldUseDarkColors(), {
name: "isDarkTheme",
});
clientData: OV<ClientDataType> = mobx.observable.box(null, { clientData: OV<ClientDataType> = mobx.observable.box(null, {
name: "clientData", name: "clientData",
}); });
@ -181,6 +183,7 @@ class Model {
getApi().onMenuItemAbout(this.onMenuItemAbout.bind(this)); getApi().onMenuItemAbout(this.onMenuItemAbout.bind(this));
getApi().onWaveSrvStatusChange(this.onWaveSrvStatusChange.bind(this)); getApi().onWaveSrvStatusChange(this.onWaveSrvStatusChange.bind(this));
getApi().onAppUpdateStatus(this.onAppUpdateStatus.bind(this)); getApi().onAppUpdateStatus(this.onAppUpdateStatus.bind(this));
getApi().onNativeThemeUpdated(this.onNativeThemeUpdated.bind(this));
document.addEventListener("keydown", this.docKeyDownHandler.bind(this)); document.addEventListener("keydown", this.docKeyDownHandler.bind(this));
document.addEventListener("selectionchange", this.docSelectionChangeHandler.bind(this)); document.addEventListener("selectionchange", this.docSelectionChangeHandler.bind(this));
setTimeout(() => this.getClientDataLoop(1), 10); setTimeout(() => this.getClientDataLoop(1), 10);
@ -389,18 +392,19 @@ class Model {
return ff; return ff;
} }
getTheme(): string { getThemeSource(): NativeThemeSource {
let cdata = this.clientData.get(); return getApi().getNativeThemeSource();
let theme = cdata?.feopts?.theme;
if (theme == null) {
theme = appconst.DefaultTheme;
}
return theme;
} }
isThemeDark(): boolean { onNativeThemeUpdated(): void {
let cdata = this.clientData.get(); console.log("native theme updated");
return cdata?.feopts?.theme != "light"; const isDark = getApi().getShouldUseDarkColors();
if (isDark != this.isDarkTheme.get()) {
mobx.action(() => {
this.isDarkTheme.set(isDark);
this.bumpRenderVersion();
})();
}
} }
getTermFontSize(): number { getTermFontSize(): number {
@ -1197,7 +1201,7 @@ class Model {
if (newTheme == null) { if (newTheme == null) {
newTheme = appconst.DefaultTheme; newTheme = appconst.DefaultTheme;
} }
const themeUpdated = newTheme != this.getTheme(); const themeUpdated = newTheme != this.getThemeSource();
mobx.action(() => { mobx.action(() => {
this.clientData.set(clientData); this.clientData.set(clientData);
})(); })();
@ -1215,7 +1219,7 @@ class Model {
this.updateTermFontSizeVars(); this.updateTermFontSizeVars();
} }
if (themeUpdated) { if (themeUpdated) {
loadTheme(newTheme); getApi().setNativeThemeSource(newTheme);
this.bumpRenderVersion(); 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 type * as MonacoTypes from "monaco-editor/esm/vs/editor/editor.api";
import cn from "classnames"; import cn from "classnames";
import { If } from "tsx-control-statements/components"; import { If } from "tsx-control-statements/components";
import { Markdown } from "@/elements"; import { Markdown, Button } from "@/elements";
import { GlobalModel, GlobalCommandRunner } from "@/models"; import { GlobalModel, GlobalCommandRunner } from "@/models";
import Split from "react-split-it"; import Split from "react-split-it";
import loader from "@monaco-editor/loader"; import loader from "@monaco-editor/loader";
import { import { adaptFromReactOrNativeKeyEvent } from "@/util/keyutil";
checkKeyPressed,
adaptFromReactOrNativeKeyEvent,
KeybindManager,
adaptFromElectronKeyEvent,
} from "@/util/keyutil";
import { Button, Dropdown } from "@/elements";
import "./code.less"; import "./code.less";
@ -99,7 +93,7 @@ class SourceCodeRenderer extends React.Component<
* codeCache is a Hashmap with key=screenId:lineId:filepath and value=code * 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. * 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 // which languages have preview options
languagesWithPreviewer: string[] = ["markdown", "mdx"]; 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 monacoEditor: MonacoTypes.editor.IStandaloneCodeEditor; // reference to mounted monaco editor. TODO need the correct type
markdownRef: React.RefObject<HTMLDivElement>; markdownRef: React.RefObject<HTMLDivElement>;
syncing: boolean; syncing: boolean;
monacoOptions: MonacoTypes.editor.IEditorOptions & MonacoTypes.editor.IGlobalEditorOptions;
constructor(props) { constructor(props) {
super(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 const editorHeight = Math.max(props.savedHeight - this.getEditorHeightBuffer(), 0); // must subtract the padding/margin to get the real editorHeight
this.markdownRef = React.createRef(); this.markdownRef = React.createRef();
this.syncing = false; this.syncing = false;
let isClosed = props.lineState["prompt:closed"]; const isClosed = props.lineState["prompt:closed"];
this.state = { this.state = {
code: null, code: null,
languages: [], languages: [],
@ -161,12 +154,12 @@ class SourceCodeRenderer extends React.Component<
} }
} }
saveLineState = (kvp) => { saveLineState(kvp) {
const { screenId, lineId } = this.props.context; const { screenId, lineId } = this.props.context;
GlobalCommandRunner.setLineState(screenId, lineId, { ...this.props.lineState, ...kvp }, false); GlobalCommandRunner.setLineState(screenId, lineId, { ...this.props.lineState, ...kvp }, false);
}; }
setInitialLanguage = (editor) => { setInitialLanguage(editor) {
// set all languages // set all languages
const languages = monaco.languages.getLanguages().map((lang) => lang.id); const languages = monaco.languages.getLanguages().map((lang) => lang.id);
this.setState({ languages }); 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 not found, we try to grab the filename from with filePath (coming from lineState["prompt:file"]) or cmdstr
if (!detectedLanguage) { if (!detectedLanguage) {
const strForFilePath = this.filePath || this.props.cmdstr; 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 const detectedLanguageObj = monaco.languages
.getLanguages() .getLanguages()
.find((lang) => lang.extensions?.includes("." + extension)); .find((lang) => lang.extensions?.includes("." + extension));
@ -194,12 +187,12 @@ class SourceCodeRenderer extends React.Component<
}); });
} }
} }
}; }
registerKeybindings() { registerKeybindings() {
const { lineId } = this.props.context; const { lineId } = this.props.context;
let domain = "code-" + lineId; const domain = "code-" + lineId;
let keybindManager = GlobalModel.keybindManager; const keybindManager = GlobalModel.keybindManager;
keybindManager.registerKeybinding("plugin", domain, "codeedit:save", (waveEvent) => { keybindManager.registerKeybinding("plugin", domain, "codeedit:save", (waveEvent) => {
this.doSave(); this.doSave();
return true; return true;
@ -216,20 +209,20 @@ class SourceCodeRenderer extends React.Component<
unregisterKeybindings() { unregisterKeybindings() {
const { lineId } = this.props.context; const { lineId } = this.props.context;
let domain = "code-" + lineId; const domain = "code-" + lineId;
GlobalModel.keybindManager.unregisterDomain(domain); GlobalModel.keybindManager.unregisterDomain(domain);
} }
handleEditorDidMount = (editor: MonacoTypes.editor.IStandaloneCodeEditor, monaco: Monaco) => { handleEditorDidMount(editor: MonacoTypes.editor.IStandaloneCodeEditor, monaco: Monaco) {
this.monacoEditor = editor; this.monacoEditor = editor;
this.setInitialLanguage(editor); this.setInitialLanguage(editor);
this.setEditorHeight(); this.setEditorHeight();
setTimeout(() => { setTimeout(() => {
let opts = this.getEditorOptions(); const opts = this.getEditorOptions();
editor.updateOptions(opts); editor.updateOptions(opts);
}, 2000); }, 2000);
editor.onKeyDown((e: MonacoTypes.IKeyboardEvent) => { editor.onKeyDown((e: MonacoTypes.IKeyboardEvent) => {
let waveEvent = adaptFromReactOrNativeKeyEvent(e.browserEvent); const waveEvent = adaptFromReactOrNativeKeyEvent(e.browserEvent);
console.log("keydown?", waveEvent); console.log("keydown?", waveEvent);
if ( if (
GlobalModel.keybindManager.checkKeysPressed(waveEvent, [ GlobalModel.keybindManager.checkKeysPressed(waveEvent, [
@ -261,7 +254,7 @@ class SourceCodeRenderer extends React.Component<
}); });
} }
if (!this.getAllowEditing()) this.setState({ showReadonly: true }); if (!this.getAllowEditing()) this.setState({ showReadonly: true });
}; }
handleEditorScrollChange(e) { handleEditorScrollChange(e) {
if (!this.state.showPreview) return; 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; const selectedLanguage = e.target.value;
this.setState({ this.setState({
selectedLanguage, selectedLanguage,
@ -304,9 +297,9 @@ class SourceCodeRenderer extends React.Component<
this.saveLineState({ lang: selectedLanguage }); this.saveLineState({ lang: selectedLanguage });
} }
} }
}; }
doSave = (onSave = () => {}) => { doSave(onSave = () => {}) {
if (!this.state.isSave) return; if (!this.state.isSave) return;
const { screenId, lineId } = this.props.context; const { screenId, lineId } = this.props.context;
const encodedCode = new TextEncoder().encode(this.state.code); 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 } }); this.setState({ message: { status: "error", text: e.message } });
setTimeout(() => this.setState({ message: null }), 3000); setTimeout(() => this.setState({ message: null }), 3000);
}); });
}; }
doClose = () => { doClose() {
// if there is unsaved data // if there is unsaved data
if (this.state.isSave) if (this.state.isSave)
return GlobalModel.showAlert({ return GlobalModel.showAlert({
@ -363,15 +356,15 @@ class SourceCodeRenderer extends React.Component<
if (this.props.shouldFocus) { if (this.props.shouldFocus) {
GlobalCommandRunner.screenSetFocus("input"); GlobalCommandRunner.screenSetFocus("input");
} }
}; }
handleEditorChange = (code) => { handleEditorChange(code) {
SourceCodeRenderer.codeCache.set(this.cacheKey, code); SourceCodeRenderer.codeCache.set(this.cacheKey, code);
this.setState({ code }, () => { this.setState({ code }, () => {
this.setEditorHeight(); this.setEditorHeight();
this.setState({ isSave: code !== this.originalCode }); this.setState({ isSave: code !== this.originalCode });
}); });
}; }
getEditorHeightBuffer(): number { getEditorHeightBuffer(): number {
const heightBuffer = GlobalModel.lineHeightEnv.lineHeight + 11; const heightBuffer = GlobalModel.lineHeightEnv.lineHeight + 11;
@ -381,7 +374,7 @@ class SourceCodeRenderer extends React.Component<
setEditorHeight = () => { setEditorHeight = () => {
const maxEditorHeight = this.props.opts.maxSize.height - this.getEditorHeightBuffer(); const maxEditorHeight = this.props.opts.maxSize.height - this.getEditorHeightBuffer();
let _editorHeight = maxEditorHeight; let _editorHeight = maxEditorHeight;
let allowEditing = this.getAllowEditing(); const allowEditing = this.getAllowEditing();
if (!allowEditing) { if (!allowEditing) {
const noOfLines = Math.max(this.state.code.split("\n").length, 5); const noOfLines = Math.max(this.state.code.split("\n").length, 5);
const lineHeight = Math.ceil(GlobalModel.lineHeightEnv.lineHeight); const lineHeight = Math.ceil(GlobalModel.lineHeightEnv.lineHeight);
@ -395,8 +388,8 @@ class SourceCodeRenderer extends React.Component<
}; };
getAllowEditing(): boolean { getAllowEditing(): boolean {
let lineState = this.props.lineState; const lineState = this.props.lineState;
let mode = lineState["mode"] || "view"; const mode = lineState["mode"] || "view";
if (mode == "view") { if (mode == "view") {
return false; return false;
} }
@ -407,32 +400,25 @@ class SourceCodeRenderer extends React.Component<
if (!this.monacoEditor) { if (!this.monacoEditor) {
return; return;
} }
let opts = this.getEditorOptions(); const opts = this.getEditorOptions();
this.monacoEditor.updateOptions(opts); this.monacoEditor.updateOptions(opts);
} }
getEditorOptions(): MonacoTypes.editor.IEditorOptions { getEditorOptions(): MonacoTypes.editor.IEditorOptions {
let opts: MonacoTypes.editor.IEditorOptions = { const opts: MonacoTypes.editor.IEditorOptions = {
scrollBeyondLastLine: false, scrollBeyondLastLine: false,
fontSize: GlobalModel.getTermFontSize(), fontSize: GlobalModel.getTermFontSize(),
fontFamily: GlobalModel.getTermFontFamily(), fontFamily: GlobalModel.getTermFontFamily(),
readOnly: !this.getAllowEditing(), readOnly: !this.getAllowEditing(),
}; };
let lineState = this.props.lineState; const lineState = this.props.lineState;
let minimap = true; if (this.state.showPreview || ("minimap" in lineState && !lineState["minimap"])) {
if (this.state.showPreview) {
minimap = false;
} else if ("minimap" in lineState && !lineState["minimap"]) {
minimap = false;
}
if (!minimap) {
opts.minimap = { enabled: false }; opts.minimap = { enabled: false };
} }
return opts; return opts;
} }
getCodeEditor = () => { getCodeEditor(theme: string) {
let theme = GlobalModel.isThemeDark() ? "wave-theme-dark" : "wave-theme-light";
return ( return (
<div className="editor-wrap" style={{ maxHeight: this.state.editorHeight }}> <div className="editor-wrap" style={{ maxHeight: this.state.editorHeight }}>
{this.state.showReadonly && <div className="readonly">{"read-only"}</div>} {this.state.showReadonly && <div className="readonly">{"read-only"}</div>}
@ -447,9 +433,9 @@ class SourceCodeRenderer extends React.Component<
/> />
</div> </div>
); );
}; }
getPreviewer = () => { getPreviewer() {
return ( return (
<div <div
className="scroller" className="scroller"
@ -460,17 +446,20 @@ class SourceCodeRenderer extends React.Component<
<Markdown text={this.state.code} style={{ width: "100%", padding: "1rem" }} /> <Markdown text={this.state.code} style={{ width: "100%", padding: "1rem" }} />
</div> </div>
); );
}; }
togglePreview = () => { togglePreview() {
this.saveLineState({ showPreview: !this.state.showPreview }); this.setState((prevState) => {
this.setState({ showPreview: !this.state.showPreview }); const newPreviewState = { showPreview: !prevState.showPreview };
this.saveLineState(newPreviewState);
return newPreviewState;
});
setTimeout(() => this.updateEditorOpts(), 0); setTimeout(() => this.updateEditorOpts(), 0);
}; }
getEditorControls = () => { getEditorControls() {
const { selectedLanguage, isSave, languages, isPreviewerAvailable, showPreview } = this.state; const { selectedLanguage, languages, isPreviewerAvailable, showPreview } = this.state;
let allowEditing = this.getAllowEditing(); const allowEditing = this.getAllowEditing();
return ( return (
<> <>
<If condition={isPreviewerAvailable}> <If condition={isPreviewerAvailable}>
@ -507,20 +496,22 @@ class SourceCodeRenderer extends React.Component<
</If> </If>
</> </>
); );
}; }
getMessage = () => ( getMessage() {
return (
<div className="messageContainer"> <div className="messageContainer">
<div className={`message ${this.state.message.status === "error" ? "error" : ""}`}> <div className={`message ${this.state.message.status === "error" ? "error" : ""}`}>
{this.state.message.text} {this.state.message.text}
</div> </div>
</div> </div>
); );
}
setSizes = (sizes) => { setSizes(sizes: number[]) {
this.setState({ editorFraction: sizes[0] }); this.setState({ editorFraction: sizes[0] });
this.saveLineState({ editorFraction: sizes[0] }); this.saveLineState({ editorFraction: sizes[0] });
}; }
render() { render() {
const { exitcode } = this.props; const { exitcode } = this.props;
@ -544,14 +535,16 @@ class SourceCodeRenderer extends React.Component<
</div> </div>
); );
} }
let { lineNum } = this.props.context; const { lineNum } = this.props.context;
let screen = GlobalModel.getActiveScreen(); const screen = GlobalModel.getActiveScreen();
let lineIsSelected = mobx.computed( const lineIsSelected = mobx.computed(
() => screen.getSelectedLine() == lineNum && screen.getFocusType() == "cmd", () => screen.getSelectedLine() == lineNum && screen.getFocusType() == "cmd",
{ {
name: "code-lineisselected", name: "code-lineisselected",
} }
); );
const theme = `wave-theme-${GlobalModel.isDarkTheme.get() ? "dark" : "light"}`;
console.log("lineis selected:", lineIsSelected.get()); console.log("lineis selected:", lineIsSelected.get());
return ( return (
<div className="code-renderer"> <div className="code-renderer">
@ -559,7 +552,7 @@ class SourceCodeRenderer extends React.Component<
<CodeKeybindings codeObject={this}></CodeKeybindings> <CodeKeybindings codeObject={this}></CodeKeybindings>
</If> </If>
<Split sizes={[editorFraction, 1 - editorFraction]} onSetSizes={this.setSizes}> <Split sizes={[editorFraction, 1 - editorFraction]} onSetSizes={this.setSizes}>
{this.getCodeEditor()} {this.getCodeEditor(theme)}
{isPreviewerAvailable && showPreview && this.getPreviewer()} {isPreviewerAvailable && showPreview && this.getPreviewer()}
</Split> </Split>
<div className="flex-spacer" /> <div className="flex-spacer" />

View File

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

View File

@ -12,6 +12,7 @@ declare global {
type RemoteStatusTypeStrs = "connected" | "connecting" | "disconnected" | "error"; type RemoteStatusTypeStrs = "connected" | "connecting" | "disconnected" | "error";
type LineContainerStrs = "main" | "sidebar" | "history"; type LineContainerStrs = "main" | "sidebar" | "history";
type AppUpdateStatusType = "unavailable" | "ready"; type AppUpdateStatusType = "unavailable" | "ready";
type NativeThemeSource = "system" | "light" | "dark";
type OV<V> = mobx.IObservableValue<V>; type OV<V> = mobx.IObservableValue<V>;
type OArr<V> = mobx.IObservableArray<V>; type OArr<V> = mobx.IObservableArray<V>;
@ -563,7 +564,7 @@ declare global {
type FeOptsType = { type FeOptsType = {
termfontsize: number; termfontsize: number;
termfontfamily: string; termfontfamily: string;
theme: string; theme: NativeThemeSource;
}; };
type ConfirmFlagsType = { type ConfirmFlagsType = {
@ -898,6 +899,10 @@ declare global {
getAuthKey: () => string; getAuthKey: () => string;
getWaveSrvStatus: () => boolean; getWaveSrvStatus: () => boolean;
getInitialTermFontFamily: () => string; getInitialTermFontFamily: () => string;
getShouldUseDarkColors: () => boolean;
getNativeThemeSource: () => NativeThemeSource;
setNativeThemeSource: (source: NativeThemeSource) => void;
onNativeThemeUpdated: (callback: () => void) => void;
restartWaveSrv: () => boolean; restartWaveSrv: () => boolean;
reloadWindow: () => void; reloadWindow: () => void;
openExternalLink: (url: string) => 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 RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoinstall", "color"}
var ConfirmFlags = []string{"hideshellprompt"} var ConfirmFlags = []string{"hideshellprompt"}
var SidebarNames = []string{"main"} 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 ScreenCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "reset", "signal", "chat"}
var NoHistCmds = []string{"_compgen", "line", "history", "_killserver"} var NoHistCmds = []string{"_compgen", "line", "history", "_killserver"}
@ -5529,20 +5529,20 @@ func ClientSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sc
} }
varsUpdated = append(varsUpdated, "termfontfamily") varsUpdated = append(varsUpdated, "termfontfamily")
} }
if themeStr, found := pk.Kwargs["theme"]; found { if themeSourceStr, found := pk.Kwargs["theme"]; found {
newTheme := themeStr newThemeSource := themeSourceStr
found := false found := false
for _, theme := range ThemeNames { for _, theme := range ThemeSources {
if newTheme == theme { if newThemeSource == theme {
found = true found = true
break break
} }
} }
if !found { if !found {
return nil, fmt.Errorf("invalid theme name") return nil, fmt.Errorf("invalid theme source")
} }
feOpts := clientData.FeOpts feOpts := clientData.FeOpts
feOpts.Theme = newTheme feOpts.Theme = newThemeSource
err = sstore.UpdateClientFeOpts(ctx, feOpts) err = sstore.UpdateClientFeOpts(ctx, feOpts)
if err != nil { if err != nil {
return nil, fmt.Errorf("error updating client feopts: %v", err) return nil, fmt.Errorf("error updating client feopts: %v", err)