Merge pull request #63 from wavetermdev/dev-0.5.0

Merge dev 0.5.0 branch to main
This commit is contained in:
sawka 2023-11-07 00:13:40 -08:00 committed by GitHub
commit be9a3c288a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1407 additions and 776 deletions

View File

@ -18,16 +18,16 @@
"electron-squirrel-startup": "^1.0.0",
"mobx": "^6.6.0",
"mobx-react": "^7.5.0",
"monaco-editor": "^0.41.0",
"monaco-editor": "^0.44.0",
"mustache": "^4.2.0",
"node-fetch": "^3.2.10",
"papaparse": "^5.4.1",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"react-json-view": "^1.21.3",
"react-markdown": "^8.0.5",
"remark": "^14.0.2",
"remark-gfm": "^3.0.1",
"react-markdown": "^9.0.0",
"remark": "^15.0.1",
"remark-gfm": "^4.0.0",
"sprintf-js": "^1.1.2",
"throttle-debounce": "^5.0.0",
"tsx-control-statements": "^4.1.1",
@ -57,16 +57,18 @@
"@svgr/webpack": "^8.1.0",
"@types/classnames": "^2.3.1",
"@types/electron": "^1.6.10",
"@types/node": "^18.0.3",
"@types/papaparse": "^5.3.9",
"@types/node": "^20.4.0",
"@types/papaparse": "^5.3.10",
"@types/react": "^18.0.12",
"@types/uuid": "9.0.0",
"@types/sprintf-js": "^1.1.3",
"@types/throttle-debounce": "^5.0.1",
"@types/uuid": "9.0.6",
"@types/webpack-env": "^1.18.3",
"babel-loader": "^9.1.3",
"babel-plugin-jsx-control-statements": "^4.1.2",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.7.1",
"electron": "27.0.0",
"electron": "27.0.3",
"file-loader": "^6.2.0",
"http-server": "^14.1.1",
"less": "^4.1.2",
@ -80,7 +82,7 @@
"typescript": "^4.7.3",
"webpack": "^5.73.0",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^4.9.2",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.9.1",
"webpack-merge": "^5.8.0"
},

View File

@ -23,7 +23,7 @@ import {
} from "./common/modals/settings";
import { RemotesModal } from "./connections/connections";
import { TosModal } from "./common/modals/modals";
import { MainSideBar } from "./sidebar/MainSideBar";
import { MainSideBar } from "./sidebar/sidebar";
import { DisconnectedModal, ClientStopModal, AlertModal, AboutModal } from "./common/modals/modals";
import { ErrorBoundary } from "./common/error/errorboundary";
import "./app.less";

View File

@ -625,7 +625,81 @@
}
}
.textfield {
display: flex;
align-items: center;
border: 1px solid @term-white;
border-radius: 6px;
position: relative;
margin-bottom: 1rem;
background-color: transparent;
height: 44px;
min-width: 412px;
gap: 6px;
&.focused {
border-color: @term-green;
}
&.error {
border-color: @term-red;
}
.textfield-inner {
display: flex;
align-items: flex-end;
height: 100%;
position: relative;
flex-grow: 1;
.textfield-label {
position: absolute;
left: 16px;
top: 16px;
font-size: 12.5px;
transition: all 0.3s;
color: @term-white;
line-height: 10px;
&.float {
font-size: 10px;
top: 5px;
}
&.start {
left: 0;
}
}
.textfield-input {
width: 100%;
height: 30px;
border: none;
padding: 5px 0 5px 16px;
font-size: 16px;
outline: none;
background-color: transparent;
color: @term-bright-white;
line-height: 20px;
&.start {
padding: 5px 16px 5px 0;
}
}
}
i {
font-size: 16px;
}
}
.input-decoration {
display: flex;
align-items: center;
justify-content: center;
padding: 0 8px;
margin: 8px;
}
.inline-edit {
.icon {
@ -665,4 +739,3 @@
}
}
}

View File

@ -10,6 +10,7 @@ import remarkGfm from "remark-gfm";
import cn from "classnames";
import { If } from "tsx-control-statements/components";
import type { RemoteType } from "../../types/types";
import { debounce } from "throttle-debounce";
import { ReactComponent as CheckIcon } from "../assets/icons/line/check.svg";
import { ReactComponent as CopyIcon } from "../assets/icons/history/copy.svg";
@ -123,6 +124,152 @@ class Checkbox extends React.Component<
}
}
interface InputDecorationProps {
children: React.ReactNode;
}
@mobxReact.observer
class InputDecoration extends React.Component<InputDecorationProps, {}> {
render() {
const { children, onClick } = this.props;
return <div className="input-decoration">{children}</div>;
}
}
interface TextFieldDecorationProps {
startDecoration?: React.ReactNode;
endDecoration?: React.ReactNode;
}
interface TextFieldProps {
label: string;
value?: string;
className?: string;
onChange?: (value: string) => void;
placeholder?: string;
defaultValue?: string;
decoration?: TextFieldDecorationProps;
required?: boolean;
}
interface TextFieldState {
focused: boolean;
internalValue: string;
error: boolean;
showHelpText: boolean;
hasContent: boolean;
}
@mobxReact.observer
class TextField extends React.Component<TextFieldProps, TextFieldState> {
inputRef: React.RefObject<HTMLInputElement>;
state: TextFieldState;
constructor(props: TextFieldProps) {
super(props);
const hasInitialContent = Boolean(props.value || props.defaultValue);
this.state = {
focused: false,
hasContent: hasInitialContent,
internalValue: props.defaultValue || "",
error: false,
showHelpText: false,
};
this.inputRef = React.createRef();
}
componentDidUpdate(prevProps: TextFieldProps) {
// Only update the focus state if using as controlled
if (this.props.value !== undefined && this.props.value !== prevProps.value) {
this.setState({ focused: Boolean(this.props.value) });
}
}
@boundMethod
handleFocus() {
this.setState({ focused: true });
}
@boundMethod
handleBlur() {
const { required } = this.props;
if (this.inputRef.current) {
const value = this.inputRef.current.value;
if (required && !value) {
this.setState({ error: true, focused: false });
} else {
this.setState({ error: false, focused: false });
}
}
}
@boundMethod
handleHelpTextClick() {
this.setState((prevState) => ({ showHelpText: !prevState.showHelpText }));
}
debouncedOnChange = debounce(300, (value) => {
const { onChange } = this.props;
onChange?.(value);
});
@boundMethod
handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {
const { required } = this.props;
const inputValue = e.target.value;
// Check if value is empty and the field is required
if (required && !inputValue) {
this.setState({ error: true, hasContent: false });
} else {
this.setState({ error: false, hasContent: Boolean(inputValue) });
}
// Update the internal state for uncontrolled version
if (this.props.value === undefined) {
this.setState({ internalValue: inputValue });
}
this.debouncedOnChange(inputValue);
}
render() {
const { label, value, placeholder, decoration, className } = this.props;
const { focused, internalValue, error } = this.state;
// Decide if the input should behave as controlled or uncontrolled
const inputValue = value !== undefined ? value : internalValue;
return (
<div className={cn(`textfield ${className || ""}`, { focused: focused, error: error })}>
{decoration?.startDecoration && <>{decoration.startDecoration}</>}
<div className="textfield-inner">
<label
className={cn("textfield-label", {
float: this.state.hasContent || this.state.focused || placeholder,
start: decoration?.startDecoration,
})}
htmlFor={label}
>
{label}
</label>
<input
className={cn("textfield-input", { start: decoration?.startDecoration })}
ref={this.inputRef}
id={label}
value={inputValue}
onChange={this.handleInputChange}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
placeholder={placeholder}
/>
</div>
{decoration?.endDecoration && <div>{decoration.endDecoration}</div>}
</div>
);
}
}
@mobxReact.observer
class RemoteStatusLight extends React.Component<{ remote: RemoteType }, {}> {
render() {
@ -362,4 +509,6 @@ export {
InfoMessage,
Markdown,
SettingsError,
TextField,
InputDecoration,
};

View File

@ -330,7 +330,7 @@
.about-content {
margin-bottom: 0;
section {
.wave-section {
.logo-wrapper {
width: 72px;
height: 72px;
@ -403,12 +403,12 @@
}
}
section:nth-child(3) {
.wave-section:nth-child(3) {
display: flex;
align-items: flex-start;
gap: 10px;
.button-link {
.wave-button-link {
display: flex;
align-items: center;
@ -418,7 +418,7 @@
}
}
section:last-child {
.wave-section:last-child {
margin-bottom: 24px;
color: @term-white;
}
@ -426,7 +426,7 @@
}
}
.button {
.wave-button {
display: flex;
padding: 6px 16px;
align-items: center;
@ -435,7 +435,7 @@
height: auto;
}
.button.color-green {
.wave-button.color-green {
color: @term-bright-white;
background: @term-green !important; // !important is needed to override the default button color
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
@ -446,7 +446,7 @@
}
}
.button.color-standard {
.wave-button.color-standard {
color: @term-white;
background: var(--overlays-white-6, rgba(255, 255, 255, 0.12));
@ -455,7 +455,7 @@
}
}
.button-link {
.wave-button-link {
display: flex;
padding: 6px 16px;
align-items: center;
@ -465,14 +465,12 @@
cursor: pointer;
}
.modal-content {
section {
display: flex;
align-items: center;
gap: 16px;
align-self: stretch;
width: 100%;
}
.wave-section {
display: flex;
align-items: center;
gap: 16px;
align-self: stretch;
width: 100%;
}
.modal.welcome-modal {

View File

@ -268,15 +268,17 @@ class TosModal extends React.Component<{}, {}> {
<div className="item-inner">
<div className="item-title">Telemetry</div>
<div className="item-text">
We only collect minimal <i>anonymous</i> telemetry data to help us
understand how many people are using Wave.
We only collect minimal <i>anonymous</i> telemetry data to help us understand
how many people are using Wave.
</div>
<div className="item-field" style={{marginTop: 2}}>
<div className="item-field" style={{ marginTop: 2 }}>
<Toggle
checked={!cdata.clientopts.notelemetry}
onChange={this.handleChangeTelemetry}
/>
<div className="item-label">Telemetry {cdata.clientopts.notelemetry ? "Disabled" : "Enabled"}</div>
<div className="item-label">
Telemetry {cdata.clientopts.notelemetry ? "Disabled" : "Enabled"}
</div>
</div>
</div>
</div>
@ -287,8 +289,9 @@ class TosModal extends React.Component<{}, {}> {
<div className="item-inner">
<div className="item-title">Join our Community</div>
<div className="item-text">
Get help, submit feature requests, report bugs,
or just chat with fellow terminal enthusiasts.<br/>
Get help, submit feature requests, report bugs, or just chat with fellow
terminal enthusiasts.
<br />
<a target="_blank" href={util.makeExternLink("https://discord.gg/XfvZ334gwU")}>
Join the Wave&nbsp;Discord&nbsp;Channel
</a>
@ -305,8 +308,8 @@ class TosModal extends React.Component<{}, {}> {
<div className="item-inner">
<div className="item-title">Support us on GitHub</div>
<div className="item-text">
We're <i>open source</i> and committed to providing a free terminal for individual
users. Please show your support us by giving us a star on{" "}
We're <i>open source</i> and committed to providing a free terminal for
individual users. Please show your support us by giving us a star on{" "}
<a
target="_blank"
href={util.makeExternLink("https://github.com/wavetermdev/waveterm")}
@ -406,7 +409,7 @@ class AboutModal extends React.Component<{}, {}> {
</div>
</header>
<div className="content about-content">
<section>
<section className="wave-section about-section">
<div className="logo-wrapper">
<img src={logo} alt="logo" />
</div>
@ -415,10 +418,12 @@ class AboutModal extends React.Component<{}, {}> {
<div className="text-standard">Modern Terminal for Seamless Workflow</div>
</div>
</section>
<section className="text-standard">{this.getStatus(this.isUpToDate())}</section>
<section>
<section className="wave-section about-section text-standard">
{this.getStatus(this.isUpToDate())}
</section>
<section className="wave-section about-section">
<a
className="button button-link color-standard"
className="wave-button wave-button-link color-standard"
href={util.makeExternLink("https://github.com/wavetermdev/waveterm")}
target="_blank"
>
@ -426,7 +431,7 @@ class AboutModal extends React.Component<{}, {}> {
Github
</a>
<a
className="button button-link color-standard"
className="wave-button wave-button-link color-standard"
href={util.makeExternLink("https://www.commandline.dev/")}
target="_blank"
>
@ -434,7 +439,7 @@ class AboutModal extends React.Component<{}, {}> {
Website
</a>
<a
className="button button-link color-standard"
className="wave-button wave-button-link color-standard"
href={util.makeExternLink(
"https://github.com/wavetermdev/waveterm/blob/main/LICENSE"
)}
@ -444,7 +449,9 @@ class AboutModal extends React.Component<{}, {}> {
License
</a>
</section>
<section className="text-standard">Copyright © 2023 Command Line Inc.</section>
<section className="wave-section about-section text-standard">
Copyright © 2023 Command Line Inc.
</section>
</div>
</div>
</div>

View File

@ -19,15 +19,15 @@
@textarea-background: #2a2a2a;
@text-primary: #fff;
@text-secondary: #C3C8C2;
@text-secondary: #c3c8c2;
@text-caption: #8b918a;
@accent-color: #3B3F3A;
@accent-color: #3b3f3a;
@status-outline: #151715;
@dropdown-menu: rgba(21, 23, 21, 1);
@status-connected: #46A758;
@status-connected: #46a758;
@status-connecting: #f5d90a;
@status-error: #e54d2e;
@status-disconnected: #c3c8c2;
@ -53,11 +53,11 @@
@tab-orange: #ef713b;
@tab-yellow: #e0b956;
@tab-green: #58c142;
@tab-mint: #4BFFA9;
@tab-cyan: #4BDFFF;
@tab-blue: #3971FF;
@tab-violet: #BA76FF;
@tab-pink: #E05677;
@tab-mint: #4bffa9;
@tab-cyan: #4bdfff;
@tab-blue: #3971ff;
@tab-violet: #ba76ff;
@tab-pink: #e05677;
@tab-white: #ffffff;
@tab-black-text: #333;

View File

@ -119,11 +119,12 @@
margin: 16px;
.newtab-section {
padding: 16px;
display: flex;
padding: 16px;
flex-direction: column;
align-items: flex-start;
gap: 4px;
gap: 8px;
align-self: stretch;
&.conn-section {
gap: 8px;
@ -151,6 +152,7 @@
height: 20px;
cursor: pointer;
position: relative;
font-size: 14px;
.icon {
width: 20px;
@ -169,6 +171,53 @@
}
}
.status-div {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
padding: 3px;
svg.status-icon {
width: 10px;
height: 10px;
}
}
.add-div {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
svg.add-icon {
width: 16px;
height: 16px;
path {
fill: @text-primary;
}
}
}
.text-standard {
color: @text-secondary;
}
.text-caption {
color: @text-caption;
}
.ellipsis {
text-overflow: ellipsis;
}
&:hover {
background-color: rgba(241, 246, 243, 0.08);
}
.icon.color-white + .check-icon {
path {
fill: black;

View File

@ -10,7 +10,7 @@ import { If, For } from "tsx-control-statements/components";
import cn from "classnames";
import { debounce } from "throttle-debounce";
import dayjs from "dayjs";
import { GlobalCommandRunner, TabColors } from "../../../model/model";
import { GlobalCommandRunner, TabColors, TabIcons } from "../../../model/model";
import type { LineType, RenderModeType, LineFactoryProps, CommandRtnType } from "../../../types/types";
import * as T from "../../../types/types";
import localizedFormat from "dayjs/plugin/localizedFormat";
@ -20,7 +20,8 @@ import { GlobalModel, ScreenLines, Screen, Session } from "../../../model/model"
import { Line } from "../../line/linecomps";
import { LinesView } from "../../line/linesview";
import { ConnectionDropdown } from "../../connections/connections";
import * as util from "../../../util/util";
import * as util from "../../../util/util";
import { TextField, InputDecoration } from "../../common/common";
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";
@ -37,7 +38,7 @@ dayjs.extend(localizedFormat);
type OV<V> = mobx.IObservableValue<V>;
@mobxReact.observer
class ScreenView extends React.Component<{ session: Session, screen: Screen }, {}> {
class ScreenView extends React.Component<{ session: Session; screen: Screen }, {}> {
render() {
let { session, screen } = this.props;
if (screen == null) {
@ -54,7 +55,8 @@ class ScreenView extends React.Component<{ session: Session, screen: Screen }, {
@mobxReact.observer
class NewTabSettings extends React.Component<{ screen: Screen }, {}> {
errorMessage: OV<string> = mobx.observable.box(null, { name: "NewTabSettings-errorMessage" });
connDropdownActive: OV<boolean> = mobx.observable.box(false, { name: "NewTabSettings-connDropdownActive" });
errorMessage: OV<string | null> = mobx.observable.box(null, { name: "NewTabSettings-errorMessage" });
@boundMethod
selectTabColor(color: string): void {
@ -67,15 +69,29 @@ class NewTabSettings extends React.Component<{ screen: Screen }, {}> {
}
@boundMethod
inlineUpdateName(val: string): void {
selectTabIcon(icon: string): void {
let { screen } = this.props;
if (util.isStrEq(val, screen.name.get())) {
if (screen.getTabIcon() == icon) {
return;
}
let prtn = GlobalCommandRunner.screenSetSettings(screen.screenId, { tabicon: icon }, false);
util.commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
updateName(val: string): void {
let { screen } = this.props;
let prtn = GlobalCommandRunner.screenSetSettings(screen.screenId, { name: val }, false);
util.commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
toggleConnDropdown(): void {
mobx.action(() => {
this.connDropdownActive.set(!this.connDropdownActive.get());
})();
}
@boundMethod
selectRemote(cname: string): void {
let prtn = GlobalCommandRunner.screenSetRemote(cname, true, false);
@ -84,62 +100,112 @@ class NewTabSettings extends React.Component<{ screen: Screen }, {}> {
@boundMethod
clickNewConnection(): void {
GlobalModel.remotesModalModel.openModalForEdit({remoteedit: true}, true);
GlobalModel.remotesModalModel.openModalForEdit({ remoteedit: true }, true);
}
renderTabIconSelector(): React.ReactNode {
let { screen } = this.props;
let curIcon = screen.getTabIcon();
if (util.isBlank(curIcon) || curIcon == "default") {
curIcon = "square";
}
let icon: string | null = null;
return (
<>
<div className="text-s1 unselectable">Select the icon</div>
<div className="control-iconlist">
<For each="icon" of={TabIcons}>
<div
className="icondiv"
key={icon}
title={icon || ""}
onClick={() => this.selectTabIcon(icon || "")}
>
<i className={`fa-sharp fa-solid fa-${icon}`}></i>
</div>
</For>
</div>
</>
);
}
renderTabColorSelector(): React.ReactNode {
let { screen } = this.props;
let curColor = screen.getTabColor();
if (util.isBlank(curColor) || curColor == "default") {
curColor = "green";
}
let color: string | null = null;
return (
<>
<div className="text-s1 unselectable">Select the color</div>
<div className="control-iconlist">
<For each="color" of={TabColors}>
<div
className="icondiv"
key={color}
title={color || ""}
onClick={() => this.selectTabColor(color || "")}
>
<EllipseIcon className={cn("icon", "color-" + color)} />
<If condition={color == curColor}>
<Check12Icon className="check-icon" />
</If>
</div>
</For>
</div>
</>
);
}
render() {
let { screen } = this.props;
let rptr = screen.curRemote.get();
let curColor = screen.getTabColor();
if (util.isBlank(curColor) || curColor == "default") {
curColor = "green";
}
let color: string = null;
let curRemote = GlobalModel.getRemote(GlobalModel.getActiveScreen().getCurRemoteInstance().remoteid);
return (
<div className="newtab-container">
<div className="newtab-section name-section">
<div className="text-standard">Name</div>
<TextField
label="Title"
required={true}
defaultValue={screen.name.get() ?? ""}
onChange={this.updateName}
decoration={{
endDecoration: (
<InputDecoration>
<i className="fa-sharp fa-regular fa-circle-question"></i>
</InputDecoration>
),
}}
/>
</div>
<div className="newtab-spacer" />
<div className="newtab-section conn-section">
<div className="text-s1 unselectable">
You're connected to [{getRemoteStr(rptr)}]. Do you want to change it?
You're connected to [{getRemoteStr(rptr)}]. Do you want to change it?
</div>
<div>
<ConnectionDropdown curRemote={curRemote} allowNewConn={true} onSelectRemote={this.selectRemote} onNewConn={this.clickNewConnection}/>
<ConnectionDropdown
curRemote={curRemote}
allowNewConn={true}
onSelectRemote={this.selectRemote}
onNewConn={this.clickNewConnection}
/>
</div>
<div className="text-caption cr-help-text">
To change connection from the command line use `cr [alias|user@host]`
</div>
</div>
<div className="newtab-spacer"/>
<div className="newtab-section settings-field">
<div className="text-s1 unselectable">
Name
</div>
<div className="settings-input">
<InlineSettingsTextEdit
placeholder="name"
text={screen.name.get() ?? "(none)"}
value={screen.name.get() ?? ""}
onChange={this.inlineUpdateName}
maxLength={50}
showIcon={true}
/>
</div>
</div>
<div className="newtab-spacer"/>
<div className="newtab-spacer" />
<div className="newtab-section">
<div className="text-s1 unselectable">
Select the color
</div>
<div className="control-iconlist">
<For each="color" of={TabColors}>
<div className="icondiv" key={color} title={color} onClick={() => this.selectTabColor(color)}>
<EllipseIcon className={cn("icon", "color-" + color)}/>
<If condition={color == curColor}>
<Check12Icon className="check-icon"/>
</If>
</div>
</For>
</div>
<div>{this.renderTabIconSelector()}</div>
</div>
<div className="newtab-spacer" />
<div className="newtab-section">
<div>{this.renderTabColorSelector()}</div>
</div>
</div>
);
@ -148,7 +214,7 @@ class NewTabSettings extends React.Component<{ screen: Screen }, {}> {
// screen is not null
@mobxReact.observer
class ScreenWindowView extends React.Component<{ session: Session, screen: Screen }, {}> {
class ScreenWindowView extends React.Component<{ session: Session; screen: Screen }, {}> {
rszObs: any;
windowViewRef: React.RefObject<any>;
@ -309,13 +375,17 @@ class ScreenWindowView extends React.Component<{ session: Session, screen: Scree
</div>
<If condition={lines.length == 0}>
<If condition={screen.nextLineNum.get() == 1}>
<NewTabSettings screen={screen}/>
<NewTabSettings screen={screen} />
</If>
<If condition={screen.nextLineNum.get() != 1}>
<div className="window-view" ref={this.windowViewRef} data-screenid={screen.screenId}>
<div key="lines" className="lines"></div>
<div key="window-empty" className={cn("window-empty")}>
<div><code className="text-standard">[workspace="{session.name.get()}" screen="{screen.name.get()}"]</code></div>
<div>
<code className="text-standard">
[workspace="{session.name.get()}" screen="{screen.name.get()}"]
</code>
</div>
</div>
</div>
</If>

View File

@ -8,14 +8,28 @@
border-radius: 12px 0px 0px 0px;
}
&.color-green, &.color-default {
&.color-green,
&.color-default {
svg.left-icon path {
fill: @tab-green;
}
.icon i {
color: @tab-green;
}
&.is-active {
border-top: 1px solid @tab-green;
background: linear-gradient(180deg, rgba(88, 193, 66, 0.20) 9.34%, rgba(88, 193, 66, 0.03) 44.16%, rgba(88, 193, 66, 0.00) 86.79%);
background: linear-gradient(
180deg,
rgba(88, 193, 66, 0.2) 9.34%,
rgba(88, 193, 66, 0.03) 44.16%,
rgba(88, 193, 66, 0) 86.79%
);
}
.icon i {
color: @tab-green;
}
}
@ -24,9 +38,18 @@
fill: @tab-orange;
}
.icon i {
color: @tab-orange;
}
&.is-active {
border-top: 1px solid @tab-orange;
background: linear-gradient(180deg, rgba(239, 113, 59, 0.20) 9.34%, rgba(239, 113, 59, 0.03) 44.16%, rgba(239, 113, 59, 0.00) 86.79%);
background: linear-gradient(
180deg,
rgba(239, 113, 59, 0.2) 9.34%,
rgba(239, 113, 59, 0.03) 44.16%,
rgba(239, 113, 59, 0) 86.79%
);
}
}
@ -35,9 +58,18 @@
fill: @tab-red;
}
.icon i {
color: @tab-red;
}
&.is-active {
border-top: 1px solid @tab-red;
background: linear-gradient(180deg, rgba(229, 77, 46, 0.20) 9.34%, rgba(229, 77, 46, 0.03) 44.16%, rgba(229, 77, 46, 0.00) 86.79%);
background: linear-gradient(
180deg,
rgba(229, 77, 46, 0.2) 9.34%,
rgba(229, 77, 46, 0.03) 44.16%,
rgba(229, 77, 46, 0) 86.79%
);
}
}
@ -46,9 +78,18 @@
fill: @tab-yellow;
}
&.is-active {
.icon i {
color: @tab-yellow;
}
&.is-active {
border-top: 1px solid @tab-yellow;
background: linear-gradient(180deg, rgba(224, 185, 86, 0.20) 9.34%, rgba(224, 185, 86, 0.03) 44.16%, rgba(224, 185, 86, 0.00) 86.79%);
background: linear-gradient(
180deg,
rgba(224, 185, 86, 0.2) 9.34%,
rgba(224, 185, 86, 0.03) 44.16%,
rgba(224, 185, 86, 0) 86.79%
);
}
}
@ -57,9 +98,18 @@
fill: @tab-blue;
}
.icon i {
color: @tab-blue;
}
&.is-active {
border-top: 1px solid @tab-blue;
background: linear-gradient(180deg, rgba(57, 113, 255, 0.20) 9.34%, rgba(57, 113, 255, 0.03) 44.16%, rgba(57, 113, 255, 0.00) 77.18%);
background: linear-gradient(
180deg,
rgba(57, 113, 255, 0.2) 9.34%,
rgba(57, 113, 255, 0.03) 44.16%,
rgba(57, 113, 255, 0) 77.18%
);
}
}
@ -68,9 +118,18 @@
fill: @tab-mint;
}
.icon i {
color: @tab-mint;
}
&.is-active {
border-top: 1px solid @tab-mint;
background: linear-gradient(180deg, rgba(75, 255, 169, 0.20) 9.34%, rgba(75, 255, 169, 0.03) 44.16%, rgba(75, 255, 169, 0.00) 77.18%);
background: linear-gradient(
180deg,
rgba(75, 255, 169, 0.2) 9.34%,
rgba(75, 255, 169, 0.03) 44.16%,
rgba(75, 255, 169, 0) 77.18%
);
}
}
@ -79,9 +138,18 @@
fill: @tab-cyan;
}
.icon i {
color: @tab-cyan;
}
&.is-active {
border-top: 1px solid @tab-cyan;
background: linear-gradient(180deg, rgba(75, 223, 255, 0.20) 9.34%, rgba(75, 223, 255, 0.03) 44.16%, rgba(58, 186, 214, 0.00) 86.79%);
background: linear-gradient(
180deg,
rgba(75, 223, 255, 0.2) 9.34%,
rgba(75, 223, 255, 0.03) 44.16%,
rgba(58, 186, 214, 0) 86.79%
);
}
}
@ -90,9 +158,18 @@
fill: @tab-white;
}
.icon i {
color: @tab-white;
}
&.is-active {
border-top: 1px solid @tab-white;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.20) 9.34%, rgba(255, 255, 255, 0.03) 44.16%, rgba(255, 255, 255, 0.00) 86.79%);
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.2) 9.34%,
rgba(255, 255, 255, 0.03) 44.16%,
rgba(255, 255, 255, 0) 86.79%
);
}
}
@ -101,9 +178,18 @@
fill: @tab-violet;
}
.icon i {
color: @tab-violet;
}
&.is-active {
border-top: 1px solid @tab-violet;
background: linear-gradient(180deg, rgba(186, 118, 255, 0.20) 9.34%, rgba(186, 118, 255, 0.03) 44.16%, rgba(186, 118, 255, 0.00) 86.79%);
background: linear-gradient(
180deg,
rgba(186, 118, 255, 0.2) 9.34%,
rgba(186, 118, 255, 0.03) 44.16%,
rgba(186, 118, 255, 0) 86.79%
);
}
}
@ -112,9 +198,18 @@
fill: @tab-pink;
}
.icon i {
color: @tab-pink;
}
&.is-active {
border-top: 1px solid @tab-pink;
background: linear-gradient(180deg, rgba(255, 136, 165, 0.20) 9.34%, rgba(255, 136, 165, 0.03) 44.16%, rgba(255, 136, 165, 0.00) 86.79%);
background: linear-gradient(
180deg,
rgba(255, 136, 165, 0.2) 9.34%,
rgba(255, 136, 165, 0.03) 44.16%,
rgba(255, 136, 165, 0) 86.79%
);
}
}

View File

@ -102,7 +102,19 @@ class ScreenTabs extends React.Component<{ session: Session }, {}> {
})();
}
renderTab(screen: Screen, activeScreenId: string, index: number): any {
renderTabIcon = (screen: Screen): React.ReactNode => {
const tabIcon = screen.getTabIcon();
if (tabIcon === "default") {
return <SquareIcon className="icon left-icon" />;
}
return (
<div className="icon">
<i className={`fa-sharp fa-solid fa-${tabIcon}`}></i>
</div>
);
};
renderTab(screen: Screen, activeScreenId: string, index: number): JSX.Element {
let tabIndex = null;
if (index + 1 <= 9) {
tabIndex = <div className="tab-index">{renderCmdText(String(index + 1))}</div>;
@ -132,7 +144,7 @@ class ScreenTabs extends React.Component<{ session: Session }, {}> {
onClick={() => this.handleSwitchScreen(screen.screenId)}
onContextMenu={(event) => this.openScreenSettings(event, screen)}
>
<SquareIcon className="icon left-icon" />
{this.renderTabIcon(screen)}
<div className="tab-name truncate">
{archived}
{webShared}
@ -149,7 +161,7 @@ class ScreenTabs extends React.Component<{ session: Session }, {}> {
if (session == null) {
return null;
}
let screen: Screen = null;
let screen: Screen | null = null;
let index = 0;
let showingScreens = [];
let activeScreenId = session.activeScreenId.get();

View File

@ -95,6 +95,18 @@ const MaxFontSize = 15;
const InputChunkSize = 500;
const RemoteColors = ["red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"];
const TabColors = ["red", "orange", "yellow", "green", "mint", "cyan", "blue", "violet", "pink", "white"];
const TabIcons = [
"sparkle",
"fire",
"ghost",
"cloud",
"compass",
"crown",
"droplet",
"graduation-cap",
"heart",
"file",
];
// @ts-ignore
const VERSION = __WAVETERM_VERSION__;
@ -469,6 +481,15 @@ class Screen {
return tabColor;
}
getTabIcon(): string {
let tabIcon = "default";
let screenOpts = this.opts.get();
if (screenOpts != null && !isBlank(screenOpts.tabicon)) {
tabIcon = screenOpts.tabicon;
}
return tabIcon;
}
getCurRemoteInstance(): RemoteInstanceType {
let session = GlobalModel.getSessionById(this.sessionId);
let rptr = this.curRemote.get();
@ -3492,7 +3513,7 @@ class Model {
submitCommand(
metaCmd: string,
metaSubCmd: string,
args: string[],
args: string[] | null,
kwargs: Record<string, string>,
interactive: boolean
): Promise<CommandRtnType> {
@ -3513,7 +3534,7 @@ class Model {
pk.kwargs,
pk.interactive
);
*/
*/
return this.submitCommandPacket(pk, interactive);
}
@ -3950,10 +3971,10 @@ class CommandRunner {
screenSetSettings(
screenId: string,
settings: { tabcolor?: string; name?: string; sharename?: string },
settings: { tabcolor?: string; tabicon?: string; name?: string; sharename?: string },
interactive: boolean
): Promise<CommandRtnType> {
let kwargs = Object.assign({}, settings);
let kwargs: { [key: string]: any } = Object.assign({}, settings);
kwargs["nohist"] = "1";
kwargs["screen"] = screenId;
return GlobalModel.submitCommand("screen", "set", null, kwargs, interactive);
@ -4169,6 +4190,7 @@ export {
Screen,
riToRPtr,
TabColors,
TabIcons,
RemoteColors,
getTermPtyData,
RemotesModalModel,

View File

@ -50,6 +50,7 @@ type LineType = {
type ScreenOptsType = {
tabcolor?: string;
tabicon?: string;
pterm?: string;
};
@ -167,7 +168,7 @@ type FeCmdPacketType = {
type: string;
metacmd: string;
metasubcmd?: string;
args: string[];
args: string[] | null;
kwargs: Record<string, string>;
rawstr?: string;
uicontext: UIContextType;
@ -632,6 +633,7 @@ type FileInfoType = {
type ExtBlob = Blob & {
notFound: boolean;
name?: string;
};
type ExtFile = File & {

View File

@ -11,6 +11,7 @@
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"isolatedModules": true
"isolatedModules": true,
"experimentalDecorators": true
}
}

View File

@ -72,6 +72,7 @@ const (
)
var ColorNames = []string{"yellow", "blue", "pink", "mint", "cyan", "violet", "orange", "green", "red", "white"}
var TabIcons = []string{"sparkle", "fire", "ghost", "cloud", "compass", "crown", "droplet", "graduation-cap", "heart", "file"}
var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"}
var RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoinstall", "color"}
@ -81,6 +82,7 @@ var GlobalCmds = []string{"session", "screen", "remote", "set", "client", "telem
var SetVarNameMap map[string]string = map[string]string{
"tabcolor": "screen.tabcolor",
"tabicon": "screen.tabicon",
"pterm": "screen.pterm",
"anchor": "screen.anchor",
"focus": "screen.focus",
@ -91,7 +93,7 @@ var SetVarScopes = []SetVarScope{
SetVarScope{ScopeName: "global", VarNames: []string{}},
SetVarScope{ScopeName: "client", VarNames: []string{"telemetry"}},
SetVarScope{ScopeName: "session", VarNames: []string{"name", "pos"}},
SetVarScope{ScopeName: "screen", VarNames: []string{"name", "tabcolor", "pos", "pterm", "anchor", "focus", "line"}},
SetVarScope{ScopeName: "screen", VarNames: []string{"name", "tabcolor", "tabicon", "pos", "pterm", "anchor", "focus", "line"}},
SetVarScope{ScopeName: "line", VarNames: []string{}},
// connection = remote, remote = remoteinstance
SetVarScope{ScopeName: "connection", VarNames: []string{"alias", "connectmode", "key", "password", "autoinstall", "color"}},
@ -757,6 +759,16 @@ func ScreenSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss
varsUpdated = append(varsUpdated, "tabcolor")
setNonAnchor = true
}
if pk.Kwargs["tabicon"] != "" {
icon := pk.Kwargs["tabicon"]
err = validateIcon(icon, "screen tabicon")
if err != nil {
return nil, err
}
updateMap[sstore.ScreenField_TabIcon] = icon
varsUpdated = append(varsUpdated, "tabicon")
setNonAnchor = true
}
if pk.Kwargs["pos"] != "" {
varsUpdated = append(varsUpdated, "pos")
setNonAnchor = true
@ -806,7 +818,7 @@ func ScreenSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss
}
}
if len(varsUpdated) == 0 {
return nil, fmt.Errorf("/screen:set no updates, can set %s", formatStrs([]string{"name", "pos", "tabcolor", "focus", "anchor", "line", "sharename"}, "or", false))
return nil, fmt.Errorf("/screen:set no updates, can set %s", formatStrs([]string{"name", "pos", "tabcolor", "tabicon", "focus", "anchor", "line", "sharename"}, "or", false))
}
screen, err := sstore.UpdateScreen(ctx, ids.ScreenId, updateMap)
if err != nil {
@ -1978,6 +1990,15 @@ func validateColor(color string, typeStr string) error {
return fmt.Errorf("invalid %s, valid colors are: %s", typeStr, formatStrs(ColorNames, "or", false))
}
func validateIcon(icon string, typeStr string) error {
for _, c := range TabIcons {
if icon == c {
return nil
}
}
return fmt.Errorf("invalid %s, valid icons are: %s", typeStr, formatStrs(TabIcons, "or", false))
}
func validateRemoteColor(color string, typeStr string) error {
for _, c := range RemoteColorNames {
if color == c {

View File

@ -1709,6 +1709,7 @@ const (
ScreenField_SelectedLine = "selectedline" // int
ScreenField_Focus = "focustype" // string
ScreenField_TabColor = "tabcolor" // string
ScreenField_TabIcon = "tabicon" // string
ScreenField_PTerm = "pterm" // string
ScreenField_Name = "name" // string
ScreenField_ShareName = "sharename" // string
@ -1743,6 +1744,10 @@ func UpdateScreen(ctx context.Context, screenId string, editMap map[string]inter
query = `UPDATE screen SET screenopts = json_set(screenopts, '$.tabcolor', ?) WHERE screenid = ?`
tx.Exec(query, tabColor, screenId)
}
if tabIcon, found := editMap[ScreenField_TabIcon]; found {
query = `UPDATE screen SET screenopts = json_set(screenopts, '$.tabicon', ?) WHERE screenid = ?`
tx.Exec(query, tabIcon, screenId)
}
if pterm, found := editMap[ScreenField_PTerm]; found {
query = `UPDATE screen SET screenopts = json_set(screenopts, '$.pterm', ?) WHERE screenid = ?`
tx.Exec(query, pterm, screenId)

View File

@ -433,6 +433,7 @@ func (h *HistoryItemType) FromMap(m map[string]interface{}) bool {
type ScreenOptsType struct {
TabColor string `json:"tabcolor,omitempty"`
TabIcon string `json:"tabicon,omitempty"`
PTerm string `json:"pterm,omitempty"`
}

1416
yarn.lock

File diff suppressed because it is too large Load Diff