Compare commits

..

8 Commits

Author SHA1 Message Date
Sylvia Crowe d1bffb5a0d merge branch 'main' into use-ssh-library 2024-01-29 13:01:50 -08:00
Sylvia Crowe df94b91e6c feat: create backend for user input requests
This is the first part of a change that allows the backend to request
user input from the frontend. Essentially, the backend will send a
request for the user to answer some query, and the frontend will send
that answer back. It is blocking, so it needs to be used within a
goroutine.

There is some placeholder code in the frontend that will be updated in
future commits. Similarly, there is some debug code in the backend
remote.go file.
2024-01-29 12:43:47 -08:00
Sylvia Crowe 1547fc856e fix: allow retry after ssh auth failure
Previously, the error status was not set when an ssh connection failed.
Because of this, an ssh connection failure would lock the failed remote
until waveterm was rebooted. This fix properly sets the error status so
this cannot happen.
2024-01-24 20:54:37 -08:00
Sylvia Crowe 37821738d8 allow switching between new and old ssh for dev
It is inconvenient to create milestones without being able to merge into
the main branch. But due to the experimental nature of the ssh changes,
it is not desired to use these changes in the main branch yet. This
change disables the new ssh launcher by default. It can be used by
changing the UseSshLibrary constant to true in remote.go. With this, it
becomes possible to merge these changes into the main branch without
them being used in production.
2024-01-24 20:18:27 -08:00
Sylvia Crowe 8e79eeccca merge branch 'main' into use-ssh-library 2024-01-24 18:29:20 -08:00
Sylvia Crowe 203f1f7505 clean up old mshell launch methods
In the debugging the addition of the ssh library, i had several versions
of the MShellProc Launch function. Since this seems mostly stable, I
have removed the old version and the experimental version in favor of
the combined version.
2024-01-24 17:19:52 -08:00
Sylvia Crowe 57a7641f82 add password and keyboard-interactive ssh auth
This adds several new ssh auth methods. In addition to the PublicKey
method used previously, this adds password authentication,
keyboard-interactive authentication, and PublicKey+Passphrase
authentication.

Furthermore, it refactores the ssh connection code into its own wavesrv
file rather than storing int in waveshell's shexec file.
2024-01-24 17:16:22 -08:00
Sylvia Crowe 03ea444030 create proof of concept ssh library integration
This is a first attempt to integrate the golang crypto/ssh library for
handling remote connections. As it stands, this features is limited to
identity files without passphrases. It needs to be expanded to include
key+passphrase and password verifications as well.
2024-01-22 23:18:49 -08:00
42 changed files with 511 additions and 1072 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
name: "Build Helper"
on: workflow_dispatch
env:
WAVETERM_VERSION: 0.6.3
WAVETERM_VERSION: 0.6.1
GO_VERSION: "1.21.5"
NODE_VERSION: "21.5.0"
jobs:
+2 -19
View File
@@ -1,21 +1,4 @@
{
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[less]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
+1 -1
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# assumes we have Wave-darwin-x64-[version].zip and Wave-darwin-arm64-[version].zip in current directory
VERSION=0.6.3
VERSION=0.6.1
rm -rf temp
rm -rf builds
mkdir temp
-1
View File
@@ -1,4 +1,3 @@
github.com/aws/aws-sdk-go-v2/service/s3 v1.27.11 h1:3/gm/JTX9bX8CpzTgIlrtYpB3EVBDxyg/GY/QdcIEZw=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "waveterm",
"author": "Command Line Inc",
"productName": "Wave",
"version": "0.6.3",
"version": "0.6.1",
"main": "dist/emain.js",
"license": "Apache-2.0",
"dependencies": {
+4 -12
View File
@@ -30,7 +30,6 @@ type OV<V> = mobx.IObservableValue<V>;
@mobxReact.observer
class App extends React.Component<{}, {}> {
dcWait: OV<boolean> = mobx.observable.box(false, { name: "dcWait" });
mainContentRef: React.RefObject<HTMLDivElement> = React.createRef();
constructor(props: any) {
super(props);
@@ -76,13 +75,6 @@ class App extends React.Component<{}, {}> {
let hasClientStop = GlobalModel.getHasClientStop();
let dcWait = this.dcWait.get();
let platform = GlobalModel.getPlatform();
let clientData = GlobalModel.clientData.get();
// 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.
if (clientData == null) {
return null;
}
if (disconnected || hasClientStop) {
if (!dcWait) {
@@ -90,8 +82,8 @@ class App extends React.Component<{}, {}> {
}
return (
<div id="main" className={"platform-" + platform} onContextMenu={this.handleContextMenu}>
<div ref={this.mainContentRef} className="main-content">
<MainSideBar parentRef={this.mainContentRef} clientData={clientData} />
<div className="main-content">
<MainSideBar />
<div className="session-view" />
</div>
<If condition={dcWait}>
@@ -110,8 +102,8 @@ class App extends React.Component<{}, {}> {
}
return (
<div id="main" className={"platform-" + platform} onContextMenu={this.handleContextMenu}>
<div ref={this.mainContentRef} className="main-content">
<MainSideBar parentRef={this.mainContentRef} clientData={clientData} />
<div className="main-content">
<MainSideBar />
<ErrorBoundary>
<PluginsView />
<WorkspaceView />
+2 -164
View File
@@ -9,12 +9,11 @@ import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import cn from "classnames";
import { If } from "tsx-control-statements/components";
import { RemoteType } from "../../types/types";
import { RemoteType, StatusIndicatorLevel } from "../../types/types";
import ReactDOM from "react-dom";
import { GlobalModel, GlobalCommandRunner } from "../../model/model";
import { GlobalModel } from "../../model/model";
import * as appconst from "../appconst";
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../util/keyutil";
import { MagicLayout } from "../magiclayout";
import { ReactComponent as CheckIcon } from "../assets/icons/line/check.svg";
import { ReactComponent as CopyIcon } from "../assets/icons/history/copy.svg";
@@ -1266,166 +1265,6 @@ In order to use Wave's advanced features like unified history and persistent ses
});
}
interface ResizableSidebarProps {
parentRef: React.RefObject<HTMLElement>;
position: "left" | "right";
enableSnap?: boolean;
className?: string;
children?: (toggleCollapsed: () => void) => React.ReactNode;
toggleCollapse?: () => void;
}
@mobxReact.observer
class ResizableSidebar extends React.Component<ResizableSidebarProps> {
resizeStartWidth: number = 0;
startX: number = 0;
prevDelta: number = 0;
prevDragDirection: string = null;
disposeReaction: any;
@boundMethod
startResizing(event: React.MouseEvent<HTMLDivElement>) {
event.preventDefault();
const { parentRef, position } = this.props;
const parentRect = parentRef.current?.getBoundingClientRect();
if (!parentRect) return;
if (position === "right") {
this.startX = parentRect.right - event.clientX;
} else {
this.startX = event.clientX - parentRect.left;
}
const mainSidebarModel = GlobalModel.mainSidebarModel;
const collapsed = mainSidebarModel.getCollapsed();
this.resizeStartWidth = mainSidebarModel.getWidth();
document.addEventListener("mousemove", this.onMouseMove);
document.addEventListener("mouseup", this.stopResizing);
document.body.style.cursor = "col-resize";
mobx.action(() => {
mainSidebarModel.setTempWidthAndTempCollapsed(this.resizeStartWidth, collapsed);
mainSidebarModel.isDragging.set(true);
})();
}
@boundMethod
onMouseMove(event: MouseEvent) {
event.preventDefault();
const { parentRef, enableSnap, position } = this.props;
const parentRect = parentRef.current?.getBoundingClientRect();
const mainSidebarModel = GlobalModel.mainSidebarModel;
if (!mainSidebarModel.isDragging.get() || !parentRect) return;
let delta: number, newWidth: number;
if (position === "right") {
delta = parentRect.right - event.clientX - this.startX;
} else {
delta = event.clientX - parentRect.left - this.startX;
}
newWidth = this.resizeStartWidth + delta;
if (enableSnap) {
const minWidth = MagicLayout.MainSidebarMinWidth;
const snapPoint = minWidth + MagicLayout.MainSidebarSnapThreshold;
const dragResistance = MagicLayout.MainSidebarDragResistance;
let dragDirection: string;
if (delta - this.prevDelta > 0) {
dragDirection = "+";
} else if (delta - this.prevDelta == 0) {
if (this.prevDragDirection == "+") {
dragDirection = "+";
} else {
dragDirection = "-";
}
} else {
dragDirection = "-";
}
this.prevDelta = delta;
this.prevDragDirection = dragDirection;
if (newWidth - dragResistance > minWidth && newWidth < snapPoint && dragDirection == "+") {
newWidth = snapPoint;
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, false);
} else if (newWidth + dragResistance < snapPoint && dragDirection == "-") {
newWidth = minWidth;
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, true);
} else if (newWidth > snapPoint) {
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, false);
}
} else {
if (newWidth <= MagicLayout.MainSidebarMinWidth) {
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, true);
} else {
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, false);
}
}
}
@boundMethod
stopResizing() {
let mainSidebarModel = GlobalModel.mainSidebarModel;
GlobalCommandRunner.clientSetSidebar(
mainSidebarModel.tempWidth.get(),
mainSidebarModel.tempCollapsed.get()
).finally(() => {
mobx.action(() => {
mainSidebarModel.isDragging.set(false);
})();
});
document.removeEventListener("mousemove", this.onMouseMove);
document.removeEventListener("mouseup", this.stopResizing);
document.body.style.cursor = "";
}
@boundMethod
toggleCollapsed() {
const mainSidebarModel = GlobalModel.mainSidebarModel;
const tempCollapsed = mainSidebarModel.getCollapsed();
const width = mainSidebarModel.getWidth(true);
mainSidebarModel.setTempWidthAndTempCollapsed(width, !tempCollapsed);
GlobalCommandRunner.clientSetSidebar(width, !tempCollapsed);
}
render() {
const { className, children } = this.props;
const mainSidebarModel = GlobalModel.mainSidebarModel;
const width = mainSidebarModel.getWidth();
const isCollapsed = mainSidebarModel.getCollapsed();
return (
<div className={cn("sidebar", className, { collapsed: isCollapsed })} style={{ width }}>
<div className="sidebar-content">{children(this.toggleCollapsed)}</div>
<div
className="sidebar-handle"
style={{
position: "absolute",
top: 0,
[this.props.position === "left" ? "right" : "left"]: 0,
bottom: 0,
width: "5px",
cursor: "col-resize",
}}
onMouseDown={this.startResizing}
onDoubleClick={this.toggleCollapsed}
></div>
</div>
);
}
}
export {
CmdStrCode,
Toggle,
@@ -1447,6 +1286,5 @@ export {
LinkButton,
Status,
Modal,
ResizableSidebar,
ShowWaveShellInstallPrompt,
};
+1 -21
View File
@@ -48,33 +48,13 @@
}
}
/*
The following accounts for a debounce in the status indicator. We will only display the status indicator icon if the parent indicates that it should be visible AND one of the following is true:
1. There is a status to be shown.
2. There is a spinner to be shown and the required delay has passed.
*/
.status-indicator-visible {
&.spinner-visible,
&.output,
&.error,
&.success {
.positional-icon-visible;
}
// This is set by the timeout in the status indicator component.
&.spinner-visible {
#spinner {
visibility: visible;
}
}
}
.status-indicator {
#spinner,
#indicator {
visibility: hidden;
}
.spin #spinner {
visibility: visible;
stroke: @term-white;
}
&.error #indicator {
+10 -171
View File
@@ -2,27 +2,17 @@ import React from "react";
import { StatusIndicatorLevel } from "../../../types/types";
import cn from "classnames";
import { ReactComponent as SpinnerIndicator } from "../../assets/icons/spinner-indicator.svg";
import { boundMethod } from "autobind-decorator";
import * as mobx from "mobx";
import * as mobxReact from "mobx-react";
import { ReactComponent as RotateIconSvg } from "../../assets/icons/line/rotate.svg";
interface PositionalIconProps {
children?: React.ReactNode;
className?: string;
onClick?: React.MouseEventHandler<HTMLDivElement>;
divRef?: React.RefObject<HTMLDivElement>;
}
export class FrontIcon extends React.Component<PositionalIconProps> {
render() {
return (
<div
ref={this.props.divRef}
className={cn("front-icon", "positional-icon", this.props.className)}
onClick={this.props.onClick}
>
<div className={cn("front-icon", "positional-icon", this.props.className)}>
<div className="positional-icon-inner">{this.props.children}</div>
</div>
);
@@ -32,11 +22,7 @@ export class FrontIcon extends React.Component<PositionalIconProps> {
export class CenteredIcon extends React.Component<PositionalIconProps> {
render() {
return (
<div
ref={this.props.divRef}
className={cn("centered-icon", "positional-icon", this.props.className)}
onClick={this.props.onClick}
>
<div className={cn("centered-icon", "positional-icon", this.props.className)} onClick={this.props.onClick}>
<div className="positional-icon-inner">{this.props.children}</div>
</div>
);
@@ -57,182 +43,35 @@ export class ActionsIcon extends React.Component<ActionsIconProps> {
}
}
class SyncSpin extends React.Component<{
classRef?: React.RefObject<HTMLDivElement>;
children?: React.ReactNode;
shouldSync?: boolean;
}> {
listenerAdded: boolean = false;
componentDidMount() {
this.syncSpinner();
}
componentDidUpdate() {
this.syncSpinner();
}
componentWillUnmount(): void {
const classRef = this.props.classRef;
if (classRef.current != null && this.listenerAdded) {
const elem = classRef.current;
const svgElem = elem.querySelector("svg");
if (svgElem != null) {
svgElem.removeEventListener("animationstart", this.handleAnimationStart);
}
}
}
@boundMethod
handleAnimationStart(e: AnimationEvent) {
const classRef = this.props.classRef;
if (classRef.current == null) {
return;
}
const svgElem = classRef.current.querySelector("svg");
if (svgElem == null) {
return;
}
const animArr = svgElem.getAnimations();
if (animArr == null || animArr.length == 0) {
return;
}
animArr[0].startTime = 0;
}
syncSpinner() {
const { classRef, shouldSync } = this.props;
const shouldSyncVal = shouldSync ?? true;
if (!shouldSyncVal || classRef.current == null) {
return;
}
const elem = classRef.current;
const svgElem = elem.querySelector("svg");
if (svgElem == null) {
return;
}
if (!this.listenerAdded) {
svgElem.addEventListener("animationstart", this.handleAnimationStart);
this.listenerAdded = true;
}
const animArr = svgElem.getAnimations();
if (animArr == null || animArr.length == 0) {
return;
}
animArr[0].startTime = 0;
}
render() {
return this.props.children;
}
}
interface StatusIndicatorProps {
/**
* The level of the status indicator. This will determine the color of the status indicator.
*/
level: StatusIndicatorLevel;
className?: string;
/**
* If true, a spinner will be shown around the status indicator.
*/
runningCommands?: boolean;
}
/**
* This component is used to show the status of a command. It will show a spinner around the status indicator if there are running commands. It will also delay showing the spinner for a short time to prevent flickering.
*/
@mobxReact.observer
export class StatusIndicator extends React.Component<StatusIndicatorProps> {
iconRef: React.RefObject<HTMLDivElement> = React.createRef();
spinnerVisible: mobx.IObservableValue<boolean> = mobx.observable.box(false);
timeout: NodeJS.Timeout;
clearSpinnerTimeout() {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
mobx.action(() => {
this.spinnerVisible.set(false);
})();
}
/**
* This will apply a delay after there is a running command before showing the spinner. This prevents flickering for commands that return quickly.
*/
updateMountCallback() {
const runningCommands = this.props.runningCommands ?? false;
if (runningCommands && !this.timeout) {
this.timeout = setTimeout(
mobx.action(() => {
this.spinnerVisible.set(true);
}),
100
);
} else if (!runningCommands) {
this.clearSpinnerTimeout();
}
}
componentDidUpdate(): void {
this.updateMountCallback();
}
componentDidMount(): void {
this.updateMountCallback();
}
componentWillUnmount(): void {
this.clearSpinnerTimeout();
}
render() {
const { level, className, runningCommands } = this.props;
const spinnerVisible = this.spinnerVisible.get();
let statusIndicator = null;
if (level != StatusIndicatorLevel.None || spinnerVisible) {
let indicatorLevelClass = null;
if (level != StatusIndicatorLevel.None || runningCommands) {
let levelClass = null;
switch (level) {
case StatusIndicatorLevel.Output:
indicatorLevelClass = "output";
levelClass = "output";
break;
case StatusIndicatorLevel.Success:
indicatorLevelClass = "success";
levelClass = "success";
break;
case StatusIndicatorLevel.Error:
indicatorLevelClass = "error";
levelClass = "error";
break;
}
const spinnerVisibleClass = spinnerVisible ? "spinner-visible" : null;
statusIndicator = (
<CenteredIcon
divRef={this.iconRef}
className={cn(className, indicatorLevelClass, spinnerVisibleClass, "status-indicator")}
>
<SpinnerIndicator className={spinnerVisible ? "spin" : null} />
<CenteredIcon className={cn(className, levelClass, "status-indicator")}>
<SpinnerIndicator className={runningCommands ? "spin" : null} />
</CenteredIcon>
);
}
return (
<SyncSpin classRef={this.iconRef} shouldSync={runningCommands}>
{statusIndicator}
</SyncSpin>
);
}
}
export class RotateIcon extends React.Component<{
className?: string;
onClick?: React.MouseEventHandler<HTMLDivElement>;
}> {
iconRef: React.RefObject<HTMLDivElement> = React.createRef();
render() {
return (
<SyncSpin classRef={this.iconRef}>
<RotateIconSvg className={this.props.className ?? ""} />
</SyncSpin>
);
return statusIndicator;
}
}
@@ -20,12 +20,6 @@
width: 100%;
}
.import-edit-warning {
display: flex;
flex-direction: row;
align-items: flex-start;
}
.name-actions-section {
margin-bottom: 10px;
display: flex;
+3 -30
View File
@@ -58,10 +58,6 @@ class EditRemoteConnModal extends React.Component<{}, {}> {
return this.selectedRemote?.local;
}
isImportedRemote(): boolean {
return this.selectedRemote?.sshconfigsrc == "sshconfig-import";
}
componentDidMount(): void {
mobx.action(() => {
this.tempAlias.set(this.selectedRemote?.remotealias);
@@ -263,27 +259,6 @@ class EditRemoteConnModal extends React.Component<{}, {}> {
);
}
renderImportedRemoteEditWarning() {
return (
<div className="import-edit-warning">
<Tooltip
message={
<span>
Most options for connections imported from an ssh config file cannot be edited. For these
changes, you must edit the config file and import it again. The shell preference can be
edited, but will return to the default if you import again. It will stay changed if you
follow <a href="https://docs.waveterm.dev/features/sshconfig-imports">this procedure</a>.
</span>
}
icon={<i className="fa-sharp fa-regular fa-fw fa-triangle-exclamation" />}
>
<i className="fa-sharp fa-regular fa-fw fa-triangle-exclamation" />
</Tooltip>
&nbsp;SSH Config Import Behavior
</div>
);
}
renderAuthMode() {
let authMode = this.tempAuthMode.get();
return (
@@ -369,7 +344,6 @@ class EditRemoteConnModal extends React.Component<{}, {}> {
return null;
}
let isLocal = this.isLocalRemote();
let isImported = this.isImportedRemote();
return (
<Modal className="erconn-modal">
<Modal.Header title="Edit Connection" onClose={this.model.closeModal} />
@@ -377,10 +351,9 @@ class EditRemoteConnModal extends React.Component<{}, {}> {
<div className="name-actions-section">
<div className="name text-primary">{util.getRemoteName(this.selectedRemote)}</div>
</div>
<If condition={!isLocal && !isImported}>{this.renderAlias()}</If>
<If condition={!isLocal && !isImported}>{this.renderAuthMode()}</If>
<If condition={!isLocal && !isImported}>{this.renderConnectMode()}</If>
<If condition={isImported}>{this.renderImportedRemoteEditWarning()}</If>
<If condition={!isLocal}>{this.renderAlias()}</If>
<If condition={!isLocal}>{this.renderAuthMode()}</If>
<If condition={!isLocal}>{this.renderConnectMode()}</If>
{this.renderShellPref()}
<If condition={!util.isBlank(this.remoteEdit?.errorstr)}>
<div className="settings-field settings-error">Error: {this.remoteEdit?.errorstr}</div>
+12 -6
View File
@@ -49,7 +49,6 @@ type OV<V> = mobx.IObservableValue<V>;
type OArr<V> = mobx.IObservableArray<V>;
const RemotePtyRows = 9;
const RemotePtyTotalRows = 25;
const RemotePtyCols = 80;
const NumOfLines = 50;
const PasswordUnchangedSentinel = "--unchanged--";
@@ -1027,6 +1026,17 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
cancelInstallButton = <></>;
}
if (remote.sshconfigsrc == "sshconfig-import") {
updateAuthButton = (
<Button theme="secondary" disabled={true}>
Edit
<Tooltip
message={`Connections imported from an ssh config file cannot be edited inside waveterm. To edit these, you must edit the config file and import it again.`}
icon={<i className="fa-sharp fa-regular fa-fw fa-ban" />}
>
<i className="fa-sharp fa-regular fa-fw fa-ban" />
</Tooltip>
</Button>
);
archiveButton = (
<Button theme="secondary" onClick={() => this.clickArchive()}>
Delete
@@ -1185,11 +1195,7 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
ref={this.termRef}
data-remoteid={remote.remoteid}
style={{
height: textmeasure.termHeightFromRows(
RemotePtyRows,
termFontSize,
RemotePtyTotalRows
),
height: textmeasure.termHeightFromRows(RemotePtyRows, termFontSize),
width: termWidth,
}}
></div>
+12 -6
View File
@@ -16,7 +16,6 @@ import * as textmeasure from "../../../util/textmeasure";
import "./viewremoteconndetail.less";
const RemotePtyRows = 9;
const RemotePtyTotalRows = 25;
const RemotePtyCols = 80;
@mobxReact.observer
@@ -210,6 +209,17 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
cancelInstallButton = <></>;
}
if (remote.sshconfigsrc == "sshconfig-import") {
updateAuthButton = (
<Button theme="secondary" disabled={true}>
Edit
<Tooltip
message={`Connections imported from an ssh config file cannot be edited inside waveterm. To edit these, you must edit the config file and import it again.`}
icon={<i className="fa-sharp fa-regular fa-fw fa-ban" />}
>
<i className="fa-sharp fa-regular fa-fw fa-ban" />
</Tooltip>
</Button>
);
archiveButton = (
<Button theme="secondary" onClick={() => this.clickArchive()}>
Delete
@@ -372,11 +382,7 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
ref={this.termRef}
data-remoteid={remote.remoteid}
style={{
height: textmeasure.termHeightFromRows(
RemotePtyRows,
termFontSize,
RemotePtyTotalRows
),
height: textmeasure.termHeightFromRows(RemotePtyRows, termFontSize),
width: termWidth,
}}
></div>
@@ -29,7 +29,6 @@ type OArr<V> = mobx.IObservableArray<V>;
type OMap<K, V> = mobx.ObservableMap<K, V>;
const RemotePtyRows = 8;
const RemotePtyTotalRows = 25;
const RemotePtyCols = 80;
const PasswordUnchangedSentinel = "--unchanged--";
@@ -1050,7 +1049,7 @@ class RemoteDetailView extends React.Component<{ model: RemotesModalModel; remot
ref={this.termRef}
data-remoteid={remote.remoteid}
style={{
height: textmeasure.termHeightFromRows(RemotePtyRows, termFontSize, RemotePtyTotalRows),
height: textmeasure.termHeightFromRows(RemotePtyRows, termFontSize),
width: termWidth,
}}
></div>
+90 -82
View File
@@ -13,6 +13,8 @@ import { GlobalModel, GlobalCommandRunner, Cmd, getTermPtyData } from "../../mod
import { termHeightFromRows } from "../../util/textmeasure";
import type {
LineType,
RemoteType,
RemotePtrType,
RenderModeType,
RendererOpts,
RendererPluginType,
@@ -22,6 +24,11 @@ import type {
} from "../../types/types";
import cn from "classnames";
import { ReactComponent as FavoritesIcon } from "../assets/icons/favourites.svg";
import { ReactComponent as PinIcon } from "../assets/icons/pin.svg";
import { ReactComponent as PlusIcon } from "../assets/icons/plus.svg";
import { ReactComponent as MinusIcon } from "../assets/icons/minus.svg";
import type { LineContainerModel } from "../../model/model";
import { renderCmdText } from "../common/common";
import { SimpleBlobRenderer } from "../../plugins/core/basicrenderer";
@@ -37,13 +44,12 @@ import * as appconst from "../appconst";
import { ReactComponent as CheckIcon } from "../assets/icons/line/check.svg";
import { ReactComponent as CommentIcon } from "../assets/icons/line/comment.svg";
import { ReactComponent as QuestionIcon } from "../assets/icons/line/question.svg";
import { ReactComponent as RotateIcon } from "../assets/icons/line/rotate.svg";
import { ReactComponent as WarningIcon } from "../assets/icons/line/triangle-exclamation.svg";
import { ReactComponent as XmarkIcon } from "../assets/icons/line/xmark.svg";
import { ReactComponent as FillIcon } from "../assets/icons/line/fill.svg";
import { ReactComponent as GearIcon } from "../assets/icons/line/gear.svg";
import { RotateIcon } from "../common/icons/icons";
import "./lines.less";
dayjs.extend(localizedFormat);
@@ -53,12 +59,12 @@ type OV<V> = mobx.IObservableValue<V>;
@mobxReact.observer
class SmallLineAvatar extends React.Component<{ line: LineType; cmd: Cmd; onRightClick?: (e: any) => void }, {}> {
render() {
const { line, cmd } = this.props;
const lineNumStr = (line.linenumtemp ? "~" : "#") + String(line.linenum);
const status = cmd != null ? cmd.getStatus() : "done";
const rtnstate = cmd != null ? cmd.getRtnState() : false;
const exitcode = cmd != null ? cmd.getExitCode() : 0;
const isComment = line.linetype == "text";
let { line, cmd } = this.props;
let lineNumStr = (line.linenumtemp ? "~" : "#") + String(line.linenum);
let status = cmd != null ? cmd.getStatus() : "done";
let rtnstate = cmd != null ? cmd.getRtnState() : false;
let exitcode = cmd != null ? cmd.getExitCode() : 0;
let isComment = line.linetype == "text";
let icon = null;
let iconTitle = null;
if (isComment) {
@@ -136,7 +142,7 @@ class LineCmd extends React.Component<
}
checkStateDiffLoad(): void {
const { screen, line, staticRender, visible } = this.props;
let { screen, line, staticRender, visible } = this.props;
if (staticRender) {
return;
}
@@ -147,7 +153,7 @@ class LineCmd extends React.Component<
}
return;
}
const cmd = screen.getCmd(line);
let cmd = screen.getCmd(line);
if (cmd == null || !cmd.getRtnState() || this.rtnStateDiffFetched) {
return;
}
@@ -161,15 +167,15 @@ class LineCmd extends React.Component<
if (this.rtnStateDiffFetched) {
return;
}
const { line } = this.props;
let { line } = this.props;
this.rtnStateDiffFetched = true;
const usp = new URLSearchParams({
let usp = new URLSearchParams({
linenum: String(line.linenum),
screenid: line.screenid,
lineid: line.lineid,
});
const url = GlobalModel.getBaseHostPort() + "/api/rtnstate?" + usp.toString();
const fetchHeaders = GlobalModel.getFetchHeaders();
let url = GlobalModel.getBaseHostPort() + "/api/rtnstate?" + usp.toString();
let fetchHeaders = GlobalModel.getFetchHeaders();
fetch(url, { headers: fetchHeaders })
.then((resp) => {
if (!resp.ok) {
@@ -227,7 +233,7 @@ class LineCmd extends React.Component<
</React.Fragment>
);
}
const isMultiLine = lineutil.isMultiLineCmdText(cmd.getCmdStr());
let isMultiLine = lineutil.isMultiLineCmdText(cmd.getCmdStr());
return (
<div key="meta2" className="meta meta-line2" ref={this.cmdTextRef}>
<div className="metapart-mono cmdtext">
@@ -246,7 +252,7 @@ class LineCmd extends React.Component<
// TODO: this might not be necessary anymore because we're using this.lastHeight
getSnapshotBeforeUpdate(prevProps, prevState): { height: number } {
const elem = this.lineRef.current;
let elem = this.lineRef.current;
if (elem == null) {
return { height: 0 };
}
@@ -260,25 +266,25 @@ class LineCmd extends React.Component<
}
checkCmdText() {
const metaElem = this.cmdTextRef.current;
let metaElem = this.cmdTextRef.current;
if (metaElem == null || metaElem.childNodes.length == 0) {
return;
}
const metaElemWidth = metaElem.offsetWidth;
let metaElemWidth = metaElem.offsetWidth;
if (metaElemWidth == 0) {
return;
}
const metaChild = metaElem.firstChild;
let metaChild = metaElem.firstChild;
if (metaChild == null) {
return;
}
const children = metaChild.childNodes;
let children = metaChild.childNodes;
let childWidth = 0;
for (let i = 0; i < children.length; i++) {
let ch = children[i];
childWidth += ch.offsetWidth;
}
const isOverflow = childWidth > metaElemWidth;
let isOverflow = childWidth > metaElemWidth;
if (isOverflow && isOverflow != this.isOverflow.get()) {
mobx.action(() => {
this.isOverflow.set(isOverflow);
@@ -291,16 +297,16 @@ class LineCmd extends React.Component<
if (this.props.onHeightChange == null) {
return;
}
const { line } = this.props;
let { line } = this.props;
let curHeight = 0;
const elem = this.lineRef.current;
let elem = this.lineRef.current;
if (elem != null) {
curHeight = elem.offsetHeight;
}
if (this.lastHeight == curHeight) {
return;
}
const lastHeight = this.lastHeight;
let lastHeight = this.lastHeight;
this.lastHeight = curHeight;
this.props.onHeightChange(line.linenum, curHeight, lastHeight);
// console.log("line height change: ", line.linenum, lastHeight, "=>", curHeight);
@@ -308,13 +314,13 @@ class LineCmd extends React.Component<
@boundMethod
handleClick() {
const { line, noSelect } = this.props;
let { line, noSelect } = this.props;
if (noSelect) {
return;
}
const sel = window.getSelection();
let sel = window.getSelection();
if (this.lineRef.current != null) {
const selText = sel.toString();
let selText = sel.toString();
if (sel.anchorNode != null && this.lineRef.current.contains(sel.anchorNode) && !isBlank(selText)) {
return;
}
@@ -324,7 +330,7 @@ class LineCmd extends React.Component<
@boundMethod
clickStar() {
const { line } = this.props;
let { line } = this.props;
if (!line.star || line.star == 0) {
GlobalCommandRunner.lineStar(line.lineid, 1);
} else {
@@ -334,7 +340,7 @@ class LineCmd extends React.Component<
@boundMethod
clickPin() {
const { line } = this.props;
let { line } = this.props;
if (!line.pinned) {
GlobalCommandRunner.linePin(line.lineid, true);
} else {
@@ -344,19 +350,19 @@ class LineCmd extends React.Component<
@boundMethod
clickBookmark() {
const { line } = this.props;
let { line } = this.props;
GlobalCommandRunner.lineBookmark(line.lineid);
}
@boundMethod
clickDelete() {
const { line } = this.props;
let { line } = this.props;
GlobalCommandRunner.lineDelete(line.lineid, true);
}
@boundMethod
clickRestart() {
const { line } = this.props;
let { line } = this.props;
GlobalCommandRunner.lineRestart(line.lineid, true);
}
@@ -369,7 +375,7 @@ class LineCmd extends React.Component<
@boundMethod
clickMoveToSidebar() {
const { line } = this.props;
let { line } = this.props;
GlobalCommandRunner.screenSidebarAddLine(line.lineid);
}
@@ -384,29 +390,29 @@ class LineCmd extends React.Component<
}
getIsHidePrompt(): boolean {
const { line } = this.props;
let { line } = this.props;
let rendererPlugin: RendererPluginType = null;
const isNoneRenderer = line.renderer == "none";
let isNoneRenderer = line.renderer == "none";
if (!isBlank(line.renderer) && line.renderer != "terminal" && !isNoneRenderer) {
rendererPlugin = PluginModel.getRendererPluginByName(line.renderer);
}
const hidePrompt = rendererPlugin?.hidePrompt;
let hidePrompt = rendererPlugin != null && rendererPlugin.hidePrompt;
return hidePrompt;
}
getTerminalRendererHeight(cmd: Cmd): number {
const { screen, line, width } = this.props;
let { screen, line, width, renderMode } = this.props;
let height = 45 + 24; // height of zero height terminal
const usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width);
let usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width);
if (usedRows > 0) {
height = 48 + 24 + termHeightFromRows(usedRows, GlobalModel.termFontSize.get(), cmd.getTermMaxRows());
height = 48 + 24 + termHeightFromRows(usedRows, GlobalModel.termFontSize.get());
}
return height;
}
@boundMethod
onAvatarRightClick(e: any): void {
const { line, noSelect } = this.props;
let { line, noSelect } = this.props;
if (noSelect) {
return;
}
@@ -420,20 +426,20 @@ class LineCmd extends React.Component<
}
renderSimple() {
const { screen, line } = this.props;
const cmd = screen.getCmd(line);
let { screen, line } = this.props;
let cmd = screen.getCmd(line);
let height: number = 0;
if (isBlank(line.renderer) || line.renderer == "terminal") {
height = this.getTerminalRendererHeight(cmd);
} else {
// header is 16px tall with hide-prompt, 36px otherwise
const { screen, line, width } = this.props;
const hidePrompt = this.getIsHidePrompt();
const usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width);
let { screen, line, width } = this.props;
let hidePrompt = this.getIsHidePrompt();
let usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width);
height = (hidePrompt ? 16 + 6 : 36 + 6) + usedRows;
}
const formattedTime = lineutil.getLineDateTimeStr(line.ts);
const mainDivCn = cn("line", "line-cmd", "line-simple");
let formattedTime = lineutil.getLineDateTimeStr(line.ts);
let mainDivCn = cn("line", "line-cmd", "line-simple");
return (
<div
className={mainDivCn}
@@ -473,16 +479,15 @@ class LineCmd extends React.Component<
if (restartTs != null && restartTs > 0) {
formattedTime = "restarted @ " + lineutil.getLineDateTimeStr(restartTs);
timeTitle = "original start time " + lineutil.getLineDateTimeStr(line.ts);
} else {
}
else {
formattedTime = lineutil.getLineDateTimeStr(line.ts);
}
let renderer = line.renderer;
return (
<div key="meta1" className="meta meta-line1">
<SmallLineAvatar line={line} cmd={cmd} />
<div title={timeTitle} className="ts">
{formattedTime}
</div>
<div title={timeTitle} className="ts">{formattedTime}</div>
<div>&nbsp;</div>
<If condition={!isBlank(renderer) && renderer != "terminal"}>
<div className="renderer">
@@ -501,7 +506,7 @@ class LineCmd extends React.Component<
}
getRendererOpts(cmd: Cmd): RendererOpts {
const { screen } = this.props;
let { screen } = this.props;
return {
maxSize: screen.getMaxContentSize(),
idealSize: screen.getIdealContentSize(),
@@ -511,9 +516,9 @@ class LineCmd extends React.Component<
}
makeRendererModelInitializeParams(): RendererModelInitializeParams {
const { screen, line } = this.props;
const context = lineutil.getRendererContext(line);
const cmd = screen.getCmd(line); // won't be null
let { screen, line } = this.props;
let context = lineutil.getRendererContext(line);
let cmd = screen.getCmd(line); // won't be null
let savedHeight = screen.getContentHeight(context);
if (savedHeight == null) {
if (line.contentheight != null && line.contentheight != -1) {
@@ -522,7 +527,7 @@ class LineCmd extends React.Component<
savedHeight = 0;
}
}
const api = {
let api = {
saveHeight: (height: number) => {
screen.setContentHeight(lineutil.getRendererContext(line), height);
},
@@ -576,12 +581,12 @@ class LineCmd extends React.Component<
};
render() {
const { screen, line, width, staticRender, visible } = this.props;
const isVisible = visible.get();
let { screen, line, width, staticRender, visible } = this.props;
let isVisible = visible.get();
if (staticRender || !isVisible) {
return this.renderSimple();
}
const cmd = screen.getCmd(line);
let cmd = screen.getCmd(line);
if (cmd == null) {
return (
<div
@@ -595,17 +600,19 @@ class LineCmd extends React.Component<
</div>
);
}
const isSelected = mobx
let status = cmd.getStatus();
let lineNumStr = (line.linenumtemp ? "~" : "") + String(line.linenum);
let isSelected = mobx
.computed(() => screen.getSelectedLine() == line.linenum, {
name: "computed-isSelected",
})
.get();
const isPhysicalFocused = mobx
let isPhysicalFocused = mobx
.computed(() => screen.getIsFocused(line.linenum), {
name: "computed-getIsFocused",
})
.get();
const isFocused = mobx
let isFocused = mobx
.computed(
() => {
let screenFocusType = screen.getFocusType();
@@ -614,7 +621,7 @@ class LineCmd extends React.Component<
{ name: "computed-isFocused" }
)
.get();
const shouldCmdFocus = mobx
let shouldCmdFocus = mobx
.computed(
() => {
let screenFocusType = screen.getFocusType();
@@ -623,7 +630,7 @@ class LineCmd extends React.Component<
{ name: "computed-shouldCmdFocus" }
)
.get();
const isInSidebar = mobx
let isInSidebar = mobx
.computed(
() => {
return screen.isSidebarOpen() && screen.isLineIdInSidebar(line.lineid);
@@ -631,10 +638,11 @@ class LineCmd extends React.Component<
{ name: "computed-isInSidebar" }
)
.get();
const isRunning = cmd.isRunning();
const isExpanded = this.isCmdExpanded.get();
const rsdiff = this.rtnStateDiff.get();
const mainDivCn = cn(
let isStatic = staticRender;
let isRunning = cmd.isRunning();
let isExpanded = this.isCmdExpanded.get();
let rsdiff = this.rtnStateDiff.get();
let mainDivCn = cn(
"line",
"line-cmd",
{ selected: isSelected },
@@ -643,18 +651,18 @@ class LineCmd extends React.Component<
{ "has-rtnstate": cmd.getRtnState() }
);
let rendererPlugin: RendererPluginType = null;
const isNoneRenderer = line.renderer == "none";
let isNoneRenderer = line.renderer == "none";
if (!isBlank(line.renderer) && line.renderer != "terminal" && !isNoneRenderer) {
rendererPlugin = PluginModel.getRendererPluginByName(line.renderer);
}
const rendererType = lineutil.getRendererType(line);
const hidePrompt = rendererPlugin?.hidePrompt;
const termFontSize = GlobalModel.termFontSize.get();
let rendererType = lineutil.getRendererType(line);
let hidePrompt = rendererPlugin != null && rendererPlugin.hidePrompt;
let termFontSize = GlobalModel.termFontSize.get();
let rtnStateDiffSize = termFontSize - 2;
if (rtnStateDiffSize < 10) {
rtnStateDiffSize = Math.max(termFontSize, 10);
}
const containerType = screen.getContainerType();
let containerType = screen.getContainerType();
return (
<div
className={mainDivCn}
@@ -673,7 +681,7 @@ class LineCmd extends React.Component<
<If condition={!hidePrompt}>{this.renderCmdText(cmd)}</If>
</div>
<div key="restart" title="Restart Command" className="line-icon" onClick={this.clickRestart}>
<i className="fa-sharp fa-regular fa-arrows-rotate" />
<i className="fa-sharp fa-regular fa-arrows-rotate"/>
</div>
<div key="delete" title="Delete Line (&#x2318;D)" className="line-icon" onClick={this.clickDelete}>
<i className="fa-sharp fa-regular fa-trash" />
@@ -813,7 +821,7 @@ class Line extends React.Component<
{}
> {
render() {
const line = this.props.line;
let line = this.props.line;
if (line.archived) {
return null;
}
@@ -839,7 +847,7 @@ class LineText extends React.Component<
> {
@boundMethod
clickHandler() {
const { line, noSelect } = this.props;
let { line, noSelect } = this.props;
if (noSelect) {
return;
}
@@ -848,7 +856,7 @@ class LineText extends React.Component<
@boundMethod
onAvatarRightClick(e: any): void {
const { line, noSelect } = this.props;
let { line, noSelect } = this.props;
if (noSelect) {
return;
}
@@ -862,19 +870,19 @@ class LineText extends React.Component<
}
render() {
const { screen, line } = this.props;
const formattedTime = lineutil.getLineDateTimeStr(line.ts);
const isSelected = mobx
let { screen, line, renderMode } = this.props;
let formattedTime = lineutil.getLineDateTimeStr(line.ts);
let isSelected = mobx
.computed(() => screen.getSelectedLine() == line.linenum, {
name: "computed-isSelected",
})
.get();
const isFocused = mobx
let isFocused = mobx
.computed(() => screen.getFocusType() == "cmd", {
name: "computed-isFocused",
})
.get();
const mainClass = cn("line", "line-text", "focus-parent");
let mainClass = cn("line", "line-text", "focus-parent");
return (
<div
className={mainClass}
+6 -5
View File
@@ -84,14 +84,11 @@
}
.meta.meta-line1 {
margin-left: 2px;
color: rgba(@base-color, 0.6) !important;
font-size: 11px;
}
.meta.meta-line2 {
margin-left: -2px;
}
&.has-rtnstate .terminal-wrapper {
padding-bottom: 0;
}
@@ -116,6 +113,11 @@
overflow-x: hidden;
}
.terminal {
margin-right: 8px;
padding: 0.25rem;
}
&.cmd-done .terminal .xterm-cursor {
display: none;
}
@@ -464,7 +466,6 @@
padding: 0 0 10px 0;
flex-grow: 1;
position: relative;
overflow-x: hidden;
&::-webkit-scrollbar-thumb {
background-color: transparent !important;
+2 -6
View File
@@ -18,6 +18,8 @@ let MagicLayout = {
ScreenMinContentSize: 100,
ScreenMaxContentSize: 5000,
// the 3 is for descenders, which get cut off in the terminal without this
TermDescendersHeight: 3,
TermWidthBuffer: 15,
TabWidth: 154,
@@ -25,12 +27,6 @@ let MagicLayout = {
ScreenSidebarWidthPadding: 5,
ScreenSidebarMinWidth: 200,
ScreenSidebarHeaderHeight: 28,
MainSidebarMinWidth: 75,
MainSidebarMaxWidth: 300,
MainSidebarSnapThreshold: 90,
MainSidebarDragResistance: 50,
MainSidebarDefaultWidth: 240,
};
let m = MagicLayout;
+18 -23
View File
@@ -3,13 +3,14 @@
.main-sidebar {
padding: 0;
min-width: 20rem;
max-width: 20rem;
display: flex;
flex-direction: column;
position: relative;
font-size: 12.5px;
line-height: 20px;
backdrop-filter: blur(4px);
z-index: 20;
.title-bar-drag {
-webkit-app-region: drag;
@@ -23,6 +24,7 @@
&.collapsed {
width: 6em;
min-width: 6em;
.arrow-container,
.collapse-button {
transform: rotate(180deg);
@@ -32,7 +34,7 @@
margin-top: 26px;
.top,
.workspaces,
.workspaces-item,
.middle,
.bottom,
.separator {
@@ -48,7 +50,7 @@
justify-content: center;
align-items: center;
.logo-container {
.logo-container img {
width: 45px;
}
@@ -84,14 +86,10 @@
display: flex;
flex-direction: row;
.logo-container {
flex-shrink: 0;
width: 100px;
}
.spacer {
flex-grow: 1;
}
img {
width: 100px;
}
@@ -129,7 +127,9 @@
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
}
.index {
font-size: 10px;
font-size: 0.8em;
text-align: center;
width: 2em;
}
.hotkey {
float: left !important;
@@ -152,6 +152,7 @@
margin-left: 6px;
border-radius: 4px;
opacity: 1;
transition: opacity 0.1s ease-in-out, visibility 0.1s step-end;
width: inherit;
max-width: inherit;
min-width: inherit;
@@ -178,7 +179,6 @@
float: right;
margin-right: 6px;
letter-spacing: 6px;
margin-left: auto;
}
&:hover {
:not(.disabled) .hotkey {
@@ -190,20 +190,15 @@
}
&:not(:hover) .status-indicator {
.status-indicator-visible;
.positional-icon-visible;
}
&.workspaces {
cursor: default;
.add-workspace {
cursor: pointer;
.positional-icon-visible;
float: right;
padding: 2px;
margin-right: 6px;
.fa-plus {
font-size: 13px;
}
.add_workspace {
float: right;
padding: 2px;
margin-right: 6px;
transition: transform 0.3s ease-in-out;
svg {
fill: @base-color;
}
}
+98 -114
View File
@@ -7,19 +7,19 @@ import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import dayjs from "dayjs";
import type { ClientDataType, RemoteType } from "../../types/types";
import type { RemoteType } from "../../types/types";
import { If } from "tsx-control-statements/components";
import { compareLoose } from "semver";
import { ReactComponent as LeftChevronIcon } from "../assets/icons/chevron_left.svg";
import { ReactComponent as AppsIcon } from "../assets/icons/apps.svg";
import { ReactComponent as WorkspacesIcon } from "../assets/icons/workspaces.svg";
import { ReactComponent as AddIcon } from "../assets/icons/add.svg";
import { ReactComponent as SettingsIcon } from "../assets/icons/settings.svg";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel, GlobalCommandRunner, Session, VERSION } from "../../model/model";
import { isBlank, openLink } from "../../util/util";
import { ResizableSidebar } from "../common/common";
import * as constants from "../appconst";
import "./sidebar.less";
@@ -27,6 +27,8 @@ import { ActionsIcon, CenteredIcon, FrontIcon, StatusIndicator } from "../common
dayjs.extend(localizedFormat);
type OV<V> = mobx.IObservableValue<V>;
class SideBarItem extends React.Component<{
frontIcon: React.ReactNode;
contents: React.ReactNode | string;
@@ -58,14 +60,16 @@ class HotKeyIcon extends React.Component<{ hotkey: string }> {
}
}
interface MainSideBarProps {
parentRef: React.RefObject<HTMLElement>;
clientData: ClientDataType;
}
@mobxReact.observer
class MainSideBar extends React.Component<MainSideBarProps, {}> {
sidebarRef = React.createRef<HTMLDivElement>();
class MainSideBar extends React.Component<{}, {}> {
collapsed: mobx.IObservableValue<boolean> = mobx.observable.box(false);
@boundMethod
toggleCollapsed() {
mobx.action(() => {
this.collapsed.set(!this.collapsed.get());
})();
}
handleSessionClick(sessionId: string) {
GlobalCommandRunner.switchSession(sessionId);
@@ -172,9 +176,9 @@ class MainSideBar extends React.Component<MainSideBarProps, {}> {
getSessions() {
if (!GlobalModel.sessionListLoaded.get()) return <div className="item">loading ...</div>;
const sessionList: Session[] = [];
const activeSessionId = GlobalModel.activeSessionId.get();
for (const session of GlobalModel.sessionList) {
let sessionList = [];
let activeSessionId = GlobalModel.activeSessionId.get();
for (let session of GlobalModel.sessionList) {
if (!session.archived.get() || session.sessionId == activeSessionId) {
sessionList.push(session);
}
@@ -186,7 +190,6 @@ class MainSideBar extends React.Component<MainSideBarProps, {}> {
const sessionRunningCommands = sessionScreens.some((screen) => screen.numRunningCmds.get() > 0);
return (
<SideBarItem
key={session.sessionId}
className={`${isActive ? "active" : ""}`}
frontIcon={<span className="index">{index + 1}</span>}
contents={session.name.get()}
@@ -205,116 +208,97 @@ class MainSideBar extends React.Component<MainSideBarProps, {}> {
}
render() {
let clientData = this.props.clientData;
let isCollapsed = this.collapsed.get();
let clientData = GlobalModel.clientData.get();
let needsUpdate = false;
if (!clientData?.clientopts.noreleasecheck && !isBlank(clientData?.releaseinfo?.latestversion)) {
needsUpdate = compareLoose(VERSION, clientData.releaseinfo.latestversion) < 0;
}
let mainSidebar = GlobalModel.mainSidebarModel;
let isCollapsed = mainSidebar.getCollapsed();
return (
<ResizableSidebar
className="main-sidebar"
position="left"
enableSnap={true}
parentRef={this.props.parentRef}
>
{(toggleCollapse) => (
<React.Fragment>
<div className="title-bar-drag" />
<div className="contents">
<div className="logo">
<If condition={isCollapsed}>
<div className="logo-container" onClick={toggleCollapse}>
<img src="public/logos/wave-logo.png" />
</div>
</If>
<If condition={!isCollapsed}>
<div className="logo-container">
<img src="public/logos/wave-dark.png" />
</div>
<div className="spacer" />
<div className="collapse-button" onClick={toggleCollapse}>
<LeftChevronIcon className="icon" />
</div>
</If>
<div className={cn("main-sidebar", { collapsed: isCollapsed }, { "is-dev": GlobalModel.isDev })}>
<div className="title-bar-drag" />
<div className="contents">
<div className="logo">
<If condition={isCollapsed}>
<div className="logo-container" onClick={this.toggleCollapsed}>
<img src="public/logos/wave-logo.png" />
</div>
<div className="separator" />
<div className="top">
<SideBarItem
key="history"
frontIcon={<i className="fa-sharp fa-regular fa-clock-rotate-left icon" />}
contents="History"
endIcons={[<HotKeyIcon key="hotkey" hotkey="H" />]}
onClick={this.handleHistoryClick}
/>
{/* <SideBarItem className="hoverEffect unselectable" frontIcon={<FavoritesIcon className="icon" />} contents="Favorites" endIcon={<span className="hotkey">&#x2318;B</span>} onClick={this.handleBookmarksClick}/> */}
<SideBarItem
key="connections"
frontIcon={<i className="fa-sharp fa-regular fa-globe icon " />}
contents="Connections"
onClick={this.handleConnectionsClick}
/>
</If>
<If condition={!isCollapsed}>
<div className="logo-container">
<img src="public/logos/wave-dark.png" />
</div>
<div className="separator" />
<div className="spacer" />
<div className="collapse-button" onClick={this.toggleCollapsed}>
<LeftChevronIcon className="icon" />
</div>
</If>
</div>
<div className="separator" />
<div className="top">
<SideBarItem
frontIcon={<i className="fa-sharp fa-regular fa-clock-rotate-left icon" />}
contents="History"
endIcons={[<HotKeyIcon key="hotkey" hotkey="H" />]}
onClick={this.handleHistoryClick}
/>
{/* <SideBarItem className="hoverEffect unselectable" frontIcon={<FavoritesIcon className="icon" />} contents="Favorites" endIcon={<span className="hotkey">&#x2318;B</span>} onClick={this.handleBookmarksClick}/> */}
<SideBarItem
frontIcon={<i className="fa-sharp fa-regular fa-globe icon " />}
contents="Connections"
onClick={this.handleConnectionsClick}
/>
</div>
<div className="separator" />
<SideBarItem
frontIcon={<WorkspacesIcon className="icon" />}
contents="Workspaces"
endIcons={[
<div
key="add_workspace"
className="add_workspace hoverEffect"
onClick={this.handleNewSession}
>
<AddIcon />
</div>,
]}
/>
<div className="middle hideScrollbarUntillHover">{this.getSessions()}</div>
<div className="bottom">
<If condition={needsUpdate}>
<SideBarItem
key="workspaces"
className="workspaces"
frontIcon={<WorkspacesIcon className="icon" />}
contents="Workspaces"
endIcons={[
<CenteredIcon
key="add-workspace"
className="add-workspace hoverEffect"
onClick={this.handleNewSession}
>
<i className="fa-sharp fa-solid fa-plus"></i>
</CenteredIcon>,
]}
className="updateBanner"
frontIcon={<i className="fa-sharp fa-regular fa-circle-up icon" />}
contents="Update Available"
onClick={() => openLink("https://www.waveterm.dev/download?ref=upgrade")}
/>
<div className="middle hideScrollbarUntillHover">{this.getSessions()}</div>
<div className="bottom">
<If condition={needsUpdate}>
<SideBarItem
key="update-available"
className="updateBanner"
frontIcon={<i className="fa-sharp fa-regular fa-circle-up icon" />}
contents="Update Available"
onClick={() => openLink("https://www.waveterm.dev/download?ref=upgrade")}
/>
</If>
<If condition={GlobalModel.isDev}>
<SideBarItem
key="apps"
frontIcon={<AppsIcon className="icon" />}
contents="Apps"
onClick={this.handlePluginsClick}
endIcons={[<HotKeyIcon key="hotkey" hotkey="A" />]}
/>
</If>
<SideBarItem
key="settings"
frontIcon={<SettingsIcon className="icon" />}
contents="Settings"
onClick={this.handleSettingsClick}
/>
<SideBarItem
key="documentation"
frontIcon={<i className="fa-sharp fa-regular fa-circle-question icon" />}
contents="Documentation"
onClick={() => openLink("https://docs.waveterm.dev")}
/>
<SideBarItem
key="discord"
frontIcon={<i className="fa-brands fa-discord icon" />}
contents="Discord"
onClick={() => openLink("https://discord.gg/XfvZ334gwU")}
/>
</div>
</div>
</React.Fragment>
)}
</ResizableSidebar>
</If>
<If condition={GlobalModel.isDev}>
<SideBarItem
frontIcon={<AppsIcon className="icon" />}
contents="Apps"
onClick={this.handlePluginsClick}
endIcons={[<HotKeyIcon key="hotkey" hotkey="A" />]}
/>
</If>
<SideBarItem
frontIcon={<SettingsIcon className="icon" />}
contents="Settings"
onClick={this.handleSettingsClick}
/>
<SideBarItem
frontIcon={<i className="fa-sharp fa-regular fa-circle-question icon" />}
contents="Documentation"
onClick={() => openLink("https://docs.waveterm.dev")}
/>
<SideBarItem
frontIcon={<i className="fa-brands fa-discord icon" />}
contents="Discord"
onClick={() => openLink("https://discord.gg/XfvZ334gwU")}
/>
</div>
</div>
</div>
);
}
}
+28 -29
View File
@@ -5,25 +5,28 @@ import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { If } from "tsx-control-statements/components";
import { If, Choose, When, Otherwise } from "tsx-control-statements/components";
import cn from "classnames";
import dayjs from "dayjs";
import type { RemoteType, RemoteInstanceType, RemotePtrType } from "../../../types/types";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel, GlobalCommandRunner, Screen } from "../../../model/model";
import { renderCmdText } from "../../common/common";
import { GlobalModel, GlobalCommandRunner, Screen, ScreenLines } from "../../../model/model";
import { renderCmdText, Button } from "../../common/common";
import { TextAreaInput } from "./textareainput";
import { InfoMsg } from "./infomsg";
import { HistoryInfo } from "./historyinfo";
import { Prompt } from "../../common/prompt/prompt";
import { ReactComponent as ExecIcon } from "../../assets/icons/exec.svg";
import { RotateIcon } from "../../common/icons/icons";
import { ReactComponent as RotateIcon } from "../../assets/icons/line/rotate.svg";
import "./cmdinput.less";
import { AIChat } from "./aichat";
import "./cmdinput.less";
dayjs.extend(localizedFormat);
const TDots = "â‹®";
type OV<V> = mobx.IObservableValue<V>;
@mobxReact.observer
class CmdInput extends React.Component<{}, {}> {
cmdInputRef: React.RefObject<any> = React.createRef();
@@ -34,11 +37,11 @@ class CmdInput extends React.Component<{}, {}> {
}
updateCmdInputHeight() {
const elem = this.cmdInputRef.current;
let elem = this.cmdInputRef.current;
if (elem == null) {
return;
}
const height = elem.offsetHeight;
let height = elem.offsetHeight;
if (height == GlobalModel.inputModel.cmdInputHeight) {
return;
}
@@ -47,7 +50,7 @@ class CmdInput extends React.Component<{}, {}> {
})();
}
componentDidUpdate(): void {
componentDidUpdate(prevProps, prevState, snapshot: {}): void {
this.updateCmdInputHeight();
}
@@ -84,7 +87,7 @@ class CmdInput extends React.Component<{}, {}> {
e.preventDefault();
e.stopPropagation();
const inputModel = GlobalModel.inputModel;
let inputModel = GlobalModel.inputModel;
if (inputModel.historyShow.get()) {
inputModel.resetHistory();
} else {
@@ -110,9 +113,9 @@ class CmdInput extends React.Component<{}, {}> {
}
render() {
const model = GlobalModel;
const inputModel = model.inputModel;
const screen = GlobalModel.getActiveScreen();
let model = GlobalModel;
let inputModel = model.inputModel;
let screen = GlobalModel.getActiveScreen();
let ri: RemoteInstanceType = null;
let rptr: RemotePtrType = null;
if (screen != null) {
@@ -126,13 +129,15 @@ class CmdInput extends React.Component<{}, {}> {
feState = ri.festate;
}
feState = feState || {};
const infoShow = inputModel.infoShow.get();
const historyShow = !infoShow && inputModel.historyShow.get();
const aiChatShow = inputModel.aIChatShow.get();
const focusVal = inputModel.physicalInputFocused.get();
const inputMode: string = inputModel.inputMode.get();
const textAreaInputKey = screen == null ? "null" : screen.screenId;
const win = GlobalModel.getScreenLinesById(screen.screenId);
let infoShow = inputModel.infoShow.get();
let historyShow = !infoShow && inputModel.historyShow.get();
let aiChatShow = inputModel.aIChatShow.get();
let infoMsg = inputModel.infoMsg.get();
let hasInfo = infoMsg != null;
let focusVal = inputModel.physicalInputFocused.get();
let inputMode: string = inputModel.inputMode.get();
let textAreaInputKey = screen == null ? "null" : screen.screenId;
let win = GlobalModel.getScreenLinesById(screen.screenId);
let numRunningLines = 0;
if (win != null) {
numRunningLines = mobx.computed(() => win.getRunningCmdLines().length).get();
@@ -227,17 +232,11 @@ class CmdInput extends React.Component<{}, {}> {
{focusVal && (
<div className="cmd-btn hoverEffect">
<If condition={historyShow}>
<div className="hint-elem" onMouseDown={this.clickHistoryHint}>
close (esc)
</div>
<div className="hint-elem" onMouseDown={this.clickHistoryHint}>close (esc)</div>
</If>
<If condition={!historyShow}>
<div className="hint-elem" onMouseDown={this.clickHistoryHint}>
history (ctrl-r)
</div>
<div className="hint-elem" onMouseDown={this.clickAIHint}>
AI (ctrl-space)
</div>
<div className="hint-elem" onMouseDown={this.clickHistoryHint}>history (ctrl-r)</div>
<div className="hint-elem" onMouseDown={this.clickAIHint}>AI (ctrl-space)</div>
</If>
</div>
)}

Some files were not shown because too many files have changed in this diff Show More