mirror of
https://github.com/wavetermdev/backup.git
synced 2026-04-22 15:26:58 -07:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 80e1e3ce55 | |||
| 85886afeb4 | |||
| 567e5b662c | |||
| 6b5d2833eb | |||
| b10910d229 | |||
| a7e6224862 | |||
| 5476b45f22 | |||
| efcd8ad03a | |||
| 03acda07b5 | |||
| 32342629c8 | |||
| d2ddf7bc58 | |||
| 01fb217785 | |||
| e5d3db7631 | |||
| c24e99e9ff |
@@ -13,6 +13,7 @@
|
||||
<link rel="stylesheet" href="dist-dev/waveterm.css" />
|
||||
|
||||
<link rel="stylesheet" id="theme-stylesheet" href="public/themes/themequery.css" />
|
||||
<link rel="stylesheet" href="public/themes/test.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="measure"></div>
|
||||
|
||||
+24
-6
@@ -22,6 +22,7 @@ import { DisconnectedModal, ClientStopModal } from "@/modals";
|
||||
import { ModalsProvider } from "@/modals/provider";
|
||||
import { Button, TermStyleBlock } from "@/elements";
|
||||
import { ErrorBoundary } from "@/common/error/errorboundary";
|
||||
import { For } from "tsx-control-statements/components";
|
||||
import cn from "classnames";
|
||||
import "./app.less";
|
||||
|
||||
@@ -80,6 +81,21 @@ class App extends React.Component<{}, {}> {
|
||||
rightSidebarModel.saveState(width, false);
|
||||
}
|
||||
|
||||
getSelector(themeKey: string) {
|
||||
const sessions = GlobalModel.getSessionNames();
|
||||
const screens = GlobalModel.getScreenNames();
|
||||
|
||||
if (themeKey === "main") {
|
||||
return ":root";
|
||||
} else if (themeKey in screens) {
|
||||
return `.main-content [data-screenid="${themeKey}"]`;
|
||||
} else if (themeKey in sessions) {
|
||||
return `.main-content [data-sessionid="${themeKey}"]`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const remotesModel = GlobalModel.remotesModel;
|
||||
const disconnected = !GlobalModel.ws.open.get() || !GlobalModel.waveSrvRunning.get();
|
||||
@@ -125,6 +141,7 @@ class App extends React.Component<{}, {}> {
|
||||
const activeMainView = GlobalModel.activeMainView.get();
|
||||
const lightDarkClass = GlobalModel.isDarkTheme.get() ? "is-dark" : "is-light";
|
||||
const termTheme = GlobalModel.getTermTheme();
|
||||
const themeKey = null;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -153,12 +170,13 @@ class App extends React.Component<{}, {}> {
|
||||
</div>
|
||||
</If>
|
||||
<div ref={this.mainContentRef} className="main-content">
|
||||
<TermStyleBlock
|
||||
scope="main"
|
||||
termTheme={termTheme}
|
||||
themeSrcEl={this.mainContentRef.current}
|
||||
themeKey="main"
|
||||
/>
|
||||
<For index="idx" each="themeKey" of={Object.keys(termTheme)}>
|
||||
<TermStyleBlock
|
||||
key={themeKey}
|
||||
themeName={termTheme[themeKey]}
|
||||
selector={this.getSelector(themeKey)}
|
||||
/>
|
||||
</For>
|
||||
<MainSideBar parentRef={this.mainContentRef} />
|
||||
<ErrorBoundary>
|
||||
<PluginsView />
|
||||
|
||||
@@ -82,7 +82,6 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
|
||||
if (currTheme == theme) {
|
||||
return;
|
||||
}
|
||||
|
||||
const prtn = GlobalCommandRunner.setMainTermTheme(theme, false);
|
||||
commandRtnHandler(prtn, this.errorMessage);
|
||||
}
|
||||
@@ -207,7 +206,7 @@ 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 termThemes = getTermThemes(GlobalModel.termThemeOptions.get(), "Wave Default");
|
||||
const currTermTheme = GlobalModel.getTermTheme()["main"] ?? termThemes[0].label;
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import * as mobx from "mobx";
|
||||
import { GlobalModel } from "@/models";
|
||||
import { isBlank } from "@/util/util";
|
||||
|
||||
const VALID_CSS_VARIABLES = [
|
||||
"--term-black",
|
||||
@@ -33,22 +31,21 @@ const VALID_CSS_VARIABLES = [
|
||||
];
|
||||
|
||||
@mobxReact.observer
|
||||
class TermStyleBlock extends React.Component<
|
||||
{ scope: "main" | "session" | "screen"; themeSrcEl: HTMLElement; themeKey: string; termTheme: TermThemeType },
|
||||
{ styleRules: string }
|
||||
> {
|
||||
styleRules: OV<string> = mobx.observable.box("", { name: "StyleBlock-styleRules" });
|
||||
theme: string;
|
||||
|
||||
componentDidMount(): void {
|
||||
this.loadThemeStyles();
|
||||
class TermStyleBlock extends React.Component<{
|
||||
themeName: string;
|
||||
selector: string;
|
||||
}> {
|
||||
componentDidMount() {
|
||||
GlobalModel.bumpTermRenderVersion();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps): void {
|
||||
const { themeKey, termTheme } = this.props;
|
||||
const currTheme = termTheme[themeKey];
|
||||
if (themeKey !== prevProps.themeKey || currTheme !== this.theme) {
|
||||
this.loadThemeStyles();
|
||||
componentWillUnmount() {
|
||||
GlobalModel.bumpTermRenderVersion();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.themeName !== this.props.themeName || prevProps.selector !== this.props.selector) {
|
||||
GlobalModel.bumpTermRenderVersion();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,59 +55,40 @@ class TermStyleBlock extends React.Component<
|
||||
return element.style.color !== "";
|
||||
}
|
||||
|
||||
isValidTermCSSVariable(key, value) {
|
||||
const cssVarName = `--term-${key}`;
|
||||
console.log("cssVarName", cssVarName, "value", value);
|
||||
return VALID_CSS_VARIABLES.includes(cssVarName);
|
||||
}
|
||||
|
||||
camelCaseToKebabCase(str) {
|
||||
return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
||||
}
|
||||
|
||||
loadThemeStyles() {
|
||||
const { themeKey, termTheme, scope } = this.props;
|
||||
const currTheme = termTheme[themeKey];
|
||||
|
||||
if (currTheme && currTheme !== this.theme && currTheme) {
|
||||
const rtn = GlobalModel.getTermThemeJson(currTheme);
|
||||
rtn.then((termThemeJson) => {
|
||||
if (termThemeJson && typeof termThemeJson === "object") {
|
||||
const styleProperties = Object.entries(termThemeJson)
|
||||
.filter(([key, value]) => {
|
||||
const cssVarName = `--term-${this.camelCaseToKebabCase(key)}`;
|
||||
return VALID_CSS_VARIABLES.includes(cssVarName) && this.isValidCSSColor(value);
|
||||
})
|
||||
.map(([key, value]) => `--term-${key}: ${value};`)
|
||||
.join(" ");
|
||||
|
||||
mobx.action(() => {
|
||||
this.styleRules.set(`:root { ${styleProperties} }`);
|
||||
GlobalModel.termThemeSrcEls.set(scope, this.props.themeSrcEl);
|
||||
})();
|
||||
GlobalModel.bumpTermRenderVersion();
|
||||
this.theme = currTheme;
|
||||
} else {
|
||||
console.error("termThemeJson is not an object:", termThemeJson);
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error("error loading theme styles:", error);
|
||||
});
|
||||
} else {
|
||||
mobx.action(() => {
|
||||
this.styleRules.set("");
|
||||
GlobalModel.termThemeSrcEls.set(scope, null);
|
||||
})();
|
||||
this.theme = currTheme;
|
||||
GlobalModel.bumpTermRenderVersion();
|
||||
getStyleRules() {
|
||||
const { selector, themeName } = this.props;
|
||||
const termThemeOptions = GlobalModel.getTermThemeOptions();
|
||||
if (!termThemeOptions) {
|
||||
return null;
|
||||
}
|
||||
const theme = termThemeOptions[themeName];
|
||||
if (!theme) {
|
||||
return null;
|
||||
}
|
||||
const styleProperties = Object.entries(theme)
|
||||
.filter(([key, value]) => {
|
||||
const cssVarName = `--term-${this.camelCaseToKebabCase(key)}`;
|
||||
return VALID_CSS_VARIABLES.includes(cssVarName) && this.isValidCSSColor(value);
|
||||
})
|
||||
.map(([key, value]) => `--term-${key}: ${value};`)
|
||||
.join(" ");
|
||||
|
||||
if (!styleProperties) {
|
||||
return null;
|
||||
}
|
||||
return `${selector} { ${styleProperties} }`;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (isBlank(this.styleRules.get())) {
|
||||
const styleRules = this.getStyleRules();
|
||||
if (!styleRules) {
|
||||
return null;
|
||||
}
|
||||
return <style>{this.styleRules.get()}</style>;
|
||||
return <style>{styleRules}</style>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -160,6 +160,7 @@ class ScreenSettingsModal extends React.Component<{}, {}> {
|
||||
if (currTheme == theme) {
|
||||
return;
|
||||
}
|
||||
GlobalModel.setTermThemeScope(this.screenId, `.main-content [data-screenid="${this.screenId}"]`);
|
||||
const prtn = GlobalCommandRunner.setScreenTermTheme(this.screenId, theme, false);
|
||||
commandRtnHandler(prtn, this.errorMessage);
|
||||
}
|
||||
@@ -175,13 +176,8 @@ class ScreenSettingsModal extends React.Component<{}, {}> {
|
||||
if (screen == null) {
|
||||
return null;
|
||||
}
|
||||
let color: string = null;
|
||||
let icon: string = null;
|
||||
let index: number = 0;
|
||||
const curRemote = GlobalModel.getRemote(GlobalModel.getActiveScreen().getCurRemoteInstance().remoteid);
|
||||
const termThemes = getTermThemes(GlobalModel.termThemes);
|
||||
const currTermTheme = GlobalModel.getTermTheme()[this.screenId] ?? termThemes[0].label;
|
||||
|
||||
return (
|
||||
<Modal className="screen-settings-modal">
|
||||
<Modal.Header onClose={this.closeModal} title={`Tab Settings (${screen.name.get()})`} />
|
||||
|
||||
@@ -99,7 +99,7 @@ class SessionSettingsModal extends React.Component<{}, {}> {
|
||||
if (this.session == null) {
|
||||
return null;
|
||||
}
|
||||
const termThemes = getTermThemes(GlobalModel.termThemes);
|
||||
const termThemes = getTermThemes(GlobalModel.termThemeOptions.get());
|
||||
const currTermTheme = GlobalModel.getTermTheme()[this.sessionId] ?? termThemes[0].label;
|
||||
|
||||
return (
|
||||
|
||||
@@ -184,17 +184,16 @@ class ScreenView extends React.Component<{ session: Session; screen: Screen }, {
|
||||
winWidth = screenWidth - realWidth + "px";
|
||||
sidebarWidth = realWidth - MagicLayout.ScreenSidebarWidthPadding + "px";
|
||||
}
|
||||
const termTheme = GlobalModel.getTermTheme();
|
||||
const termRenderVersion = GlobalModel.termRenderVersion.get();
|
||||
|
||||
return (
|
||||
<div className="screen-view" data-screenid={screen.screenId} ref={this.screenViewRef}>
|
||||
<TermStyleBlock
|
||||
<div className="screen-view" id={screen.screenId} data-screenid={screen.screenId} ref={this.screenViewRef}>
|
||||
{/* <TermStyleBlock
|
||||
scope="screen"
|
||||
termTheme={termTheme}
|
||||
themeSrcEl={this.screenViewRef.current}
|
||||
themeKey={screen.screenId}
|
||||
/>
|
||||
/> */}
|
||||
<ScreenWindowView
|
||||
key={screen.screenId + ":" + fontSize + ":" + dprStr + ":" + termRenderVersion}
|
||||
session={session}
|
||||
|
||||
@@ -140,6 +140,7 @@ class TabSettings extends React.Component<{ screen: Screen }, {}> {
|
||||
|
||||
@boundMethod
|
||||
handleChangeTermTheme(theme: string): void {
|
||||
console.log("theme", theme);
|
||||
const { screenId } = this.props.screen;
|
||||
const currTheme = GlobalModel.getTermTheme()[screenId];
|
||||
if (currTheme == theme) {
|
||||
@@ -152,7 +153,7 @@ class TabSettings extends React.Component<{ screen: Screen }, {}> {
|
||||
render() {
|
||||
const { screen } = this.props;
|
||||
const rptr = screen.curRemote.get();
|
||||
const termThemes = getTermThemes(GlobalModel.termThemes);
|
||||
const termThemes = getTermThemes(GlobalModel.termThemeOptions.get());
|
||||
const currTermTheme = GlobalModel.getTermTheme()[screen.screenId] ?? termThemes[0].label;
|
||||
return (
|
||||
<div className="newtab-container">
|
||||
@@ -228,25 +229,16 @@ class WorkspaceView extends React.Component<{}, {}> {
|
||||
const isHidden = GlobalModel.activeMainView.get() != "session";
|
||||
const mainSidebarModel = GlobalModel.mainSidebarModel;
|
||||
const showTabSettings = GlobalModel.tabSettingsOpen.get();
|
||||
const termTheme = GlobalModel.getTermTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={this.sessionRef}
|
||||
className={cn("mainview", "session-view", { "is-hidden": isHidden })}
|
||||
id={sessionId}
|
||||
data-sessionid={sessionId}
|
||||
style={{
|
||||
width: `${window.innerWidth - mainSidebarModel.getWidth()}px`,
|
||||
}}
|
||||
>
|
||||
<If condition={session != null}>
|
||||
<TermStyleBlock
|
||||
scope="session"
|
||||
themeSrcEl={this.sessionRef.current}
|
||||
themeKey={session.sessionId}
|
||||
termTheme={termTheme}
|
||||
/>
|
||||
</If>
|
||||
<If condition={!isHidden}>
|
||||
<SessionKeybindings key="keybindings"></SessionKeybindings>
|
||||
</If>
|
||||
|
||||
+16
-85
@@ -32,7 +32,7 @@ import { MainSidebarModel } from "./mainsidebar";
|
||||
import { RightSidebarModel } from "./rightsidebar";
|
||||
import { Screen } from "./screen";
|
||||
import { Cmd } from "./cmd";
|
||||
import { GlobalCommandRunner } from "./global";
|
||||
import { GlobalCommandRunner, GlobalModel } from "./global";
|
||||
import { clearMonoFontCache, getMonoFontSize } from "@/util/textmeasure";
|
||||
import type { TermWrap } from "@/plugins/terminal/term";
|
||||
import * as util from "@/util/util";
|
||||
@@ -135,21 +135,13 @@ class Model {
|
||||
renderVersion: OV<number> = mobx.observable.box(0, {
|
||||
name: "renderVersion",
|
||||
});
|
||||
|
||||
appUpdateStatus = mobx.observable.box(getApi().getAppUpdateStatus(), {
|
||||
name: "appUpdateStatus",
|
||||
});
|
||||
|
||||
termThemes: OMap<string, string> = mobx.observable.array([], {
|
||||
termThemeOptions: OV<TermThemeOptionsType> = mobx.observable.box(null, {
|
||||
name: "terminalThemes",
|
||||
deep: false,
|
||||
});
|
||||
termThemeSrcEls: OMap<string, HTMLElement> = mobx.observable.map(
|
||||
{},
|
||||
{
|
||||
name: "termThemeSrcEls",
|
||||
}
|
||||
);
|
||||
termRenderVersion: OV<number> = mobx.observable.box(0, {
|
||||
name: "termRenderVersion",
|
||||
});
|
||||
@@ -166,7 +158,6 @@ class Model {
|
||||
this.ws.reconnect();
|
||||
this.keybindManager = new KeybindManager(this);
|
||||
this.readConfigKeybindings();
|
||||
this.fetchTerminalThemes();
|
||||
this.initSystemKeybindings();
|
||||
this.initAppKeybindings();
|
||||
this.inputModel = new InputModel(this);
|
||||
@@ -217,16 +208,6 @@ class Model {
|
||||
};
|
||||
}
|
||||
|
||||
getThemeSrcElForScope() {
|
||||
const scopes = ["screen", "session", "main"];
|
||||
for (let scope of scopes) {
|
||||
if (this.termThemeSrcEls.get(scope)) {
|
||||
return this.termThemeSrcEls.get(scope);
|
||||
}
|
||||
}
|
||||
return document.documentElement;
|
||||
}
|
||||
|
||||
readConfigKeybindings() {
|
||||
const url = new URL(this.getBaseHostPort() + "/config/keybindings.json");
|
||||
let prtn = fetch(url, { method: "get", body: null, headers: this.getFetchHeaders() });
|
||||
@@ -248,35 +229,6 @@ 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);
|
||||
});
|
||||
}
|
||||
|
||||
getTermThemeJson(themeFileName: string) {
|
||||
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) => {
|
||||
return themeVars;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`error applying theme: ${themeFileName}`, error);
|
||||
});
|
||||
}
|
||||
|
||||
bumpTermRenderVersion() {
|
||||
mobx.action(() => {
|
||||
this.termRenderVersion.set(this.termRenderVersion.get() + 1);
|
||||
@@ -931,6 +883,17 @@ class Model {
|
||||
}
|
||||
}
|
||||
|
||||
setTermThemeOptions(termThemeOptions: TermThemeOptionsType) {
|
||||
mobx.action(() => {
|
||||
this.termThemeOptions.set(termThemeOptions);
|
||||
})();
|
||||
GlobalModel.bumpTermRenderVersion();
|
||||
}
|
||||
|
||||
getTermThemeOptions(): TermThemeOptionsType {
|
||||
return this.termThemeOptions.get();
|
||||
}
|
||||
|
||||
updateScreenStatusIndicators(screenStatusIndicators: ScreenStatusIndicatorUpdateType[]) {
|
||||
for (const update of screenStatusIndicators) {
|
||||
this.getScreenById_single(update.screenid)?.setStatusIndicator(update.status);
|
||||
@@ -1050,6 +1013,8 @@ class Model {
|
||||
} else if (update.userinputrequest != null) {
|
||||
const userInputRequest: UserInputRequest = update.userinputrequest;
|
||||
this.modalsModel.pushModal(appconst.USER_INPUT, userInputRequest);
|
||||
} else if (update.termthemeoptions != null) {
|
||||
this.setTermThemeOptions(update.termthemeoptions);
|
||||
} else if (update.sessiontombstone != null || update.screentombstone != null) {
|
||||
// nothing (ignore)
|
||||
} else {
|
||||
@@ -1096,7 +1061,7 @@ class Model {
|
||||
const activeScreen = this.getActiveScreen();
|
||||
if (activeScreen?.getCurRemoteInstance() != null) {
|
||||
setTimeout(() => {
|
||||
GlobalCommandRunner.syncShellState();
|
||||
// GlobalCommandRunner.syncShellState();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
@@ -1303,9 +1268,6 @@ 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);
|
||||
})();
|
||||
@@ -1326,37 +1288,6 @@ 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 (ttUpdated) {
|
||||
// this.bumpTermRenderVersion();
|
||||
// const el = document.documentElement;
|
||||
// const globaltt = newTermTheme["global"] ?? this.currGlobalTermTheme;
|
||||
// const reset = newTermTheme["global"] == null;
|
||||
// if (globaltt) {
|
||||
// const rtn = this.updateTermTheme(el, globaltt, reset);
|
||||
// rtn.then(() => {
|
||||
// if (ttUpdated) {
|
||||
// this.bumpTermRenderVersion();
|
||||
// }
|
||||
// });
|
||||
// this.currGlobalTermTheme = globaltt;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
termThemeUpdated(newTermTheme, oldTermTheme) {
|
||||
for (const key in oldTermTheme) {
|
||||
if (!(key in newTermTheme)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (const key in newTermTheme) {
|
||||
if (!oldTermTheme[key] || oldTermTheme[key] !== newTermTheme[key]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
submitCommandPacket(cmdPk: FeCmdPacketType, interactive: boolean): Promise<CommandRtnType> {
|
||||
|
||||
@@ -51,10 +51,11 @@ type TermWrapOpts = {
|
||||
onUpdateContentHeight: (termContext: RendererContext, height: number) => void;
|
||||
};
|
||||
|
||||
function getThemeFromCSSVars(themeSrcEl: HTMLElement): ITheme {
|
||||
function getThemeFromCSSVars(el: Element): ITheme {
|
||||
const theme: ITheme = {};
|
||||
const tse = themeSrcEl ?? document.documentElement;
|
||||
let rootStyle = getComputedStyle(tse);
|
||||
// console.log("el======", el);
|
||||
const rootStyle = getComputedStyle(el);
|
||||
// console.log("rootStyle======", rootStyle.getPropertyValue("--term-foreground"));
|
||||
theme.foreground = rootStyle.getPropertyValue("--term-foreground");
|
||||
theme.background = rootStyle.getPropertyValue("--term-background");
|
||||
theme.black = rootStyle.getPropertyValue("--term-black");
|
||||
@@ -131,8 +132,7 @@ class TermWrap {
|
||||
let cols = windowWidthToCols(opts.winSize.width, opts.fontSize);
|
||||
this.termSize = { rows: opts.termOpts.rows, cols: cols };
|
||||
}
|
||||
const themeSrcEl = GlobalModel.getThemeSrcElForScope();
|
||||
let theme = getThemeFromCSSVars(themeSrcEl);
|
||||
let theme = getThemeFromCSSVars(this.connectedElem);
|
||||
this.terminal = new Terminal({
|
||||
rows: this.termSize.rows,
|
||||
cols: this.termSize.cols,
|
||||
|
||||
Vendored
+8
-1
@@ -379,6 +379,13 @@ declare global {
|
||||
userinputrequest?: UserInputRequest;
|
||||
screentombstone?: any;
|
||||
sessiontombstone?: any;
|
||||
termthemeoptions?: TermThemeOptionsType;
|
||||
};
|
||||
|
||||
type TermThemeOptionsType = {
|
||||
[key: string]: {
|
||||
[innerKey: string]: string;
|
||||
};
|
||||
};
|
||||
|
||||
type HistoryViewDataType = {
|
||||
@@ -582,7 +589,7 @@ declare global {
|
||||
termfontsize: number;
|
||||
termfontfamily: string;
|
||||
theme: NativeThemeSource;
|
||||
termtheme: TermThemeType;
|
||||
termthemeconfig: TermThemeType;
|
||||
};
|
||||
|
||||
type ConfirmFlagsType = {
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
function getTermThemes(termThemes: string[], noneLabel = "Inherit"): DropdownItem[] {
|
||||
function getTermThemes(termThemeOptions: string[], noneLabel = "Inherit"): DropdownItem[] {
|
||||
if (!termThemeOptions) {
|
||||
return [];
|
||||
}
|
||||
const tt: DropdownItem[] = [];
|
||||
tt.push({
|
||||
label: noneLabel,
|
||||
value: null,
|
||||
});
|
||||
for (const themeName of termThemes) {
|
||||
for (const themeName of Object.keys(termThemeOptions)) {
|
||||
tt.push({
|
||||
label: themeName,
|
||||
value: themeName,
|
||||
|
||||
@@ -37,6 +37,7 @@ import (
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/wlog"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/bufferedpipe"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/cmdrunner"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/configstore"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/ephemeral"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/pcloud"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/promptenc"
|
||||
@@ -156,6 +157,9 @@ func HandleWs(w http.ResponseWriter, r *http.Request) {
|
||||
removeWSStateAfterTimeout(clientId, stateConnectTime, WSStateReconnectTime)
|
||||
}()
|
||||
log.Printf("WebSocket opened %s %s\n", state.ClientId, shell.RemoteAddr)
|
||||
|
||||
tt := configstore.MakeTermThemes(state)
|
||||
tt.LoadAndWatchThemes()
|
||||
state.RunWSRead()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
package configstore
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbase"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbus"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/scws"
|
||||
)
|
||||
|
||||
const (
|
||||
TermThemesTypeStr = "termthemeoptions"
|
||||
TermThemeDir = "config/terminal-themes/"
|
||||
)
|
||||
|
||||
type TermThemesType map[string]map[string]string
|
||||
|
||||
func (tt TermThemesType) GetType() string {
|
||||
return TermThemesTypeStr
|
||||
}
|
||||
|
||||
type TermThemes struct {
|
||||
Themes TermThemesType
|
||||
State *scws.WSState // Using WSState to manage WebSocket operations
|
||||
}
|
||||
|
||||
// Factory function to create a new TermThemes instance with WSState.
|
||||
func MakeTermThemes(state *scws.WSState) *TermThemes {
|
||||
return &TermThemes{
|
||||
Themes: make(TermThemesType),
|
||||
State: state,
|
||||
}
|
||||
}
|
||||
|
||||
// LoadAndWatchThemes initializes file scanning and sets up file watching.
|
||||
func (t *TermThemes) LoadAndWatchThemes() {
|
||||
dirPath := path.Join(scbase.GetWaveHomeDir(), TermThemeDir)
|
||||
if _, err := os.Stat(dirPath); errors.Is(err, os.ErrNotExist) {
|
||||
log.Printf("directory does not exist: %s", dirPath)
|
||||
return
|
||||
}
|
||||
|
||||
if err := t.scanDirAndUpdate(dirPath); err != nil {
|
||||
log.Printf("failed to scan directory and update themes: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.setupFileWatcher(dirPath)
|
||||
}
|
||||
|
||||
// scanDirAndUpdate scans the directory and updates themes.
|
||||
func (t *TermThemes) scanDirAndUpdate(dirPath string) error {
|
||||
log.Println("performing directory scan...")
|
||||
newThemes, err := t.scanDir(dirPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.Themes = newThemes
|
||||
return t.updateThemes()
|
||||
}
|
||||
|
||||
// scanDir reads all JSON files in the specified directory.
|
||||
func (t *TermThemes) scanDir(dirPath string) (TermThemesType, error) {
|
||||
newThemes := make(TermThemesType)
|
||||
|
||||
files, err := os.ReadDir(dirPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if filepath.Ext(file.Name()) == ".json" {
|
||||
filePath := filepath.Join(dirPath, file.Name())
|
||||
content, err := t.readFileContents(filePath)
|
||||
if err != nil {
|
||||
log.Printf("error reading file %s: %v", filePath, err)
|
||||
continue
|
||||
}
|
||||
newThemes[file.Name()] = content
|
||||
}
|
||||
}
|
||||
|
||||
return newThemes, nil
|
||||
}
|
||||
|
||||
// setupFileWatcher sets up a file system watcher on the given directory.
|
||||
func (t *TermThemes) setupFileWatcher(dirPath string) {
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
log.Println("error creating file watcher:", err)
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
defer func() {
|
||||
watcher.Close()
|
||||
log.Println("file watcher stopped.")
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
log.Printf("event: %s, Op: %v", event.Name, event.Op)
|
||||
t.handleFileEvent(event)
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
log.Println("watcher error:", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err := watcher.Add(dirPath); err != nil {
|
||||
log.Println("error adding directory to watcher:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// handleFileEvent handles file system events and triggers a directory rescan only on rename events.
|
||||
func (t *TermThemes) handleFileEvent(event fsnotify.Event) {
|
||||
filePath := event.Name
|
||||
fileName := filepath.Base(filePath)
|
||||
|
||||
// Normalize the file path for consistency across platforms
|
||||
normalizedPath := filepath.ToSlash(filePath)
|
||||
|
||||
switch event.Op {
|
||||
case fsnotify.Write, fsnotify.Create:
|
||||
log.Println("performing write or create event...")
|
||||
// For write and create events, update or add the file to the Themes map.
|
||||
content, err := t.readFileContents(normalizedPath)
|
||||
if err != nil {
|
||||
log.Printf("error reading file %s: %v", normalizedPath, err)
|
||||
return
|
||||
}
|
||||
t.Themes[fileName] = content
|
||||
t.updateThemes() // Update themes after adding or changing the file.
|
||||
|
||||
case fsnotify.Remove:
|
||||
log.Println("performing delete event...")
|
||||
// For remove events, delete the file from the Themes map.
|
||||
delete(t.Themes, fileName)
|
||||
t.updateThemes() // Update themes after removing the file.
|
||||
|
||||
case fsnotify.Rename:
|
||||
// Rename might affect file identity; rescan to ensure accuracy
|
||||
log.Printf("rename event detected, rescanning directory: %s", normalizedPath)
|
||||
if err := t.scanDirAndUpdate(path.Dir(normalizedPath)); err != nil {
|
||||
log.Printf("error rescanning directory after rename: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// readFileContents reads and unmarshals the JSON content from a file.
|
||||
func (t *TermThemes) readFileContents(filePath string) (map[string]string, error) {
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var content map[string]string
|
||||
if err := json.Unmarshal(data, &content); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// updateThemes sends an update of all themes via WebSocket.
|
||||
func (t *TermThemes) updateThemes() error {
|
||||
update := scbus.MakeUpdatePacket()
|
||||
update.AddUpdate(t.Themes)
|
||||
if err := t.State.WriteUpdate(update); err != nil {
|
||||
return fmt.Errorf("error sending updated themes via WebSocket: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user