Compare commits

..

5 Commits

Author SHA1 Message Date
Red Adaya 08f2da159e fix model type 2024-04-10 21:55:12 +08:00
Red Adaya 641c8da1c7 use Button element for close trigger 2024-04-10 16:16:07 +08:00
Red Adaya 85c49032f7 simplify logic 2024-04-10 15:59:01 +08:00
Red Adaya a4f405451c fix wrong prop 2024-04-10 15:49:25 +08:00
Red Adaya bfe9d8f9ac add connection label. add New Conection item 2024-04-10 15:48:26 +08:00
19 changed files with 217 additions and 359 deletions
-1
View File
@@ -13,7 +13,6 @@
<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>
+6 -24
View File
@@ -22,7 +22,6 @@ 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";
@@ -81,21 +80,6 @@ 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();
@@ -141,7 +125,6 @@ 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
@@ -170,13 +153,12 @@ class App extends React.Component<{}, {}> {
</div>
</If>
<div ref={this.mainContentRef} className="main-content">
<For index="idx" each="themeKey" of={Object.keys(termTheme)}>
<TermStyleBlock
key={themeKey}
themeName={termTheme[themeKey]}
selector={this.getSelector(themeKey)}
/>
</For>
<TermStyleBlock
scope="main"
termTheme={termTheme}
themeSrcEl={this.mainContentRef.current}
themeKey="main"
/>
<MainSideBar parentRef={this.mainContentRef} />
<ErrorBoundary>
<PluginsView />
+2 -1
View File
@@ -82,6 +82,7 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
if (currTheme == theme) {
return;
}
const prtn = GlobalCommandRunner.setMainTermTheme(theme, false);
commandRtnHandler(prtn, this.errorMessage);
}
@@ -206,7 +207,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.termThemeOptions.get(), "Wave Default");
const termThemes = getTermThemes(GlobalModel.termThemes, "Wave Default");
const currTermTheme = GlobalModel.getTermTheme()["main"] ?? termThemes[0].label;
return (
+4 -1
View File
@@ -63,11 +63,14 @@
display: flex;
min-width: 120px;
padding: 5px 8px;
justify-content: space-between;
align-items: center;
align-self: stretch;
border-radius: 6px;
i {
margin-right: 8px;
}
&-highlighted,
&:hover {
background: rgba(241, 246, 243, 0.08);
+8 -6
View File
@@ -163,7 +163,7 @@ class Dropdown extends React.Component<DropdownProps, DropdownState> {
if (isOpen) {
const option = options[highlightedIndex];
if (option) {
this.handleSelect(option.value, undefined);
this.handleSelect(option, undefined);
}
} else {
this.toggleDropdown();
@@ -185,18 +185,19 @@ class Dropdown extends React.Component<DropdownProps, DropdownState> {
handleKeyDown(event: React.KeyboardEvent) {}
@boundMethod
handleSelect(value: string, event?: React.MouseEvent | React.KeyboardEvent) {
handleSelect({ value, noop }: DropdownItem, event?: React.MouseEvent | React.KeyboardEvent) {
const { onChange } = this.props;
if (event) {
event.stopPropagation(); // This stops the event from bubbling up to the wrapper
}
if (!("value" in this.props)) {
this.setState({ internalValue: value });
}
onChange(value);
this.setState({ isOpen: false, isTouched: true });
this.unregisterKeybindings();
if (!("value" in this.props) && !noop) {
this.setState({ internalValue: value });
}
}
@boundMethod
@@ -245,10 +246,11 @@ class Dropdown extends React.Component<DropdownProps, DropdownState> {
className={cn("wave-dropdown-item unselectable", {
"wave-dropdown-item-highlighted": index === highlightedIndex,
})}
onClick={(e) => this.handleSelect(option.value, e)}
onClick={(e) => this.handleSelect(option, e)}
onMouseEnter={() => this.setState({ highlightedIndex: index })}
onMouseLeave={() => this.setState({ highlightedIndex: -1 })}
>
{option.icon && <span className="wave-dropdown-item-icon">{option.icon}</span>}
{option.label}
</div>
))}
+58 -36
View File
@@ -3,7 +3,9 @@
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",
@@ -31,21 +33,22 @@ const VALID_CSS_VARIABLES = [
];
@mobxReact.observer
class TermStyleBlock extends React.Component<{
themeName: string;
selector: string;
}> {
componentDidMount() {
GlobalModel.bumpTermRenderVersion();
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();
}
componentWillUnmount() {
GlobalModel.bumpTermRenderVersion();
}
componentDidUpdate(prevProps) {
if (prevProps.themeName !== this.props.themeName || prevProps.selector !== this.props.selector) {
GlobalModel.bumpTermRenderVersion();
componentDidUpdate(prevProps): void {
const { themeKey, termTheme } = this.props;
const currTheme = termTheme[themeKey];
if (themeKey !== prevProps.themeKey || currTheme !== this.theme) {
this.loadThemeStyles();
}
}
@@ -55,40 +58,59 @@ 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();
}
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(" ");
loadThemeStyles() {
const { themeKey, termTheme, scope } = this.props;
const currTheme = termTheme[themeKey];
if (!styleProperties) {
return null;
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();
}
return `${selector} { ${styleProperties} }`;
}
render() {
const styleRules = this.getStyleRules();
if (!styleRules) {
if (isBlank(this.styleRules.get())) {
return null;
}
return <style>{styleRules}</style>;
return <style>{this.styleRules.get()}</style>;
}
}
+5 -1
View File
@@ -160,7 +160,6 @@ 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);
}
@@ -176,8 +175,13 @@ 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()})`} />
+1 -1
View File
@@ -99,7 +99,7 @@ class SessionSettingsModal extends React.Component<{}, {}> {
if (this.session == null) {
return null;
}
const termThemes = getTermThemes(GlobalModel.termThemeOptions.get());
const termThemes = getTermThemes(GlobalModel.termThemes);
const currTermTheme = GlobalModel.getTermTheme()[this.sessionId] ?? termThemes[0].label;
return (
+16 -51
View File
@@ -5,12 +5,10 @@ import { boundMethod } from "autobind-decorator";
import { If, For } from "tsx-control-statements/components";
import cn from "classnames";
import { GlobalCommandRunner, GlobalModel, Screen } from "@/models";
import { Button, TextField, Dropdown } from "@/elements";
import { getRemoteStr, getRemoteStrWithAlias } from "@/common/prompt/prompt";
import { TextField, Dropdown } from "@/elements";
import { getRemoteStrWithAlias } from "@/common/prompt/prompt";
import * as util from "@/util/util";
import { TabIcon } from "@/elements/tabicon";
import { ReactComponent as EllipseIcon } from "@/assets/icons/ellipse.svg";
import { ReactComponent as Check12Icon } from "@/assets/icons/check12.svg";
import { ReactComponent as GlobeIcon } from "@/assets/icons/globe.svg";
import { ReactComponent as StatusCircleIcon } from "@/assets/icons/statuscircle.svg";
import * as appconst from "@/app/appconst";
@@ -18,52 +16,6 @@ import * as appconst from "@/app/appconst";
import "./screenview.less";
import "./tabs.less";
@mobxReact.observer
class NewTabSettings extends React.Component<{ screen: Screen }, {}> {
errorMessage: OV<string | null> = mobx.observable.box(null, { name: "NewTabSettings-errorMessage" });
constructor(props) {
super(props);
}
@boundMethod
clickNewConnection(): void {
GlobalModel.remotesModel.openAddModal({ remoteedit: true });
}
render() {
let { screen } = this.props;
let rptr = screen.curRemote.get();
return (
<div className="newtab-container">
<div className="newtab-section name-section">
<TabNameTextField screen={screen} errorMessage={this.errorMessage} />
</div>
<div className="newtab-spacer" />
<div className="newtab-section conn-section">
<div className="unselectable">
You're connected to <b>[{getRemoteStrWithAlias(rptr)}]</b>. Do you want to change it?
</div>
<div>
<TabRemoteSelector screen={screen} errorMessage={this.errorMessage} />
</div>
<div className="text-caption cr-help-text">
To change connection from the command line use `/connect [alias|user@host]`
</div>
</div>
<div className="newtab-spacer" />
<div className="newtab-section">
<TabIconSelector screen={screen} errorMessage={this.errorMessage} />
</div>
<div className="newtab-spacer" />
<div className="newtab-section">
<TabColorSelector screen={screen} errorMessage={this.errorMessage} />
</div>
</div>
);
}
}
@mobxReact.observer
class TabNameTextField extends React.Component<{ screen: Screen; errorMessage?: OV<string> }, {}> {
@boundMethod
@@ -163,6 +115,10 @@ class TabRemoteSelector extends React.Component<{ screen: Screen; errorMessage?:
@boundMethod
selectRemote(cname: string): void {
if (cname == null) {
GlobalModel.remotesModel.openAddModal({ remoteedit: true });
return;
}
mobx.action(() => {
this.selectedRemoteCN.set(cname);
})();
@@ -191,6 +147,14 @@ class TabRemoteSelector extends React.Component<{ screen: Screen; errorMessage?:
}
return a.remoteidx - b.remoteidx;
});
options.push({
label: "New Connection",
value: null,
icon: <i className="fa-sharp fa-solid fa-plus"></i>,
noop: true,
});
return options;
}
@@ -210,6 +174,7 @@ class TabRemoteSelector extends React.Component<{ screen: Screen; errorMessage?:
let curRemote = GlobalModel.getRemoteByName(selectedRemote);
return (
<Dropdown
label="Connection"
className="conn-dropdown"
options={this.getOptions()}
defaultValue={curRemote.remotecanonicalname}
@@ -227,4 +192,4 @@ class TabRemoteSelector extends React.Component<{ screen: Screen; errorMessage?:
}
}
export { NewTabSettings, TabColorSelector, TabIconSelector, TabNameTextField, TabRemoteSelector };
export { TabColorSelector, TabIconSelector, TabNameTextField, TabRemoteSelector };
+4 -3
View File
@@ -184,16 +184,17 @@ 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" id={screen.screenId} data-screenid={screen.screenId} ref={this.screenViewRef}>
{/* <TermStyleBlock
<div className="screen-view" 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}
+1 -1
View File
@@ -40,7 +40,7 @@
border-bottom: none;
}
.close-icon {
.close-button {
position: absolute;
right: 10px;
top: 10px;
+14 -7
View File
@@ -15,10 +15,9 @@ import { ScreenTabs } from "./screen/tabs";
import { ErrorBoundary } from "@/common/error/errorboundary";
import { boundMethod } from "autobind-decorator";
import type { Screen } from "@/models";
import { Button, TermStyleBlock } from "@/elements";
import { Button, Dropdown, TermStyleBlock } from "@/elements";
import { commandRtnHandler } from "@/util/util";
import { getTermThemes } from "@/util/themeutil";
import { Dropdown } from "@/elements/dropdown";
import { getRemoteStrWithAlias } from "@/common/prompt/prompt";
import { TabColorSelector, TabIconSelector, TabNameTextField, TabRemoteSelector } from "./screen/newtabsettings";
import * as util from "@/util/util";
@@ -140,7 +139,6 @@ 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) {
@@ -153,7 +151,7 @@ class TabSettings extends React.Component<{ screen: Screen }, {}> {
render() {
const { screen } = this.props;
const rptr = screen.curRemote.get();
const termThemes = getTermThemes(GlobalModel.termThemeOptions.get());
const termThemes = getTermThemes(GlobalModel.termThemes);
const currTermTheme = GlobalModel.getTermTheme()[screen.screenId] ?? termThemes[0].label;
return (
<div className="newtab-container">
@@ -229,25 +227,34 @@ 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>
<ScreenTabs key={"tabs-" + sessionId} session={session} />
<If condition={activeScreen != null}>
<div key="pulldown" className={cn("tab-settings-pulldown", { closed: !showTabSettings })}>
<button className="close-icon" onClick={this.toggleTabSettings}>
<Button className="close-button secondary ghost" onClick={this.toggleTabSettings}>
<i className="fa-solid fa-sharp fa-xmark-large" />
</button>
</Button>
<TabSettings key={activeScreen.screenId} screen={activeScreen} />
<If condition={showTabSettings && !isHidden}>
<TabSettingsPulldownKeybindings />
+85 -16
View File
@@ -32,7 +32,7 @@ import { MainSidebarModel } from "./mainsidebar";
import { RightSidebarModel } from "./rightsidebar";
import { Screen } from "./screen";
import { Cmd } from "./cmd";
import { GlobalCommandRunner, GlobalModel } from "./global";
import { GlobalCommandRunner } from "./global";
import { clearMonoFontCache, getMonoFontSize } from "@/util/textmeasure";
import type { TermWrap } from "@/plugins/terminal/term";
import * as util from "@/util/util";
@@ -135,13 +135,21 @@ class Model {
renderVersion: OV<number> = mobx.observable.box(0, {
name: "renderVersion",
});
appUpdateStatus = mobx.observable.box(getApi().getAppUpdateStatus(), {
name: "appUpdateStatus",
});
termThemeOptions: OV<TermThemeOptionsType> = mobx.observable.box(null, {
termThemes: OMap<string, string> = mobx.observable.array([], {
name: "terminalThemes",
deep: false,
});
termThemeSrcEls: OMap<string, HTMLElement> = mobx.observable.map(
{},
{
name: "termThemeSrcEls",
}
);
termRenderVersion: OV<number> = mobx.observable.box(0, {
name: "termRenderVersion",
});
@@ -158,6 +166,7 @@ class Model {
this.ws.reconnect();
this.keybindManager = new KeybindManager(this);
this.readConfigKeybindings();
this.fetchTerminalThemes();
this.initSystemKeybindings();
this.initAppKeybindings();
this.inputModel = new InputModel(this);
@@ -208,6 +217,16 @@ 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() });
@@ -229,6 +248,35 @@ 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);
@@ -883,17 +931,6 @@ 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);
@@ -1013,8 +1050,6 @@ 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 {
@@ -1061,7 +1096,7 @@ class Model {
const activeScreen = this.getActiveScreen();
if (activeScreen?.getCurRemoteInstance() != null) {
setTimeout(() => {
// GlobalCommandRunner.syncShellState();
GlobalCommandRunner.syncShellState();
}, 100);
}
}
@@ -1268,6 +1303,9 @@ class Model {
newTheme = appconst.DefaultTheme;
}
const themeUpdated = newTheme != this.getThemeSource();
const oldTermTheme = this.getTermTheme();
const newTermTheme = clientData?.feopts?.termtheme;
const ttUpdated = this.termThemeUpdated(newTermTheme, oldTermTheme);
mobx.action(() => {
this.clientData.set(clientData);
})();
@@ -1288,6 +1326,37 @@ 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> {
+5 -5
View File
@@ -51,11 +51,10 @@ type TermWrapOpts = {
onUpdateContentHeight: (termContext: RendererContext, height: number) => void;
};
function getThemeFromCSSVars(el: Element): ITheme {
function getThemeFromCSSVars(themeSrcEl: HTMLElement): ITheme {
const theme: ITheme = {};
// console.log("el======", el);
const rootStyle = getComputedStyle(el);
// console.log("rootStyle======", rootStyle.getPropertyValue("--term-foreground"));
const tse = themeSrcEl ?? document.documentElement;
let rootStyle = getComputedStyle(tse);
theme.foreground = rootStyle.getPropertyValue("--term-foreground");
theme.background = rootStyle.getPropertyValue("--term-background");
theme.black = rootStyle.getPropertyValue("--term-black");
@@ -132,7 +131,8 @@ class TermWrap {
let cols = windowWidthToCols(opts.winSize.width, opts.fontSize);
this.termSize = { rows: opts.termOpts.rows, cols: cols };
}
let theme = getThemeFromCSSVars(this.connectedElem);
const themeSrcEl = GlobalModel.getThemeSrcElForScope();
let theme = getThemeFromCSSVars(themeSrcEl);
this.terminal = new Terminal({
rows: this.termSize.rows,
cols: this.termSize.cols,
+4 -9
View File
@@ -308,7 +308,9 @@ declare global {
type DropdownItem = {
label: string;
value: string;
value?: string;
icon?: React.ReactNode;
noop?: boolean;
};
/**
@@ -379,13 +381,6 @@ declare global {
userinputrequest?: UserInputRequest;
screentombstone?: any;
sessiontombstone?: any;
termthemeoptions?: TermThemeOptionsType;
};
type TermThemeOptionsType = {
[key: string]: {
[innerKey: string]: string;
};
};
type HistoryViewDataType = {
@@ -589,7 +584,7 @@ declare global {
termfontsize: number;
termfontfamily: string;
theme: NativeThemeSource;
termthemeconfig: TermThemeType;
termtheme: TermThemeType;
};
type ConfirmFlagsType = {
+2 -3
View File
@@ -1,8 +1,7 @@
import * as React from "react";
import * as mobx from "mobx";
import * as electron from "electron";
import { parse } from "node:path";
import { v4 as uuidv4 } from "uuid";
import { Model } from "@/models";
import defaultKeybindingsFile from "../../assets/default-keybindings.json";
const defaultKeybindings: KeybindConfigArray = defaultKeybindingsFile;
@@ -52,7 +51,7 @@ class KeybindManager {
activeKeybindsVersion: OV<number>;
lastKeyData: { domain: string; keyPress: string };
constructor(GlobalModel: any) {
constructor(GlobalModel: Model) {
this.levelMap = new Map();
this.domainCallbacks = new Map();
this.levelArray = KeybindLevels;
+2 -5
View File
@@ -1,13 +1,10 @@
function getTermThemes(termThemeOptions: string[], noneLabel = "Inherit"): DropdownItem[] {
if (!termThemeOptions) {
return [];
}
function getTermThemes(termThemes: string[], noneLabel = "Inherit"): DropdownItem[] {
const tt: DropdownItem[] = [];
tt.push({
label: noneLabel,
value: null,
});
for (const themeName of Object.keys(termThemeOptions)) {
for (const themeName of termThemes) {
tt.push({
label: themeName,
value: themeName,
-4
View File
@@ -37,7 +37,6 @@ 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"
@@ -157,9 +156,6 @@ 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()
}
-184
View File
@@ -1,184 +0,0 @@
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
}