mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-19 21:11:32 +01:00
Simplified terminal theming (#570)
* save work * reusable StyleBlock component * StyleBlock in elements dir * root level * ability to inherit root styles * change prop from classname to selector * selector should always be :root * remove selector prop from StyleBlock * working * cleanup * loadThemeStyles doesn't have to be async * revert changes in tabs2.less * remove old implementation * cleanup * remove file from another branch * fix issue where line in history view doesn't reflect the terminal theme * add key and value validation * add label to tab settings terminal theme dropdown * save work * save work * save work * working * trigger componentDidUpdate when switching tabs and sessions * cleanup * save work * save work * use UpdatePacket for theme changes as well * make methods cohesive * use themes coming from backend * reload terminal when styel block is unmounted and mounted * fix validation * re-render terminal when theme is updated * remove test styles * cleanup * more cleanup * revert unneeded change * more cleanup * fix type * more cleanup * render style blocks in the header instead of body using portal * add ability to reuse and dispose TermThemes instance and file watcher * remove comment * minor change * separate filewatcher as singleton * do not render app when term theme style blocks aren't rendered first * only render main when termstyles have been rendered already * add comment * use DoUpdate to send themes to front-end * support to watch subdirectories * added support for watch subdirectories * make watcher more flexible so it can be closed anywhere * cleanup * undo the app/main split * use TermThemesType in creating initial value for Themes field * simplify code * fix issue where dropdown label doesn't float when the theme selected is Inherit * remove unsed var * start watcher in main, merge themes (don't overwrite) on event. * ensure terminal-themes directory is created on startup * ah, wait for termThemes to be set (the connect packet needs to have been processed to proceed with rendering)
This commit is contained in:
parent
8f93b3e263
commit
50203a6934
@ -4,10 +4,10 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as mobxReact from "mobx-react";
|
import * as mobxReact from "mobx-react";
|
||||||
import * as mobx from "mobx";
|
import * as mobx from "mobx";
|
||||||
|
import cn from "classnames";
|
||||||
|
|
||||||
import { boundMethod } from "autobind-decorator";
|
import { boundMethod } from "autobind-decorator";
|
||||||
import { If } from "tsx-control-statements/components";
|
import { If } from "tsx-control-statements/components";
|
||||||
import dayjs from "dayjs";
|
|
||||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
|
||||||
import { 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";
|
||||||
@ -22,15 +22,15 @@ import { DisconnectedModal, ClientStopModal } from "@/modals";
|
|||||||
import { ModalsProvider } from "@/modals/provider";
|
import { ModalsProvider } from "@/modals/provider";
|
||||||
import { Button } from "@/elements";
|
import { Button } from "@/elements";
|
||||||
import { ErrorBoundary } from "@/common/error/errorboundary";
|
import { ErrorBoundary } from "@/common/error/errorboundary";
|
||||||
import cn from "classnames";
|
import { TermStyleList } from "@/elements";
|
||||||
import "./app.less";
|
|
||||||
|
|
||||||
dayjs.extend(localizedFormat);
|
import "./app.less";
|
||||||
|
|
||||||
@mobxReact.observer
|
@mobxReact.observer
|
||||||
class App extends React.Component<{}, {}> {
|
class App extends React.Component<{}, {}> {
|
||||||
dcWait: OV<boolean> = mobx.observable.box(false, { name: "dcWait" });
|
dcWait: OV<boolean> = mobx.observable.box(false, { name: "dcWait" });
|
||||||
mainContentRef: React.RefObject<HTMLDivElement> = React.createRef();
|
mainContentRef: React.RefObject<HTMLDivElement> = React.createRef();
|
||||||
|
termThemesLoaded: OV<boolean> = mobx.observable.box(false, { name: "termThemesLoaded" });
|
||||||
|
|
||||||
constructor(props: {}) {
|
constructor(props: {}) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -80,6 +80,13 @@ class App extends React.Component<{}, {}> {
|
|||||||
rightSidebarModel.saveState(width, false);
|
rightSidebarModel.saveState(width, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleTermThemesRendered() {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.termThemesLoaded.set(true);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const remotesModel = GlobalModel.remotesModel;
|
const remotesModel = GlobalModel.remotesModel;
|
||||||
const disconnected = !GlobalModel.ws.open.get() || !GlobalModel.waveSrvRunning.get();
|
const disconnected = !GlobalModel.ws.open.get() || !GlobalModel.waveSrvRunning.get();
|
||||||
@ -90,7 +97,8 @@ class App extends React.Component<{}, {}> {
|
|||||||
|
|
||||||
// Previously, this is done in sidebar.tsx but it causes flicker when clientData is null cos screen-view shifts around.
|
// Previously, this is done in sidebar.tsx but it causes flicker when clientData is null cos screen-view shifts around.
|
||||||
// Doing it here fixes the flicker cos app is not rendered until clientData is populated.
|
// Doing it here fixes the flicker cos app is not rendered until clientData is populated.
|
||||||
if (clientData == null) {
|
// wait for termThemes as well (this actually means that the "connect" packet has been received)
|
||||||
|
if (clientData == null || GlobalModel.termThemes.get() == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,23 +126,31 @@ class App extends React.Component<{}, {}> {
|
|||||||
if (dcWait) {
|
if (dcWait) {
|
||||||
setTimeout(() => this.updateDcWait(false), 0);
|
setTimeout(() => this.updateDcWait(false), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// used to force a full reload of the application
|
// used to force a full reload of the application
|
||||||
const renderVersion = GlobalModel.renderVersion.get();
|
const renderVersion = GlobalModel.renderVersion.get();
|
||||||
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.isDarkTheme.get() ? "is-dark" : "is-light";
|
const lightDarkClass = GlobalModel.isDarkTheme.get() ? "is-dark" : "is-light";
|
||||||
return (
|
const mainClassName = cn(
|
||||||
<div
|
|
||||||
key={"version-" + renderVersion}
|
|
||||||
id="main"
|
|
||||||
className={cn(
|
|
||||||
"platform-" + platform,
|
"platform-" + platform,
|
||||||
{ "mainsidebar-collapsed": mainSidebarCollapsed, "rightsidebar-collapsed": rightSidebarCollapsed },
|
{
|
||||||
|
"mainsidebar-collapsed": mainSidebarCollapsed,
|
||||||
|
"rightsidebar-collapsed": rightSidebarCollapsed,
|
||||||
|
},
|
||||||
lightDarkClass
|
lightDarkClass
|
||||||
)}
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TermStyleList onRendered={this.handleTermThemesRendered} />
|
||||||
|
<div
|
||||||
|
key={`version- + ${renderVersion}`}
|
||||||
|
id="main"
|
||||||
|
className={mainClassName}
|
||||||
onContextMenu={this.handleContextMenu}
|
onContextMenu={this.handleContextMenu}
|
||||||
>
|
>
|
||||||
|
<If condition={this.termThemesLoaded.get()}>
|
||||||
<If condition={mainSidebarCollapsed}>
|
<If condition={mainSidebarCollapsed}>
|
||||||
<div key="logo-button" className="logo-button-container">
|
<div key="logo-button" className="logo-button-container">
|
||||||
<div className="logo-button-spacer" />
|
<div className="logo-button-spacer" />
|
||||||
@ -145,7 +161,10 @@ class App extends React.Component<{}, {}> {
|
|||||||
</If>
|
</If>
|
||||||
<If condition={GlobalModel.isDev && rightSidebarCollapsed && activeMainView == "session"}>
|
<If condition={GlobalModel.isDev && rightSidebarCollapsed && activeMainView == "session"}>
|
||||||
<div className="right-sidebar-triggers">
|
<div className="right-sidebar-triggers">
|
||||||
<Button className="secondary ghost right-sidebar-trigger" onClick={this.openRightSidebar}>
|
<Button
|
||||||
|
className="secondary ghost right-sidebar-trigger"
|
||||||
|
onClick={this.openRightSidebar}
|
||||||
|
>
|
||||||
<i className="fa-sharp fa-solid fa-sidebar-flip"></i>
|
<i className="fa-sharp fa-solid fa-sidebar-flip"></i>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -163,7 +182,9 @@ class App extends React.Component<{}, {}> {
|
|||||||
<RightSideBar parentRef={this.mainContentRef} />
|
<RightSideBar parentRef={this.mainContentRef} />
|
||||||
</div>
|
</div>
|
||||||
<ModalsProvider />
|
<ModalsProvider />
|
||||||
|
</If>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,14 +76,13 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
|
|||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
handleChangeTermTheme(theme: string): void {
|
handleChangeTermTheme(theme: string): void {
|
||||||
// For global terminal theme, the key is global, otherwise it's either
|
// For root terminal theme, the key is root, otherwise it's either
|
||||||
// sessionId or screenId.
|
// sessionId or screenId.
|
||||||
const currTheme = GlobalModel.getTermTheme()["global"];
|
const currTheme = GlobalModel.getTermThemeSettings()["root"];
|
||||||
if (currTheme == theme) {
|
if (currTheme == theme) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const prtn = GlobalCommandRunner.setRootTermTheme(theme, false);
|
||||||
const prtn = GlobalCommandRunner.setGlobalTermTheme(theme, false);
|
|
||||||
commandRtnHandler(prtn, this.errorMessage);
|
commandRtnHandler(prtn, this.errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,8 +206,8 @@ 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.getThemeSource();
|
const curTheme = GlobalModel.getThemeSource();
|
||||||
const termThemes = getTermThemes(GlobalModel.termThemes, "Wave Default");
|
const termThemes = getTermThemes(GlobalModel.termThemes.get(), "Wave Default");
|
||||||
const currTermTheme = GlobalModel.getTermTheme()["global"] ?? termThemes[0].label;
|
const currTermTheme = GlobalModel.getTermThemeSettings()["root"] ?? termThemes[0].label;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainView className="clientsettings-view" title="Client Settings" onClose={this.handleClose}>
|
<MainView className="clientsettings-view" title="Client Settings" onClose={this.handleClose}>
|
||||||
|
@ -18,4 +18,5 @@ export { Toggle } from "./toggle";
|
|||||||
export { Tooltip } from "./tooltip";
|
export { Tooltip } from "./tooltip";
|
||||||
export { TabIcon } from "./tabicon";
|
export { TabIcon } from "./tabicon";
|
||||||
export { DatePicker } from "./datepicker";
|
export { DatePicker } from "./datepicker";
|
||||||
|
export { TermStyleList } from "./termstyle";
|
||||||
export { CopyButton } from "./copybutton";
|
export { CopyButton } from "./copybutton";
|
||||||
|
133
src/app/common/elements/termstyle.tsx
Normal file
133
src/app/common/elements/termstyle.tsx
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as mobxReact from "mobx-react";
|
||||||
|
import { GlobalModel } from "@/models";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import { For } from "tsx-control-statements/components";
|
||||||
|
import * as mobx from "mobx";
|
||||||
|
|
||||||
|
const VALID_CSS_VARIABLES = [
|
||||||
|
"--term-black",
|
||||||
|
"--term-red",
|
||||||
|
"--term-green",
|
||||||
|
"--term-yellow",
|
||||||
|
"--term-blue",
|
||||||
|
"--term-magenta",
|
||||||
|
"--term-cyan",
|
||||||
|
"--term-white",
|
||||||
|
"--term-bright-black",
|
||||||
|
"--term-bright-red",
|
||||||
|
"--term-bright-green",
|
||||||
|
"--term-bright-yellow",
|
||||||
|
"--term-bright-blue",
|
||||||
|
"--term-bright-magenta",
|
||||||
|
"--term-bright-cyan",
|
||||||
|
"--term-bright-white",
|
||||||
|
"--term-gray",
|
||||||
|
"--term-cmdtext",
|
||||||
|
"--term-foreground",
|
||||||
|
"--term-background",
|
||||||
|
"--term-selection-background",
|
||||||
|
"--term-cursor-accent",
|
||||||
|
];
|
||||||
|
|
||||||
|
@mobxReact.observer
|
||||||
|
class TermStyle extends React.Component<{
|
||||||
|
themeName: string;
|
||||||
|
selector: string;
|
||||||
|
}> {
|
||||||
|
componentDidMount() {
|
||||||
|
GlobalModel.bumpTermRenderVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
GlobalModel.bumpTermRenderVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (prevProps.themeName !== this.props.themeName || prevProps.selector !== this.props.selector) {
|
||||||
|
GlobalModel.bumpTermRenderVersion();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isValidCSSColor(color) {
|
||||||
|
const element = document.createElement("div");
|
||||||
|
element.style.color = color;
|
||||||
|
return element.style.color !== "";
|
||||||
|
}
|
||||||
|
|
||||||
|
camelCaseToKebabCase(str) {
|
||||||
|
return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
getStyleRules() {
|
||||||
|
const { selector, themeName } = this.props;
|
||||||
|
const termThemeOptions = GlobalModel.getTermThemes();
|
||||||
|
if (!(themeName in 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() {
|
||||||
|
const styleRules = this.getStyleRules();
|
||||||
|
if (!styleRules) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ReactDOM.createPortal(<style>{styleRules}</style>, document.head);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mobxReact.observer
|
||||||
|
class TermStyleList extends React.Component<{ onRendered: () => void }, {}> {
|
||||||
|
componentDidMount(): void {
|
||||||
|
this.props.onRendered();
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelector(themeKey: string) {
|
||||||
|
const sessions = GlobalModel.getSessionNames();
|
||||||
|
const screens = GlobalModel.getScreenNames();
|
||||||
|
|
||||||
|
if (themeKey === "root") {
|
||||||
|
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 termTheme = GlobalModel.getTermThemeSettings();
|
||||||
|
const themeKey = null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<For index="idx" each="themeKey" of={Object.keys(termTheme)}>
|
||||||
|
<TermStyle key={themeKey} themeName={termTheme[themeKey]} selector={this.getSelector(themeKey)} />
|
||||||
|
</For>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { TermStyleList };
|
@ -156,7 +156,7 @@ class ScreenSettingsModal extends React.Component<{}, {}> {
|
|||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
handleChangeTermTheme(theme: string): void {
|
handleChangeTermTheme(theme: string): void {
|
||||||
const currTheme = GlobalModel.getTermTheme()[this.screenId];
|
const currTheme = GlobalModel.getTermThemeSettings()[this.screenId];
|
||||||
if (currTheme == theme) {
|
if (currTheme == theme) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -175,13 +175,8 @@ class ScreenSettingsModal extends React.Component<{}, {}> {
|
|||||||
if (screen == null) {
|
if (screen == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
let color: string = null;
|
const termThemes = getTermThemes(GlobalModel.termThemes.get());
|
||||||
let icon: string = null;
|
const currTermTheme = GlobalModel.getTermThemeSettings()[this.screenId] ?? termThemes[0].label;
|
||||||
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 (
|
return (
|
||||||
<Modal className="screen-settings-modal">
|
<Modal className="screen-settings-modal">
|
||||||
<Modal.Header onClose={this.closeModal} title={`Tab Settings (${screen.name.get()})`} />
|
<Modal.Header onClose={this.closeModal} title={`Tab Settings (${screen.name.get()})`} />
|
||||||
|
@ -80,7 +80,7 @@ class SessionSettingsModal extends React.Component<{}, {}> {
|
|||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
handleChangeTermTheme(theme: string): void {
|
handleChangeTermTheme(theme: string): void {
|
||||||
const currTheme = GlobalModel.getTermTheme()[this.sessionId];
|
const currTheme = GlobalModel.getTermThemeSettings()[this.sessionId];
|
||||||
if (currTheme == theme) {
|
if (currTheme == theme) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -99,8 +99,8 @@ class SessionSettingsModal extends React.Component<{}, {}> {
|
|||||||
if (this.session == null) {
|
if (this.session == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const termThemes = getTermThemes(GlobalModel.termThemes);
|
const termThemes = getTermThemes(GlobalModel.termThemes.get());
|
||||||
const currTermTheme = GlobalModel.getTermTheme()[this.sessionId] ?? termThemes[0].label;
|
const currTermTheme = GlobalModel.getTermThemeSettings()[this.sessionId] ?? termThemes[0].label;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal className="session-settings-modal">
|
<Modal className="session-settings-modal">
|
||||||
|
@ -649,11 +649,6 @@ class LineContainer extends React.Component<{ historyId: string; width: number }
|
|||||||
this.line = hvm.getLineById(this.historyItem.lineid);
|
this.line = hvm.getLineById(this.historyItem.lineid);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount(): void {
|
|
||||||
GlobalModel.bumpTermRenderVersion();
|
|
||||||
GlobalModel.termThemeSrcEl.set(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
handleHeightChange(lineNum: number, newHeight: number, oldHeight: number): void {
|
handleHeightChange(lineNum: number, newHeight: number, oldHeight: number): void {
|
||||||
return;
|
return;
|
||||||
|
@ -184,10 +184,12 @@ class ScreenView extends React.Component<{ session: Session; screen: Screen }, {
|
|||||||
winWidth = screenWidth - realWidth + "px";
|
winWidth = screenWidth - realWidth + "px";
|
||||||
sidebarWidth = realWidth - MagicLayout.ScreenSidebarWidthPadding + "px";
|
sidebarWidth = realWidth - MagicLayout.ScreenSidebarWidthPadding + "px";
|
||||||
}
|
}
|
||||||
|
const termRenderVersion = GlobalModel.termRenderVersion.get();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="screen-view" data-screenid={screen.screenId} ref={this.screenViewRef}>
|
<div className="screen-view" id={screen.screenId} data-screenid={screen.screenId} ref={this.screenViewRef}>
|
||||||
<ScreenWindowView
|
<ScreenWindowView
|
||||||
key={screen.screenId + ":" + fontSize + ":" + dprStr}
|
key={screen.screenId + ":" + fontSize + ":" + dprStr + ":" + termRenderVersion}
|
||||||
session={session}
|
session={session}
|
||||||
screen={screen}
|
screen={screen}
|
||||||
width={winWidth}
|
width={winWidth}
|
||||||
|
@ -144,7 +144,7 @@ class TabSettings extends React.Component<{ screen: Screen }, {}> {
|
|||||||
@boundMethod
|
@boundMethod
|
||||||
handleChangeTermTheme(theme: string): void {
|
handleChangeTermTheme(theme: string): void {
|
||||||
const { screenId } = this.props.screen;
|
const { screenId } = this.props.screen;
|
||||||
const currTheme = GlobalModel.getTermTheme()[screenId];
|
const currTheme = GlobalModel.getTermThemeSettings()[screenId];
|
||||||
if (currTheme == theme) {
|
if (currTheme == theme) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -155,8 +155,8 @@ class TabSettings extends React.Component<{ screen: Screen }, {}> {
|
|||||||
render() {
|
render() {
|
||||||
const { screen } = this.props;
|
const { screen } = this.props;
|
||||||
const rptr = screen.curRemote.get();
|
const rptr = screen.curRemote.get();
|
||||||
const termThemes = getTermThemes(GlobalModel.termThemes);
|
const termThemes = getTermThemes(GlobalModel.termThemes.get());
|
||||||
const currTermTheme = GlobalModel.getTermTheme()[screen.screenId] ?? termThemes[0].label;
|
const currTermTheme = GlobalModel.getTermThemeSettings()[screen.screenId] ?? termThemes[0].label;
|
||||||
return (
|
return (
|
||||||
<div className="newtab-container">
|
<div className="newtab-container">
|
||||||
<div className="newtab-section name-section">
|
<div className="newtab-section name-section">
|
||||||
@ -212,59 +212,6 @@ class TabSettings extends React.Component<{ screen: Screen }, {}> {
|
|||||||
@mobxReact.observer
|
@mobxReact.observer
|
||||||
class WorkspaceView extends React.Component<{}, {}> {
|
class WorkspaceView extends React.Component<{}, {}> {
|
||||||
sessionRef = React.createRef<HTMLDivElement>();
|
sessionRef = React.createRef<HTMLDivElement>();
|
||||||
theme: string;
|
|
||||||
themeReactionDisposer: mobx.IReactionDisposer;
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.setupThemeReaction();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
this.setupThemeReaction();
|
|
||||||
}
|
|
||||||
|
|
||||||
setupThemeReaction() {
|
|
||||||
if (this.themeReactionDisposer) {
|
|
||||||
this.themeReactionDisposer();
|
|
||||||
}
|
|
||||||
|
|
||||||
// This handles session and screen-level terminal theming.
|
|
||||||
// Ideally, screen-level theming should be handled in the inner-level component, but
|
|
||||||
// the frequent mounting and unmounting of the screen view make it really difficult to work.
|
|
||||||
this.themeReactionDisposer = mobx.reaction(
|
|
||||||
() => {
|
|
||||||
return {
|
|
||||||
termTheme: GlobalModel.getTermTheme(),
|
|
||||||
session: GlobalModel.getActiveSession(),
|
|
||||||
screen: GlobalModel.getActiveScreen(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
({ termTheme, session, screen }) => {
|
|
||||||
let currTheme = termTheme[session.sessionId];
|
|
||||||
if (termTheme[screen.screenId]) {
|
|
||||||
currTheme = termTheme[screen.screenId];
|
|
||||||
}
|
|
||||||
if (session && currTheme !== this.theme && this.sessionRef.current) {
|
|
||||||
const reset = currTheme == null;
|
|
||||||
const theme = currTheme ?? this.theme;
|
|
||||||
const themeSrcEl = reset ? null : this.sessionRef.current;
|
|
||||||
const rtn = GlobalModel.updateTermTheme(this.sessionRef.current, theme, reset);
|
|
||||||
rtn.then(() => {
|
|
||||||
GlobalModel.termThemeSrcEl.set(themeSrcEl);
|
|
||||||
}).then(() => {
|
|
||||||
GlobalModel.bumpTermRenderVersion();
|
|
||||||
});
|
|
||||||
this.theme = currTheme;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this.themeReactionDisposer) {
|
|
||||||
this.themeReactionDisposer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
toggleTabSettings() {
|
toggleTabSettings() {
|
||||||
@ -281,15 +228,14 @@ class WorkspaceView extends React.Component<{}, {}> {
|
|||||||
sessionId = session.sessionId;
|
sessionId = session.sessionId;
|
||||||
activeScreen = session.getActiveScreen();
|
activeScreen = session.getActiveScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
const isHidden = GlobalModel.activeMainView.get() != "session";
|
const isHidden = GlobalModel.activeMainView.get() != "session";
|
||||||
const mainSidebarModel = GlobalModel.mainSidebarModel;
|
const mainSidebarModel = GlobalModel.mainSidebarModel;
|
||||||
const termRenderVersion = GlobalModel.termRenderVersion.get();
|
|
||||||
const showTabSettings = GlobalModel.tabSettingsOpen.get();
|
const showTabSettings = GlobalModel.tabSettingsOpen.get();
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={this.sessionRef}
|
ref={this.sessionRef}
|
||||||
className={cn("mainview", "session-view", { "is-hidden": isHidden })}
|
className={cn("mainview", "session-view", { "is-hidden": isHidden })}
|
||||||
|
id={sessionId}
|
||||||
data-sessionid={sessionId}
|
data-sessionid={sessionId}
|
||||||
style={{
|
style={{
|
||||||
width: `${window.innerWidth - mainSidebarModel.getWidth()}px`,
|
width: `${window.innerWidth - mainSidebarModel.getWidth()}px`,
|
||||||
@ -311,11 +257,7 @@ class WorkspaceView extends React.Component<{}, {}> {
|
|||||||
</div>
|
</div>
|
||||||
</If>
|
</If>
|
||||||
<ErrorBoundary key="eb">
|
<ErrorBoundary key="eb">
|
||||||
<ScreenView
|
<ScreenView key={`screenview-${sessionId}`} session={session} screen={activeScreen} />
|
||||||
key={`screenview-${sessionId}-${termRenderVersion}`}
|
|
||||||
session={session}
|
|
||||||
screen={activeScreen}
|
|
||||||
/>
|
|
||||||
<If condition={activeScreen != null}>
|
<If condition={activeScreen != null}>
|
||||||
<CmdInput key={"cmdinput-" + sessionId} />
|
<CmdInput key={"cmdinput-" + sessionId} />
|
||||||
</If>
|
</If>
|
||||||
|
@ -381,29 +381,40 @@ class CommandRunner {
|
|||||||
return GlobalModel.submitCommand("client", "set", null, kwargs, interactive);
|
return GlobalModel.submitCommand("client", "set", null, kwargs, interactive);
|
||||||
}
|
}
|
||||||
|
|
||||||
setGlobalTermTheme(theme: string, interactive: boolean): Promise<CommandRtnType> {
|
setRootTermTheme(theme: string, interactive: boolean): Promise<CommandRtnType> {
|
||||||
|
let ftheme = theme;
|
||||||
|
if (ftheme == "inherit") {
|
||||||
|
ftheme = "";
|
||||||
|
}
|
||||||
let kwargs = {
|
let kwargs = {
|
||||||
nohist: "1",
|
nohist: "1",
|
||||||
termtheme: theme,
|
termtheme: ftheme,
|
||||||
};
|
};
|
||||||
return GlobalModel.submitCommand("client", "set", null, kwargs, interactive);
|
return GlobalModel.submitCommand("client", "set", null, kwargs, interactive);
|
||||||
}
|
}
|
||||||
|
|
||||||
setSessionTermTheme(sessionId: string, name: string, interactive: boolean): Promise<CommandRtnType> {
|
setSessionTermTheme(sessionId: string, name: string, interactive: boolean): Promise<CommandRtnType> {
|
||||||
console.log("setSessionTermTheme-------");
|
let fname = name;
|
||||||
|
if (name == "inherit") {
|
||||||
|
fname = "";
|
||||||
|
}
|
||||||
let kwargs = {
|
let kwargs = {
|
||||||
nohist: "1",
|
nohist: "1",
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
name: name,
|
name: fname,
|
||||||
};
|
};
|
||||||
return GlobalModel.submitCommand("session", "termtheme", null, kwargs, interactive);
|
return GlobalModel.submitCommand("session", "termtheme", null, kwargs, interactive);
|
||||||
}
|
}
|
||||||
|
|
||||||
setScreenTermTheme(screenId: string, name: string, interactive: boolean): Promise<CommandRtnType> {
|
setScreenTermTheme(screenId: string, name: string, interactive: boolean): Promise<CommandRtnType> {
|
||||||
|
let fname = name;
|
||||||
|
if (name == "inherit") {
|
||||||
|
fname = "";
|
||||||
|
}
|
||||||
let kwargs = {
|
let kwargs = {
|
||||||
nohist: "1",
|
nohist: "1",
|
||||||
id: screenId,
|
id: screenId,
|
||||||
name: name,
|
name: fname,
|
||||||
};
|
};
|
||||||
return GlobalModel.submitCommand("screen", "termtheme", null, kwargs, interactive);
|
return GlobalModel.submitCommand("screen", "termtheme", null, kwargs, interactive);
|
||||||
}
|
}
|
||||||
|
@ -137,22 +137,16 @@ class Model {
|
|||||||
renderVersion: OV<number> = mobx.observable.box(0, {
|
renderVersion: OV<number> = mobx.observable.box(0, {
|
||||||
name: "renderVersion",
|
name: "renderVersion",
|
||||||
});
|
});
|
||||||
|
|
||||||
appUpdateStatus = mobx.observable.box(getApi().getAppUpdateStatus(), {
|
appUpdateStatus = mobx.observable.box(getApi().getAppUpdateStatus(), {
|
||||||
name: "appUpdateStatus",
|
name: "appUpdateStatus",
|
||||||
});
|
});
|
||||||
|
termThemes: OV<TermThemesType> = mobx.observable.box(null, {
|
||||||
termThemes: OMap<string, string> = mobx.observable.array([], {
|
|
||||||
name: "terminalThemes",
|
name: "terminalThemes",
|
||||||
deep: false,
|
deep: false,
|
||||||
});
|
});
|
||||||
termThemeSrcEl: OV<HTMLElement> = mobx.observable.box(null, {
|
|
||||||
name: "termThemeSrcEl",
|
|
||||||
});
|
|
||||||
termRenderVersion: OV<number> = mobx.observable.box(0, {
|
termRenderVersion: OV<number> = mobx.observable.box(0, {
|
||||||
name: "termRenderVersion",
|
name: "termRenderVersion",
|
||||||
});
|
});
|
||||||
currGlobalTermTheme: string;
|
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
this.clientId = getApi().getId();
|
this.clientId = getApi().getId();
|
||||||
@ -166,7 +160,6 @@ class Model {
|
|||||||
this.ws.reconnect();
|
this.ws.reconnect();
|
||||||
this.keybindManager = new KeybindManager(this);
|
this.keybindManager = new KeybindManager(this);
|
||||||
this.readConfigKeybindings();
|
this.readConfigKeybindings();
|
||||||
this.fetchTerminalThemes();
|
|
||||||
this.initSystemKeybindings();
|
this.initSystemKeybindings();
|
||||||
this.initAppKeybindings();
|
this.initAppKeybindings();
|
||||||
this.inputModel = new InputModel(this);
|
this.inputModel = new InputModel(this);
|
||||||
@ -239,42 +232,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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTermTheme(element: HTMLElement, themeFileName: string, reset: boolean) {
|
|
||||||
const url = new URL(this.getBaseHostPort() + `/config/terminal-themes/${themeFileName}.json`);
|
|
||||||
return fetch(url, { method: "get", body: null, headers: this.getFetchHeaders() })
|
|
||||||
.then((resp) => resp.json())
|
|
||||||
.then((themeVars: TermThemeType) => {
|
|
||||||
Object.keys(themeVars).forEach((key) => {
|
|
||||||
if (reset) {
|
|
||||||
this.resetStyleVar(element, `--term-${key}`);
|
|
||||||
} else {
|
|
||||||
this.resetStyleVar(element, `--term-${key}`);
|
|
||||||
this.setStyleVar(element, `--term-${key}`, themeVars[key]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(`error applying theme: ${themeFileName}`, error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bumpTermRenderVersion() {
|
bumpTermRenderVersion() {
|
||||||
mobx.action(() => {
|
mobx.action(() => {
|
||||||
this.termRenderVersion.set(this.termRenderVersion.get() + 1);
|
this.termRenderVersion.set(this.termRenderVersion.get() + 1);
|
||||||
@ -486,10 +443,10 @@ class Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getTermTheme(): TermThemeType {
|
getTermThemeSettings(): TermThemeSettingsType {
|
||||||
let cdata = this.clientData.get();
|
let cdata = this.clientData.get();
|
||||||
if (cdata?.feopts?.termtheme) {
|
if (cdata?.feopts?.termthemesettings) {
|
||||||
return mobx.toJS(cdata.feopts.termtheme);
|
return mobx.toJS(cdata.feopts.termthemesettings);
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@ -937,6 +894,27 @@ class Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mergeTermThemes(termThemes: TermThemesType) {
|
||||||
|
mobx.action(() => {
|
||||||
|
if (this.termThemes.get() == null) {
|
||||||
|
this.termThemes.set(termThemes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const [themeName, theme] of Object.entries(termThemes)) {
|
||||||
|
if (theme == null) {
|
||||||
|
delete this.termThemes.get()[themeName];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this.termThemes.get()[themeName] = theme;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
this.bumpTermRenderVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
getTermThemes(): TermThemesType {
|
||||||
|
return this.termThemes.get();
|
||||||
|
}
|
||||||
|
|
||||||
updateScreenStatusIndicators(screenStatusIndicators: ScreenStatusIndicatorUpdateType[]) {
|
updateScreenStatusIndicators(screenStatusIndicators: ScreenStatusIndicatorUpdateType[]) {
|
||||||
for (const update of screenStatusIndicators) {
|
for (const update of screenStatusIndicators) {
|
||||||
this.getScreenById_single(update.screenid)?.setStatusIndicator(update.status);
|
this.getScreenById_single(update.screenid)?.setStatusIndicator(update.status);
|
||||||
@ -983,7 +961,7 @@ class Model {
|
|||||||
if (update.connect.screenstatusindicators != null) {
|
if (update.connect.screenstatusindicators != null) {
|
||||||
this.updateScreenStatusIndicators(update.connect.screenstatusindicators);
|
this.updateScreenStatusIndicators(update.connect.screenstatusindicators);
|
||||||
}
|
}
|
||||||
|
this.mergeTermThemes(update.connect.termthemes ?? {});
|
||||||
this.sessionListLoaded.set(true);
|
this.sessionListLoaded.set(true);
|
||||||
this.remotesLoaded.set(true);
|
this.remotesLoaded.set(true);
|
||||||
} else if (update.screen != null) {
|
} else if (update.screen != null) {
|
||||||
@ -1056,6 +1034,8 @@ class Model {
|
|||||||
} else if (update.userinputrequest != null) {
|
} else if (update.userinputrequest != null) {
|
||||||
const userInputRequest: UserInputRequest = update.userinputrequest;
|
const userInputRequest: UserInputRequest = update.userinputrequest;
|
||||||
this.modalsModel.pushModal(appconst.USER_INPUT, userInputRequest);
|
this.modalsModel.pushModal(appconst.USER_INPUT, userInputRequest);
|
||||||
|
} else if (update.termthemes != null) {
|
||||||
|
this.mergeTermThemes(update.termthemes);
|
||||||
} else if (update.sessiontombstone != null || update.screentombstone != null) {
|
} else if (update.sessiontombstone != null || update.screentombstone != null) {
|
||||||
// nothing (ignore)
|
// nothing (ignore)
|
||||||
} else {
|
} else {
|
||||||
@ -1309,9 +1289,6 @@ class Model {
|
|||||||
newTheme = appconst.DefaultTheme;
|
newTheme = appconst.DefaultTheme;
|
||||||
}
|
}
|
||||||
const themeUpdated = newTheme != this.getThemeSource();
|
const themeUpdated = newTheme != this.getThemeSource();
|
||||||
const oldTermTheme = this.getTermTheme();
|
|
||||||
const newTermTheme = clientData?.feopts?.termtheme;
|
|
||||||
const ttUpdated = this.termThemeUpdated(newTermTheme, oldTermTheme);
|
|
||||||
mobx.action(() => {
|
mobx.action(() => {
|
||||||
this.clientData.set(clientData);
|
this.clientData.set(clientData);
|
||||||
})();
|
})();
|
||||||
@ -1332,36 +1309,6 @@ class Model {
|
|||||||
getApi().setNativeThemeSource(newTheme);
|
getApi().setNativeThemeSource(newTheme);
|
||||||
this.bumpRenderVersion();
|
this.bumpRenderVersion();
|
||||||
}
|
}
|
||||||
// Only for global terminal theme. For session and screen terminal theme,
|
|
||||||
// they are handled in workspace view.
|
|
||||||
if (newTermTheme) {
|
|
||||||
const el = document.documentElement;
|
|
||||||
const globaltt = newTermTheme["global"] ?? this.currGlobalTermTheme;
|
|
||||||
const reset = newTermTheme["global"] == null;
|
|
||||||
if (globaltt) {
|
|
||||||
const rtn = this.updateTermTheme(el, globaltt, reset);
|
|
||||||
rtn.then(() => {
|
|
||||||
if (ttUpdated) {
|
|
||||||
this.bumpTermRenderVersion();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.currGlobalTermTheme = globaltt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
termThemeUpdated(newTermTheme, oldTermTheme) {
|
|
||||||
for (const key in oldTermTheme) {
|
|
||||||
if (!(key in newTermTheme)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const key in newTermTheme) {
|
|
||||||
if (!oldTermTheme[key] || oldTermTheme[key] !== newTermTheme[key]) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
submitCommandPacket(cmdPk: FeCmdPacketType, interactive: boolean): Promise<CommandRtnType> {
|
submitCommandPacket(cmdPk: FeCmdPacketType, interactive: boolean): Promise<CommandRtnType> {
|
||||||
|
@ -51,10 +51,9 @@ type TermWrapOpts = {
|
|||||||
onUpdateContentHeight: (termContext: RendererContext, height: number) => void;
|
onUpdateContentHeight: (termContext: RendererContext, height: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getThemeFromCSSVars(themeSrcEl: HTMLElement): ITheme {
|
function getThemeFromCSSVars(el: Element): ITheme {
|
||||||
const theme: ITheme = {};
|
const theme: ITheme = {};
|
||||||
const tse = themeSrcEl ?? document.documentElement;
|
const rootStyle = getComputedStyle(el);
|
||||||
let rootStyle = getComputedStyle(tse);
|
|
||||||
theme.foreground = rootStyle.getPropertyValue("--term-foreground");
|
theme.foreground = rootStyle.getPropertyValue("--term-foreground");
|
||||||
theme.background = rootStyle.getPropertyValue("--term-background");
|
theme.background = rootStyle.getPropertyValue("--term-background");
|
||||||
theme.black = rootStyle.getPropertyValue("--term-black");
|
theme.black = rootStyle.getPropertyValue("--term-black");
|
||||||
@ -131,8 +130,7 @@ class TermWrap {
|
|||||||
let cols = windowWidthToCols(opts.winSize.width, opts.fontSize);
|
let cols = windowWidthToCols(opts.winSize.width, opts.fontSize);
|
||||||
this.termSize = { rows: opts.termOpts.rows, cols: cols };
|
this.termSize = { rows: opts.termOpts.rows, cols: cols };
|
||||||
}
|
}
|
||||||
const themeSrcEl = GlobalModel.termThemeSrcEl.get();
|
let theme = getThemeFromCSSVars(this.connectedElem);
|
||||||
let theme = getThemeFromCSSVars(themeSrcEl);
|
|
||||||
this.terminal = new Terminal({
|
this.terminal = new Terminal({
|
||||||
rows: this.termSize.rows,
|
rows: this.termSize.rows,
|
||||||
cols: this.termSize.cols,
|
cols: this.termSize.cols,
|
||||||
|
12
src/types/custom.d.ts
vendored
12
src/types/custom.d.ts
vendored
@ -345,6 +345,7 @@ declare global {
|
|||||||
screenstatusindicators: ScreenStatusIndicatorUpdateType[];
|
screenstatusindicators: ScreenStatusIndicatorUpdateType[];
|
||||||
screennumrunningcommands: ScreenNumRunningCommandsUpdateType[];
|
screennumrunningcommands: ScreenNumRunningCommandsUpdateType[];
|
||||||
activesessionid: string;
|
activesessionid: string;
|
||||||
|
termthemes: TermThemesType;
|
||||||
};
|
};
|
||||||
|
|
||||||
type BookmarksUpdateType = {
|
type BookmarksUpdateType = {
|
||||||
@ -386,6 +387,13 @@ declare global {
|
|||||||
userinputrequest?: UserInputRequest;
|
userinputrequest?: UserInputRequest;
|
||||||
screentombstone?: any;
|
screentombstone?: any;
|
||||||
sessiontombstone?: any;
|
sessiontombstone?: any;
|
||||||
|
termthemes?: TermThemesType;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TermThemesType = {
|
||||||
|
[key: string]: {
|
||||||
|
[innerKey: string]: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type HistoryViewDataType = {
|
type HistoryViewDataType = {
|
||||||
@ -581,7 +589,7 @@ declare global {
|
|||||||
data: Uint8Array;
|
data: Uint8Array;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TermThemeType = {
|
type TermThemeSettingsType = {
|
||||||
[k: string]: string | null;
|
[k: string]: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -589,7 +597,7 @@ declare global {
|
|||||||
termfontsize: number;
|
termfontsize: number;
|
||||||
termfontfamily: string;
|
termfontfamily: string;
|
||||||
theme: NativeThemeSource;
|
theme: NativeThemeSource;
|
||||||
termtheme: TermThemeType;
|
termthemesettings: TermThemeSettingsType;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ConfirmFlagsType = {
|
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[] = [];
|
const tt: DropdownItem[] = [];
|
||||||
tt.push({
|
tt.push({
|
||||||
label: noneLabel,
|
label: noneLabel,
|
||||||
value: null,
|
value: "inherit",
|
||||||
});
|
});
|
||||||
for (const themeName of termThemes) {
|
for (const themeName of Object.keys(termThemeOptions)) {
|
||||||
tt.push({
|
tt.push({
|
||||||
label: themeName,
|
label: themeName,
|
||||||
value: themeName,
|
value: themeName,
|
||||||
|
@ -37,6 +37,7 @@ import (
|
|||||||
"github.com/wavetermdev/waveterm/waveshell/pkg/wlog"
|
"github.com/wavetermdev/waveterm/waveshell/pkg/wlog"
|
||||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/bufferedpipe"
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/bufferedpipe"
|
||||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/cmdrunner"
|
"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/ephemeral"
|
||||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/pcloud"
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/pcloud"
|
||||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/releasechecker"
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/releasechecker"
|
||||||
@ -156,6 +157,7 @@ func HandleWs(w http.ResponseWriter, r *http.Request) {
|
|||||||
removeWSStateAfterTimeout(clientId, stateConnectTime, WSStateReconnectTime)
|
removeWSStateAfterTimeout(clientId, stateConnectTime, WSStateReconnectTime)
|
||||||
}()
|
}()
|
||||||
log.Printf("WebSocket opened %s %s\n", state.ClientId, shell.RemoteAddr)
|
log.Printf("WebSocket opened %s %s\n", state.ClientId, shell.RemoteAddr)
|
||||||
|
|
||||||
state.RunWSRead()
|
state.RunWSRead()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -976,6 +978,10 @@ func doShutdown(reason string) {
|
|||||||
log.Printf("[wave] closing db connection\n")
|
log.Printf("[wave] closing db connection\n")
|
||||||
sstore.CloseDB()
|
sstore.CloseDB()
|
||||||
log.Printf("[wave] *** shutting down local server\n")
|
log.Printf("[wave] *** shutting down local server\n")
|
||||||
|
watcher := configstore.GetWatcher()
|
||||||
|
if watcher != nil {
|
||||||
|
watcher.Close()
|
||||||
|
}
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
syscall.Kill(syscall.Getpid(), syscall.SIGINT)
|
syscall.Kill(syscall.Getpid(), syscall.SIGINT)
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(5 * time.Second)
|
||||||
@ -1016,6 +1022,13 @@ func configDirHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write(dirListJson)
|
w.Write(dirListJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func configWatcher() {
|
||||||
|
watcher := configstore.GetWatcher()
|
||||||
|
if watcher != nil {
|
||||||
|
watcher.Start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func startupActivityUpdate() {
|
func startupActivityUpdate() {
|
||||||
activity := telemetry.ActivityUpdate{
|
activity := telemetry.ActivityUpdate{
|
||||||
NumConns: remote.NumRemotes(),
|
NumConns: remote.NumRemotes(),
|
||||||
@ -1080,7 +1093,7 @@ func main() {
|
|||||||
log.Printf("[error] %v\n", err)
|
log.Printf("[error] %v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, err = scbase.EnsureConfigDir()
|
_, err = scbase.EnsureConfigDirs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[error] ensuring config directory: %v\n", err)
|
log.Printf("[error] ensuring config directory: %v\n", err)
|
||||||
return
|
return
|
||||||
@ -1120,6 +1133,7 @@ func main() {
|
|||||||
startupActivityUpdate()
|
startupActivityUpdate()
|
||||||
installSignalHandlers()
|
installSignalHandlers()
|
||||||
go telemetryLoop()
|
go telemetryLoop()
|
||||||
|
go configWatcher()
|
||||||
go stdinReadWatch()
|
go stdinReadWatch()
|
||||||
go runWebSocketServer()
|
go runWebSocketServer()
|
||||||
go func() {
|
go func() {
|
||||||
|
@ -8,6 +8,7 @@ require (
|
|||||||
github.com/alessio/shellescape v1.4.1
|
github.com/alessio/shellescape v1.4.1
|
||||||
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2
|
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2
|
||||||
github.com/creack/pty v1.1.18
|
github.com/creack/pty v1.1.18
|
||||||
|
github.com/fsnotify/fsnotify v1.6.0
|
||||||
github.com/golang-migrate/migrate/v4 v4.16.2
|
github.com/golang-migrate/migrate/v4 v4.16.2
|
||||||
github.com/google/go-github/v60 v60.0.0
|
github.com/google/go-github/v60 v60.0.0
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
|
@ -9,6 +9,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA=
|
github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA=
|
||||||
github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
|
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||||
|
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
github.com/golang-migrate/migrate/v4 v4.16.2 h1:8coYbMKUyInrFk1lfGfRovTLAW7PhWp8qQDT2iKfuoA=
|
github.com/golang-migrate/migrate/v4 v4.16.2 h1:8coYbMKUyInrFk1lfGfRovTLAW7PhWp8qQDT2iKfuoA=
|
||||||
@ -63,6 +65,7 @@ golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
|||||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||||
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
|
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
|
||||||
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
||||||
|
@ -3640,13 +3640,13 @@ func TermSetThemeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType)
|
|||||||
}
|
}
|
||||||
themeName, themeNameOk := pk.Kwargs["name"]
|
themeName, themeNameOk := pk.Kwargs["name"]
|
||||||
feOpts := clientData.FeOpts
|
feOpts := clientData.FeOpts
|
||||||
if feOpts.TermTheme == nil {
|
if feOpts.TermThemeSettings == nil {
|
||||||
feOpts.TermTheme = make(map[string]string)
|
feOpts.TermThemeSettings = make(map[string]string)
|
||||||
}
|
}
|
||||||
if themeNameOk && themeName != "" {
|
if themeNameOk && themeName != "" {
|
||||||
feOpts.TermTheme[id] = themeName
|
feOpts.TermThemeSettings[id] = themeName
|
||||||
} else {
|
} else {
|
||||||
delete(feOpts.TermTheme, id)
|
delete(feOpts.TermThemeSettings, id)
|
||||||
}
|
}
|
||||||
err = sstore.UpdateClientFeOpts(ctx, feOpts)
|
err = sstore.UpdateClientFeOpts(ctx, feOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -5856,13 +5856,13 @@ func ClientSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sc
|
|||||||
}
|
}
|
||||||
if termthemeStr, found := pk.Kwargs["termtheme"]; found {
|
if termthemeStr, found := pk.Kwargs["termtheme"]; found {
|
||||||
feOpts := clientData.FeOpts
|
feOpts := clientData.FeOpts
|
||||||
if feOpts.TermTheme == nil {
|
if feOpts.TermThemeSettings == nil {
|
||||||
feOpts.TermTheme = make(map[string]string)
|
feOpts.TermThemeSettings = make(map[string]string)
|
||||||
}
|
}
|
||||||
if termthemeStr == "" {
|
if termthemeStr == "" {
|
||||||
delete(feOpts.TermTheme, "global")
|
delete(feOpts.TermThemeSettings, "root")
|
||||||
} else {
|
} else {
|
||||||
feOpts.TermTheme["global"] = termthemeStr
|
feOpts.TermThemeSettings["root"] = termthemeStr
|
||||||
}
|
}
|
||||||
err = sstore.UpdateClientFeOpts(ctx, feOpts)
|
err = sstore.UpdateClientFeOpts(ctx, feOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
102
wavesrv/pkg/configstore/filewatcher.go
Normal file
102
wavesrv/pkg/configstore/filewatcher.go
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
package configstore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var instance *Watcher
|
||||||
|
var once sync.Once
|
||||||
|
|
||||||
|
type Watcher struct {
|
||||||
|
watcher *fsnotify.Watcher
|
||||||
|
mutex sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWatcher returns the singleton instance of the Watcher
|
||||||
|
func GetWatcher() *Watcher {
|
||||||
|
once.Do(func() {
|
||||||
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to create file watcher: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
instance = &Watcher{watcher: watcher}
|
||||||
|
log.Printf("started config watcher: %v\n", configDirAbsPath)
|
||||||
|
if err := instance.addPath(configDirAbsPath); err != nil {
|
||||||
|
log.Printf("failed to add path %s to watcher: %v", configDirAbsPath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
|
// addPath adds the specified path and all its subdirectories to the watcher
|
||||||
|
func (w *Watcher) addPath(path string) error {
|
||||||
|
return filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if info.IsDir() {
|
||||||
|
if err := w.watcher.Add(path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("added to watcher: %s", path)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) Start() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event, ok := <-w.watcher.Events:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.handleEvent(event)
|
||||||
|
case err, ok := <-w.watcher.Errors:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Println("watcher error:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) Close() {
|
||||||
|
w.mutex.Lock()
|
||||||
|
defer w.mutex.Unlock()
|
||||||
|
if w.watcher != nil {
|
||||||
|
w.watcher.Close()
|
||||||
|
w.watcher = nil
|
||||||
|
log.Println("file watcher closed.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) handleEvent(event fsnotify.Event) {
|
||||||
|
config := make(ConfigReturn)
|
||||||
|
fileName, normalizedPath := getNameAndPath(event)
|
||||||
|
|
||||||
|
if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create || event.Op&fsnotify.Rename == fsnotify.Rename {
|
||||||
|
content, err := readFileContents(normalizedPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error reading file %s: %v", normalizedPath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
config[fileName] = content
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.Op&fsnotify.Remove == fsnotify.Remove {
|
||||||
|
config[fileName] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
update := scbus.MakeUpdatePacket()
|
||||||
|
update.AddUpdate(config)
|
||||||
|
scbus.MainUpdateBus.DoUpdate(update)
|
||||||
|
}
|
83
wavesrv/pkg/configstore/termthemes.go
Normal file
83
wavesrv/pkg/configstore/termthemes.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package configstore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbase"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ConfigReturnTypeStr = "termthemes"
|
||||||
|
const configDir = "config/terminal-themes/"
|
||||||
|
|
||||||
|
var configDirAbsPath = path.Join(scbase.GetWaveHomeDir(), configDir)
|
||||||
|
|
||||||
|
type ConfigReturn map[string]map[string]string
|
||||||
|
|
||||||
|
func (tt ConfigReturn) GetType() string {
|
||||||
|
return ConfigReturnTypeStr
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNameAndPath(event fsnotify.Event) (string, string) {
|
||||||
|
filePath := event.Name
|
||||||
|
fileName := filepath.Base(filePath)
|
||||||
|
|
||||||
|
// Normalize the file path for consistency across platforms
|
||||||
|
normalizedPath := filepath.ToSlash(filePath)
|
||||||
|
return fileName, normalizedPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// readFileContents reads and unmarshals the JSON content from a file.
|
||||||
|
func 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScanConfigs reads all JSON files in the specified directory and its subdirectories.
|
||||||
|
func ScanConfigs() (ConfigReturn, error) {
|
||||||
|
config := make(ConfigReturn)
|
||||||
|
|
||||||
|
if _, err := os.Stat(configDirAbsPath); errors.Is(err, os.ErrNotExist) {
|
||||||
|
log.Printf("directory does not exist: %s", configDirAbsPath)
|
||||||
|
return ConfigReturn{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := filepath.Walk(configDirAbsPath, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.IsDir() && filepath.Ext(info.Name()) == ".json" {
|
||||||
|
content, err := readFileContents(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error reading file %s: %v", path, err)
|
||||||
|
return nil // continue walking despite error in reading file
|
||||||
|
}
|
||||||
|
// Use the relative path from the directory as the key to store themes
|
||||||
|
relPath, err := filepath.Rel(configDirAbsPath, path)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error getting relative file path %s: %v", path, err)
|
||||||
|
return nil // continue walking despite error in getting relative path
|
||||||
|
}
|
||||||
|
config[relPath] = content
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
@ -234,7 +234,7 @@ func GetScreensDir() string {
|
|||||||
return sdir
|
return sdir
|
||||||
}
|
}
|
||||||
|
|
||||||
func EnsureConfigDir() (string, error) {
|
func EnsureConfigDirs() (string, error) {
|
||||||
scHome := GetWaveHomeDir()
|
scHome := GetWaveHomeDir()
|
||||||
configDir := path.Join(scHome, "config")
|
configDir := path.Join(scHome, "config")
|
||||||
err := ensureDir(configDir)
|
err := ensureDir(configDir)
|
||||||
@ -250,6 +250,11 @@ func EnsureConfigDir() (string, error) {
|
|||||||
keybindingsFileObj.WriteString("[]\n")
|
keybindingsFileObj.WriteString("[]\n")
|
||||||
keybindingsFileObj.Close()
|
keybindingsFileObj.Close()
|
||||||
}
|
}
|
||||||
|
terminalThemesDir := path.Join(configDir, "terminal-themes")
|
||||||
|
err = ensureDir(terminalThemesDir)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
return configDir, nil
|
return configDir, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/wavetermdev/waveterm/waveshell/pkg/packet"
|
"github.com/wavetermdev/waveterm/waveshell/pkg/packet"
|
||||||
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/configstore"
|
||||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/mapqueue"
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/mapqueue"
|
||||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/remote"
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/remote"
|
||||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbus"
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbus"
|
||||||
@ -165,6 +166,11 @@ func (ws *WSState) handleConnection() error {
|
|||||||
connectUpdate.Remotes = remotes
|
connectUpdate.Remotes = remotes
|
||||||
// restore status indicators
|
// restore status indicators
|
||||||
connectUpdate.ScreenStatusIndicators, connectUpdate.ScreenNumRunningCommands = sstore.GetCurrentIndicatorState()
|
connectUpdate.ScreenStatusIndicators, connectUpdate.ScreenNumRunningCommands = sstore.GetCurrentIndicatorState()
|
||||||
|
configs, err := configstore.ScanConfigs()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting configs: %w", err)
|
||||||
|
}
|
||||||
|
connectUpdate.TermThemes = &configs
|
||||||
mu := scbus.MakeUpdatePacket()
|
mu := scbus.MakeUpdatePacket()
|
||||||
mu.AddUpdate(*connectUpdate)
|
mu.AddUpdate(*connectUpdate)
|
||||||
err = ws.Shell.WriteJson(mu)
|
err = ws.Shell.WriteJson(mu)
|
||||||
|
@ -253,7 +253,7 @@ type FeOptsType struct {
|
|||||||
TermFontSize int `json:"termfontsize,omitempty"`
|
TermFontSize int `json:"termfontsize,omitempty"`
|
||||||
TermFontFamily string `json:"termfontfamily,omitempty"`
|
TermFontFamily string `json:"termfontfamily,omitempty"`
|
||||||
Theme string `json:"theme,omitempty"`
|
Theme string `json:"theme,omitempty"`
|
||||||
TermTheme map[string]string `json:"termtheme"`
|
TermThemeSettings map[string]string `json:"termthemesettings"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReleaseInfoType struct {
|
type ReleaseInfoType struct {
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/wavetermdev/waveterm/waveshell/pkg/packet"
|
"github.com/wavetermdev/waveterm/waveshell/pkg/packet"
|
||||||
"github.com/wavetermdev/waveterm/waveshell/pkg/utilfn"
|
"github.com/wavetermdev/waveterm/waveshell/pkg/utilfn"
|
||||||
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/configstore"
|
||||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbus"
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -104,6 +105,7 @@ type ConnectUpdate struct {
|
|||||||
ScreenStatusIndicators []*ScreenStatusIndicatorType `json:"screenstatusindicators,omitempty"`
|
ScreenStatusIndicators []*ScreenStatusIndicatorType `json:"screenstatusindicators,omitempty"`
|
||||||
ScreenNumRunningCommands []*ScreenNumRunningCommandsType `json:"screennumrunningcommands,omitempty"`
|
ScreenNumRunningCommands []*ScreenNumRunningCommandsType `json:"screennumrunningcommands,omitempty"`
|
||||||
ActiveSessionId string `json:"activesessionid,omitempty"`
|
ActiveSessionId string `json:"activesessionid,omitempty"`
|
||||||
|
TermThemes *configstore.ConfigReturn `json:"termthemes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ConnectUpdate) GetType() string {
|
func (ConnectUpdate) GetType() string {
|
||||||
|
Loading…
Reference in New Issue
Block a user