mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +01:00
merge 'main' into use-ssh-library--add-user-input
This merge is rather large, so it needs some explaining. Most of it was a straightforward merge, but there were a few markdown edits i made that had to manually be resolved. There were also a few path changes to be made in the userinput.tsx file.
This commit is contained in:
commit
75778963c5
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
# Wave Terminal
|
# Wave Terminal
|
||||||
|
|
||||||
A open-source, cross-platform, modern terminal for seamless workflows.
|
A open-source, cross-platform, AI-integrated, modern terminal for seamless workflows.
|
||||||
|
|
||||||
Wave isn't just another terminal emulator; it's a rethink on how terminals are built. Wave combines command line with the power of the open web to help veteran CLI users and new developers alike.
|
Wave isn't just another terminal emulator; it's a rethink on how terminals are built. Wave combines command line with the power of the open web to help veteran CLI users and new developers alike.
|
||||||
|
|
||||||
@ -18,6 +18,7 @@ Wave isn't just another terminal emulator; it's a rethink on how terminals are b
|
|||||||
* Persistent sessions that can restore state across network disconnections and reboots
|
* Persistent sessions that can restore state across network disconnections and reboots
|
||||||
* Searchable contextual command history across all remote sessions (saved locally)
|
* Searchable contextual command history across all remote sessions (saved locally)
|
||||||
* Workspaces, tabs, and command blocks to keep you organized
|
* Workspaces, tabs, and command blocks to keep you organized
|
||||||
|
* AI Integration with ChatGPT (or ChatGPT compatible APIs) to help write commands and get answers inline
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@ -35,6 +36,7 @@ brew install --cask wave
|
|||||||
* Homepage — https://www.waveterm.dev
|
* Homepage — https://www.waveterm.dev
|
||||||
* Download Page — https://www.waveterm.dev/download
|
* Download Page — https://www.waveterm.dev/download
|
||||||
* Documentation — https://docs.waveterm.dev/
|
* Documentation — https://docs.waveterm.dev/
|
||||||
|
* Blog — https://blog.waveterm.dev/
|
||||||
* Quick Start Guide — https://docs.waveterm.dev/quickstart/
|
* Quick Start Guide — https://docs.waveterm.dev/quickstart/
|
||||||
* Discord Community — https://discord.gg/XfvZ334gwU
|
* Discord Community — https://discord.gg/XfvZ334gwU
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import { If, For } from "tsx-control-statements/components";
|
|||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import type { BookmarkType } from "../../types/types";
|
import type { BookmarkType } from "../../types/types";
|
||||||
import { GlobalModel } from "../../model/model";
|
import { GlobalModel } from "../../model/model";
|
||||||
import { CmdStrCode, Markdown } from "../common/common";
|
import { CmdStrCode, Markdown } from "../common/elements";
|
||||||
|
|
||||||
import { ReactComponent as XmarkIcon } from "../assets/icons/line/xmark.svg";
|
import { ReactComponent as XmarkIcon } from "../assets/icons/line/xmark.svg";
|
||||||
import { ReactComponent as CopyIcon } from "../assets/icons/favourites/copy.svg";
|
import { ReactComponent as CopyIcon } from "../assets/icons/favourites/copy.svg";
|
||||||
|
@ -7,7 +7,7 @@ import * as mobx from "mobx";
|
|||||||
import { boundMethod } from "autobind-decorator";
|
import { boundMethod } from "autobind-decorator";
|
||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import { GlobalModel, GlobalCommandRunner, MinFontSize, MaxFontSize, RemotesModel } from "../../model/model";
|
import { GlobalModel, GlobalCommandRunner, MinFontSize, MaxFontSize, RemotesModel } from "../../model/model";
|
||||||
import { Toggle, InlineSettingsTextEdit, SettingsError, Dropdown } from "../common/common";
|
import { Toggle, InlineSettingsTextEdit, SettingsError, Dropdown } from "../common/elements";
|
||||||
import { CommandRtnType, ClientDataType } from "../../types/types";
|
import { CommandRtnType, ClientDataType } from "../../types/types";
|
||||||
import { commandRtnHandler, isBlank } from "../../util/util";
|
import { commandRtnHandler, isBlank } from "../../util/util";
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
123
src/app/common/elements/button.less
Normal file
123
src/app/common/elements/button.less
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
@import "../../../app/common/themes/themes.less";
|
||||||
|
|
||||||
|
.wave-button {
|
||||||
|
background: none;
|
||||||
|
color: inherit;
|
||||||
|
border: none;
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: inherit;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
padding: 6px 16px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
border-radius: 6px;
|
||||||
|
height: auto;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: @term-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
fill: rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
color: @term-green;
|
||||||
|
background: none;
|
||||||
|
|
||||||
|
i {
|
||||||
|
fill: @term-green;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.solid {
|
||||||
|
color: @term-bright-white;
|
||||||
|
background: @term-green;
|
||||||
|
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
|
||||||
|
0px 0px 0.5px 0px rgba(255, 255, 255, 0.8) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.6) inset;
|
||||||
|
|
||||||
|
i {
|
||||||
|
fill: @term-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.outlined {
|
||||||
|
border: 1px solid @term-green;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ghost {
|
||||||
|
// Styles for .ghost are already defined above
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: @term-bright-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.secondary {
|
||||||
|
color: @term-white;
|
||||||
|
background: none;
|
||||||
|
|
||||||
|
&.solid {
|
||||||
|
background: rgba(255, 255, 255, 0.09);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.outlined {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.09);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ghost {
|
||||||
|
padding: 6px 10px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
fill: @term-green;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.color-yellow {
|
||||||
|
&.solid {
|
||||||
|
border-color: @warning-yellow;
|
||||||
|
background-color: mix(@warning-yellow, @term-white, 50%);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.outlined {
|
||||||
|
color: @warning-yellow;
|
||||||
|
border-color: @warning-yellow;
|
||||||
|
&:hover {
|
||||||
|
color: @term-white;
|
||||||
|
border-color: @term-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ghost {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.color-red {
|
||||||
|
&.solid {
|
||||||
|
border-color: @term-red;
|
||||||
|
background-color: mix(@term-red, @term-white, 50%);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.outlined {
|
||||||
|
color: @term-red;
|
||||||
|
border-color: @term-red;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ghost {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.link-button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
63
src/app/common/elements/button.tsx
Normal file
63
src/app/common/elements/button.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { boundMethod } from "autobind-decorator";
|
||||||
|
import cn from "classnames";
|
||||||
|
|
||||||
|
import "./button.less";
|
||||||
|
|
||||||
|
type ButtonVariantType = "outlined" | "solid" | "ghost";
|
||||||
|
type ButtonThemeType = "primary" | "secondary";
|
||||||
|
|
||||||
|
interface ButtonProps {
|
||||||
|
theme?: ButtonThemeType;
|
||||||
|
children: React.ReactNode;
|
||||||
|
onClick?: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
variant?: ButtonVariantType;
|
||||||
|
leftIcon?: React.ReactNode;
|
||||||
|
rightIcon?: React.ReactNode;
|
||||||
|
color?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
autoFocus?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Button extends React.Component<ButtonProps> {
|
||||||
|
static defaultProps = {
|
||||||
|
theme: "primary",
|
||||||
|
variant: "solid",
|
||||||
|
color: "",
|
||||||
|
style: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleClick() {
|
||||||
|
if (this.props.onClick && !this.props.disabled) {
|
||||||
|
this.props.onClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { leftIcon, rightIcon, theme, children, disabled, variant, color, style, autoFocus, className } =
|
||||||
|
this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={cn("wave-button", theme, variant, color, { disabled: disabled }, className)}
|
||||||
|
onClick={this.handleClick}
|
||||||
|
disabled={disabled}
|
||||||
|
style={style}
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
>
|
||||||
|
{leftIcon && <span className="icon-left">{leftIcon}</span>}
|
||||||
|
{children}
|
||||||
|
{rightIcon && <span className="icon-right">{rightIcon}</span>}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button };
|
||||||
|
export type { ButtonProps };
|
68
src/app/common/elements/checkbox.less
Normal file
68
src/app/common/elements/checkbox.less
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
@import "../../../app/common/themes/themes.less";
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] + label {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: @term-bright-white;
|
||||||
|
transition: color 250ms cubic-bezier(0.4, 0, 0.23, 1);
|
||||||
|
}
|
||||||
|
input[type="checkbox"] + label > span {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 10px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background: transparent;
|
||||||
|
border: 2px solid #9e9e9e;
|
||||||
|
border-radius: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 250ms cubic-bezier(0.4, 0, 0.23, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] + label:hover > span,
|
||||||
|
input[type="checkbox"]:focus + label > span {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
input[type="checkbox"]:checked + label > ins {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"]:checked + label > span {
|
||||||
|
border: 10px solid @term-green;
|
||||||
|
}
|
||||||
|
input[type="checkbox"]:checked + label > span:before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
left: 3px;
|
||||||
|
width: 7px;
|
||||||
|
height: 12px;
|
||||||
|
border-right: 2px solid #fff;
|
||||||
|
border-bottom: 2px solid #fff;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
transform-origin: 0% 100%;
|
||||||
|
animation: checkbox-check 500ms cubic-bezier(0.4, 0, 0.23, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes checkbox-check {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
33% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
70
src/app/common/elements/checkbox.tsx
Normal file
70
src/app/common/elements/checkbox.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as mobx from "mobx";
|
||||||
|
import cn from "classnames";
|
||||||
|
|
||||||
|
import "./checkbox.less";
|
||||||
|
|
||||||
|
class Checkbox extends React.Component<
|
||||||
|
{
|
||||||
|
checked?: boolean;
|
||||||
|
defaultChecked?: boolean;
|
||||||
|
onChange: (value: boolean) => void;
|
||||||
|
label: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
id?: string;
|
||||||
|
},
|
||||||
|
{ checkedInternal: boolean }
|
||||||
|
> {
|
||||||
|
generatedId;
|
||||||
|
static idCounter = 0;
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
checkedInternal: this.props.checked ?? Boolean(this.props.defaultChecked),
|
||||||
|
};
|
||||||
|
this.generatedId = `checkbox-${Checkbox.idCounter++}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (this.props.checked !== undefined && this.props.checked !== prevProps.checked) {
|
||||||
|
this.setState({ checkedInternal: this.props.checked });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange = (e) => {
|
||||||
|
const newChecked = e.target.checked;
|
||||||
|
if (this.props.checked === undefined) {
|
||||||
|
this.setState({ checkedInternal: newChecked });
|
||||||
|
}
|
||||||
|
this.props.onChange(newChecked);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { label, className, id } = this.props;
|
||||||
|
const { checkedInternal } = this.state;
|
||||||
|
const checkboxId = id || this.generatedId;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("checkbox", className)}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={checkboxId}
|
||||||
|
checked={checkedInternal}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
aria-checked={checkedInternal}
|
||||||
|
role="checkbox"
|
||||||
|
/>
|
||||||
|
<label htmlFor={checkboxId}>
|
||||||
|
<span></span>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Checkbox };
|
102
src/app/common/elements/cmdstrcode.less
Normal file
102
src/app/common/elements/cmdstrcode.less
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
@import "../../../app/common/themes/themes.less";
|
||||||
|
|
||||||
|
.cmdstr-code {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 0px 10px 0px 0;
|
||||||
|
|
||||||
|
&.is-large {
|
||||||
|
.use-button {
|
||||||
|
height: 28px;
|
||||||
|
width: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-div code {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.limit-height .code-div {
|
||||||
|
max-height: 58px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.limit-height.is-large .code-div {
|
||||||
|
max-height: 68px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.use-button {
|
||||||
|
flex-grow: 0;
|
||||||
|
padding: 3px;
|
||||||
|
border-radius: 3px 0 0 3px;
|
||||||
|
height: 22px;
|
||||||
|
width: 22px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
align-self: flex-start;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-div {
|
||||||
|
background-color: @term-black;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
min-width: 100px;
|
||||||
|
overflow: auto;
|
||||||
|
border-left: 1px solid #777;
|
||||||
|
|
||||||
|
code {
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 100px;
|
||||||
|
color: @term-white;
|
||||||
|
white-space: pre;
|
||||||
|
padding: 2px 8px 2px 8px;
|
||||||
|
background-color: @term-black;
|
||||||
|
font-size: 1em;
|
||||||
|
font-family: @fixed-font;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-control {
|
||||||
|
width: 0;
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
visibility: hidden;
|
||||||
|
|
||||||
|
.inner-copy {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -1px;
|
||||||
|
right: -20px;
|
||||||
|
|
||||||
|
padding: 2px;
|
||||||
|
padding-left: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 20px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: @term-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .copy-control {
|
||||||
|
visibility: visible !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.copied-indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: @term-white;
|
||||||
|
opacity: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
pointer-events: none;
|
||||||
|
animation-name: fade-in-out;
|
||||||
|
animation-duration: 0.3s;
|
||||||
|
}
|
66
src/app/common/elements/cmdstrcode.tsx
Normal file
66
src/app/common/elements/cmdstrcode.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { boundMethod } from "autobind-decorator";
|
||||||
|
import cn from "classnames";
|
||||||
|
import { If } from "tsx-control-statements/components";
|
||||||
|
|
||||||
|
import { ReactComponent as CheckIcon } from "../../assets/icons/line/check.svg";
|
||||||
|
import { ReactComponent as CopyIcon } from "../../assets/icons/history/copy.svg";
|
||||||
|
|
||||||
|
import "./cmdstrcode.less";
|
||||||
|
|
||||||
|
class CmdStrCode extends React.Component<
|
||||||
|
{
|
||||||
|
cmdstr: string;
|
||||||
|
onUse: () => void;
|
||||||
|
onCopy: () => void;
|
||||||
|
isCopied: boolean;
|
||||||
|
fontSize: "normal" | "large";
|
||||||
|
limitHeight: boolean;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
> {
|
||||||
|
@boundMethod
|
||||||
|
handleUse(e: any) {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (this.props.onUse != null) {
|
||||||
|
this.props.onUse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleCopy(e: any) {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (this.props.onCopy != null) {
|
||||||
|
this.props.onCopy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let { isCopied, cmdstr, fontSize, limitHeight } = this.props;
|
||||||
|
return (
|
||||||
|
<div className={cn("cmdstr-code", { "is-large": fontSize == "large" }, { "limit-height": limitHeight })}>
|
||||||
|
<If condition={isCopied}>
|
||||||
|
<div key="copied" className="copied-indicator">
|
||||||
|
<div>copied</div>
|
||||||
|
</div>
|
||||||
|
</If>
|
||||||
|
<div key="use" className="use-button hoverEffect" title="Use Command" onClick={this.handleUse}>
|
||||||
|
<CheckIcon className="icon" />
|
||||||
|
</div>
|
||||||
|
<div key="code" className="code-div">
|
||||||
|
<code>{cmdstr}</code>
|
||||||
|
</div>
|
||||||
|
<div key="copy" className="copy-control hoverEffect">
|
||||||
|
<div className="inner-copy" onClick={this.handleCopy} title="copy">
|
||||||
|
<CopyIcon className="icon" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { CmdStrCode };
|
10
src/app/common/elements/cmdtext.tsx
Normal file
10
src/app/common/elements/cmdtext.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
function renderCmdText(text: string): any {
|
||||||
|
return <span>⌘{text}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { renderCmdText };
|
127
src/app/common/elements/dropdown.less
Normal file
127
src/app/common/elements/dropdown.less
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
@import "../../../app/common/themes/themes.less";
|
||||||
|
|
||||||
|
.wave-dropdown {
|
||||||
|
position: relative;
|
||||||
|
height: 44px;
|
||||||
|
min-width: 150px;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid rgba(241, 246, 243, 0.15);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
|
||||||
|
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||||
|
|
||||||
|
&.no-label {
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.offset-left {
|
||||||
|
left: 42px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-display {
|
||||||
|
position: absolute;
|
||||||
|
left: 16px;
|
||||||
|
bottom: 5px;
|
||||||
|
|
||||||
|
&.offset-left {
|
||||||
|
left: 42px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-arrow {
|
||||||
|
position: absolute;
|
||||||
|
right: 16px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
transition: transform 0.3s;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-arrow-rotate {
|
||||||
|
transform: translateY(-50%) rotate(180deg); // Rotate the arrow when dropdown is open
|
||||||
|
}
|
||||||
|
|
||||||
|
&-item {
|
||||||
|
display: flex;
|
||||||
|
min-width: 120px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
align-self: stretch;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
&-highlighted,
|
||||||
|
&:hover {
|
||||||
|
background: rgba(241, 246, 243, 0.08);
|
||||||
|
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
|
||||||
|
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-input-decoration {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-input-decoration.end-position {
|
||||||
|
margin-right: 44px;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-input-decoration.start-position {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-error {
|
||||||
|
border-color: @term-red;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: @term-green;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-dropdown-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin-top: 2px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 6px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #151715;
|
||||||
|
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5), 0px 3px 8px 0px rgba(0, 0, 0, 0.35), 0px 0px 0.5px 0px #fff inset,
|
||||||
|
0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||||
|
animation-fill-mode: forwards;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-dropdown-menu-close {
|
||||||
|
z-index: 0;
|
||||||
|
}
|
259
src/app/common/elements/dropdown.tsx
Normal file
259
src/app/common/elements/dropdown.tsx
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as mobxReact from "mobx-react";
|
||||||
|
import { boundMethod } from "autobind-decorator";
|
||||||
|
import cn from "classnames";
|
||||||
|
import { If } from "tsx-control-statements/components";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
|
||||||
|
import "./dropdown.less";
|
||||||
|
|
||||||
|
interface DropdownDecorationProps {
|
||||||
|
startDecoration?: React.ReactNode;
|
||||||
|
endDecoration?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DropdownProps {
|
||||||
|
label?: string;
|
||||||
|
options: { value: string; label: string }[];
|
||||||
|
value?: string;
|
||||||
|
className?: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
decoration?: DropdownDecorationProps;
|
||||||
|
defaultValue?: string;
|
||||||
|
required?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DropdownState {
|
||||||
|
isOpen: boolean;
|
||||||
|
internalValue: string;
|
||||||
|
highlightedIndex: number;
|
||||||
|
isTouched: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mobxReact.observer
|
||||||
|
class Dropdown extends React.Component<DropdownProps, DropdownState> {
|
||||||
|
wrapperRef: React.RefObject<HTMLDivElement>;
|
||||||
|
menuRef: React.RefObject<HTMLDivElement>;
|
||||||
|
timeoutId: any;
|
||||||
|
|
||||||
|
constructor(props: DropdownProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
isOpen: false,
|
||||||
|
internalValue: props.defaultValue || "",
|
||||||
|
highlightedIndex: -1,
|
||||||
|
isTouched: false,
|
||||||
|
};
|
||||||
|
this.wrapperRef = React.createRef();
|
||||||
|
this.menuRef = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
document.addEventListener("mousedown", this.handleClickOutside);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
document.removeEventListener("mousedown", this.handleClickOutside);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: Readonly<DropdownProps>, prevState: Readonly<DropdownState>, snapshot?: any): void {
|
||||||
|
// If the dropdown was open but now is closed, start the timeout
|
||||||
|
if (prevState.isOpen && !this.state.isOpen) {
|
||||||
|
this.timeoutId = setTimeout(() => {
|
||||||
|
if (this.menuRef.current) {
|
||||||
|
this.menuRef.current.style.display = "none";
|
||||||
|
}
|
||||||
|
}, 300); // Time is equal to the animation duration
|
||||||
|
}
|
||||||
|
// If the dropdown is now open, cancel any existing timeout and show the menu
|
||||||
|
else if (!prevState.isOpen && this.state.isOpen) {
|
||||||
|
if (this.timeoutId !== null) {
|
||||||
|
clearTimeout(this.timeoutId); // Cancel any existing timeout
|
||||||
|
this.timeoutId = null;
|
||||||
|
}
|
||||||
|
if (this.menuRef.current) {
|
||||||
|
this.menuRef.current.style.display = "inline-flex";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleClickOutside(event: MouseEvent) {
|
||||||
|
// Check if the click is outside both the wrapper and the menu
|
||||||
|
if (
|
||||||
|
this.wrapperRef.current &&
|
||||||
|
!this.wrapperRef.current.contains(event.target as Node) &&
|
||||||
|
this.menuRef.current &&
|
||||||
|
!this.menuRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
this.setState({ isOpen: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleClick() {
|
||||||
|
this.toggleDropdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleFocus() {
|
||||||
|
this.setState({ isTouched: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleKeyDown(event: React.KeyboardEvent) {
|
||||||
|
const { options } = this.props;
|
||||||
|
const { isOpen, highlightedIndex } = this.state;
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
case "Enter":
|
||||||
|
case " ":
|
||||||
|
if (isOpen) {
|
||||||
|
const option = options[highlightedIndex];
|
||||||
|
if (option) {
|
||||||
|
this.handleSelect(option.value, undefined);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.toggleDropdown();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "Escape":
|
||||||
|
this.setState({ isOpen: false });
|
||||||
|
break;
|
||||||
|
case "ArrowUp":
|
||||||
|
if (isOpen) {
|
||||||
|
this.setState((prevState) => ({
|
||||||
|
highlightedIndex:
|
||||||
|
prevState.highlightedIndex > 0 ? prevState.highlightedIndex - 1 : options.length - 1,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "ArrowDown":
|
||||||
|
if (isOpen) {
|
||||||
|
this.setState((prevState) => ({
|
||||||
|
highlightedIndex:
|
||||||
|
prevState.highlightedIndex < options.length - 1 ? prevState.highlightedIndex + 1 : 0,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "Tab":
|
||||||
|
this.setState({ isOpen: false });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleSelect(value: string, event?: React.MouseEvent | React.KeyboardEvent) {
|
||||||
|
const { onChange } = this.props;
|
||||||
|
if (event) {
|
||||||
|
event.stopPropagation(); // This stops the event from bubbling up to the wrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!("value" in this.props)) {
|
||||||
|
this.setState({ internalValue: value });
|
||||||
|
}
|
||||||
|
onChange(value);
|
||||||
|
this.setState({ isOpen: false, isTouched: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
toggleDropdown() {
|
||||||
|
this.setState((prevState) => ({ isOpen: !prevState.isOpen, isTouched: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
calculatePosition(): React.CSSProperties {
|
||||||
|
if (this.wrapperRef.current) {
|
||||||
|
const rect = this.wrapperRef.current.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
position: "absolute",
|
||||||
|
top: `${rect.bottom + window.scrollY}px`,
|
||||||
|
left: `${rect.left + window.scrollX}px`,
|
||||||
|
width: `${rect.width}px`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { label, options, value, placeholder, decoration, className, required } = this.props;
|
||||||
|
const { isOpen, internalValue, highlightedIndex, isTouched } = this.state;
|
||||||
|
|
||||||
|
const currentValue = value ?? internalValue;
|
||||||
|
const selectedOptionLabel =
|
||||||
|
options.find((option) => option.value === currentValue)?.label || placeholder || internalValue;
|
||||||
|
|
||||||
|
// Determine if the dropdown should be marked as having an error
|
||||||
|
const isError =
|
||||||
|
required &&
|
||||||
|
(value === undefined || value === "") &&
|
||||||
|
(internalValue === undefined || internalValue === "") &&
|
||||||
|
isTouched;
|
||||||
|
|
||||||
|
// Determine if the label should float
|
||||||
|
const shouldLabelFloat = !!value || !!internalValue || !!placeholder || isOpen;
|
||||||
|
|
||||||
|
const dropdownMenu = isOpen
|
||||||
|
? ReactDOM.createPortal(
|
||||||
|
<div className={cn("wave-dropdown-menu")} ref={this.menuRef} style={this.calculatePosition()}>
|
||||||
|
{options.map((option, index) => (
|
||||||
|
<div
|
||||||
|
key={option.value}
|
||||||
|
className={cn("wave-dropdown-item unselectable", {
|
||||||
|
"wave-dropdown-item-highlighted": index === highlightedIndex,
|
||||||
|
})}
|
||||||
|
onClick={(e) => this.handleSelect(option.value, e)}
|
||||||
|
onMouseEnter={() => this.setState({ highlightedIndex: index })}
|
||||||
|
onMouseLeave={() => this.setState({ highlightedIndex: -1 })}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>,
|
||||||
|
document.getElementById("app")!
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("wave-dropdown", className, {
|
||||||
|
"wave-dropdown-error": isError,
|
||||||
|
"no-label": !label,
|
||||||
|
})}
|
||||||
|
ref={this.wrapperRef}
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={this.handleKeyDown}
|
||||||
|
onClick={this.handleClick}
|
||||||
|
onFocus={this.handleFocus}
|
||||||
|
>
|
||||||
|
{decoration?.startDecoration && <>{decoration.startDecoration}</>}
|
||||||
|
<If condition={label}>
|
||||||
|
<div
|
||||||
|
className={cn("wave-dropdown-label unselectable", {
|
||||||
|
float: shouldLabelFloat,
|
||||||
|
"offset-left": decoration?.startDecoration,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
</If>
|
||||||
|
<div
|
||||||
|
className={cn("wave-dropdown-display unselectable", { "offset-left": decoration?.startDecoration })}
|
||||||
|
>
|
||||||
|
{selectedOptionLabel}
|
||||||
|
</div>
|
||||||
|
<div className={cn("wave-dropdown-arrow", { "wave-dropdown-arrow-rotate": isOpen })}>
|
||||||
|
<i className="fa-sharp fa-solid fa-chevron-down"></i>
|
||||||
|
</div>
|
||||||
|
{dropdownMenu}
|
||||||
|
{decoration?.endDecoration && <>{decoration.endDecoration}</>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Dropdown };
|
21
src/app/common/elements/iconbutton.tsx
Normal file
21
src/app/common/elements/iconbutton.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { Button } from "./button";
|
||||||
|
class IconButton extends Button {
|
||||||
|
render() {
|
||||||
|
const { children, theme, variant = "solid", ...rest } = this.props;
|
||||||
|
const className = `wave-button icon-button ${theme} ${variant}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button {...rest} className={className}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IconButton;
|
||||||
|
|
||||||
|
export { IconButton };
|
20
src/app/common/elements/index.tsx
Normal file
20
src/app/common/elements/index.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export { Button } from "./button";
|
||||||
|
export { Checkbox } from "./checkbox";
|
||||||
|
export { CmdStrCode } from "./cmdstrcode";
|
||||||
|
export { renderCmdText } from "./cmdtext";
|
||||||
|
export { Dropdown } from "./dropdown";
|
||||||
|
export { IconButton } from "./iconbutton";
|
||||||
|
export { InlineSettingsTextEdit } from "./inlinesettingstextedit";
|
||||||
|
export { InputDecoration } from "./inputdecoration";
|
||||||
|
export { LinkButton } from "./linkbutton";
|
||||||
|
export { Markdown } from "./markdown";
|
||||||
|
export { Modal } from "./modal";
|
||||||
|
export { NumberField } from "./numberfield";
|
||||||
|
export { PasswordField } from "./passwordfield";
|
||||||
|
export { ResizableSidebar } from "./resizablesidebar";
|
||||||
|
export { SettingsError } from "./settingserror";
|
||||||
|
export { ShowWaveShellInstallPrompt } from "./showwaveshellinstallprompt";
|
||||||
|
export { Status } from "./status";
|
||||||
|
export { TextField } from "./textfield";
|
||||||
|
export { Toggle } from "./toggle";
|
||||||
|
export { Tooltip } from "./tooltip";
|
40
src/app/common/elements/inlinesettingstextedit.less
Normal file
40
src/app/common/elements/inlinesettingstextedit.less
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
@import "../../../app/common/themes/themes.less";
|
||||||
|
|
||||||
|
.inline-edit {
|
||||||
|
.icon {
|
||||||
|
display: inline;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
margin-left: 1em;
|
||||||
|
vertical-align: middle;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.edit-not-active {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
i.fa-pen {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-style: dotted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.edit-active {
|
||||||
|
input.input {
|
||||||
|
padding: 0;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
149
src/app/common/elements/inlinesettingstextedit.tsx
Normal file
149
src/app/common/elements/inlinesettingstextedit.tsx
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as mobxReact from "mobx-react";
|
||||||
|
import * as mobx from "mobx";
|
||||||
|
import { boundMethod } from "autobind-decorator";
|
||||||
|
import cn from "classnames";
|
||||||
|
import { If } from "tsx-control-statements/components";
|
||||||
|
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../../util/keyutil";
|
||||||
|
|
||||||
|
import "./inlinesettingstextedit.less";
|
||||||
|
|
||||||
|
type OV<V> = mobx.IObservableValue<V>;
|
||||||
|
|
||||||
|
@mobxReact.observer
|
||||||
|
class InlineSettingsTextEdit extends React.Component<
|
||||||
|
{
|
||||||
|
text: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (val: string) => void;
|
||||||
|
maxLength: number;
|
||||||
|
placeholder: string;
|
||||||
|
showIcon?: boolean;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
> {
|
||||||
|
isEditing: OV<boolean> = mobx.observable.box(false, { name: "inlineedit-isEditing" });
|
||||||
|
tempText: OV<string>;
|
||||||
|
shouldFocus: boolean = false;
|
||||||
|
inputRef: React.RefObject<any> = React.createRef();
|
||||||
|
|
||||||
|
componentDidUpdate(): void {
|
||||||
|
if (this.shouldFocus) {
|
||||||
|
this.shouldFocus = false;
|
||||||
|
if (this.inputRef.current != null) {
|
||||||
|
this.inputRef.current.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleChangeText(e: any): void {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.tempText.set(e.target.value);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
confirmChange(): void {
|
||||||
|
mobx.action(() => {
|
||||||
|
let newText = this.tempText.get();
|
||||||
|
this.isEditing.set(false);
|
||||||
|
this.tempText = null;
|
||||||
|
this.props.onChange(newText);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
cancelChange(): void {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.isEditing.set(false);
|
||||||
|
this.tempText = null;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleKeyDown(e: any): void {
|
||||||
|
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||||
|
if (checkKeyPressed(waveEvent, "Enter")) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.confirmChange();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (checkKeyPressed(waveEvent, "Escape")) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.cancelChange();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
clickEdit(): void {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.isEditing.set(true);
|
||||||
|
this.shouldFocus = true;
|
||||||
|
this.tempText = mobx.observable.box(this.props.value, { name: "inlineedit-tempText" });
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.isEditing.get()) {
|
||||||
|
return (
|
||||||
|
<div className={cn("settings-input inline-edit", "edit-active")}>
|
||||||
|
<div className="field has-addons">
|
||||||
|
<div className="control">
|
||||||
|
<input
|
||||||
|
ref={this.inputRef}
|
||||||
|
className="input"
|
||||||
|
type="text"
|
||||||
|
onKeyDown={this.handleKeyDown}
|
||||||
|
placeholder={this.props.placeholder}
|
||||||
|
onChange={this.handleChangeText}
|
||||||
|
value={this.tempText.get()}
|
||||||
|
maxLength={this.props.maxLength}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="control">
|
||||||
|
<div
|
||||||
|
onClick={this.cancelChange}
|
||||||
|
title="Cancel (Esc)"
|
||||||
|
className="button is-prompt-danger is-outlined is-small"
|
||||||
|
>
|
||||||
|
<span className="icon is-small">
|
||||||
|
<i className="fa-sharp fa-solid fa-xmark" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="control">
|
||||||
|
<div
|
||||||
|
onClick={this.confirmChange}
|
||||||
|
title="Confirm (Enter)"
|
||||||
|
className="button is-wave-green is-outlined is-small"
|
||||||
|
>
|
||||||
|
<span className="icon is-small">
|
||||||
|
<i className="fa-sharp fa-solid fa-check" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div onClick={this.clickEdit} className={cn("settings-input inline-edit", "edit-not-active")}>
|
||||||
|
{this.props.text}
|
||||||
|
<If condition={this.props.showIcon}>
|
||||||
|
<i className="fa-sharp fa-solid fa-pen" />
|
||||||
|
</If>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { InlineSettingsTextEdit };
|
19
src/app/common/elements/inputdecoration.less
Normal file
19
src/app/common/elements/inputdecoration.less
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
@import "../../../app/common/themes/themes.less";
|
||||||
|
|
||||||
|
.wave-input-decoration {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-input-decoration.start-position {
|
||||||
|
margin: 0 4px 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-input-decoration.end-position {
|
||||||
|
margin: 0 16px 0 8px;
|
||||||
|
}
|
32
src/app/common/elements/inputdecoration.tsx
Normal file
32
src/app/common/elements/inputdecoration.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as mobxReact from "mobx-react";
|
||||||
|
import cn from "classnames";
|
||||||
|
|
||||||
|
import "./inputdecoration.less";
|
||||||
|
|
||||||
|
interface InputDecorationProps {
|
||||||
|
position?: "start" | "end";
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mobxReact.observer
|
||||||
|
class InputDecoration extends React.Component<InputDecorationProps, {}> {
|
||||||
|
render() {
|
||||||
|
const { children, position = "end" } = this.props;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("wave-input-decoration", {
|
||||||
|
"start-position": position === "start",
|
||||||
|
"end-position": position === "end",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { InputDecoration };
|
28
src/app/common/elements/linkbutton.tsx
Normal file
28
src/app/common/elements/linkbutton.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import cn from "classnames";
|
||||||
|
import { ButtonProps } from "./button";
|
||||||
|
|
||||||
|
interface LinkButtonProps extends ButtonProps {
|
||||||
|
href: string;
|
||||||
|
rel?: string;
|
||||||
|
target?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class LinkButton extends React.Component<LinkButtonProps> {
|
||||||
|
render() {
|
||||||
|
const { leftIcon, rightIcon, children, className, ...rest } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a {...rest} className={cn(`wave-button link-button`, className)}>
|
||||||
|
{leftIcon && <span className="icon-left">{leftIcon}</span>}
|
||||||
|
{children}
|
||||||
|
{rightIcon && <span className="icon-right">{rightIcon}</span>}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { LinkButton };
|
92
src/app/common/elements/markdown.less
Normal file
92
src/app/common/elements/markdown.less
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
@import "../../../app/common/themes/themes.less";
|
||||||
|
|
||||||
|
.markdown {
|
||||||
|
color: @term-white;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-family: @markdown-font;
|
||||||
|
font-size: 14px;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
|
||||||
|
code {
|
||||||
|
background-color: @markdown-highlight;
|
||||||
|
color: @term-white;
|
||||||
|
font-family: @terminal-font;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
code.inline {
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
font-family: @terminal-font;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: @term-white;
|
||||||
|
margin-top: 16px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: @term-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #32afff;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
tr th {
|
||||||
|
color: @term-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
list-style-position: outside;
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol {
|
||||||
|
list-style-position: outside;
|
||||||
|
margin-left: 19px;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
margin: 4px 10px 4px 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: @markdown-highlight;
|
||||||
|
padding: 2px 4px 2px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background-color: @markdown-highlight;
|
||||||
|
margin: 4px 10px 4px 10px;
|
||||||
|
padding: 6px 6px 6px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.selected {
|
||||||
|
outline: 2px solid @term-green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title.is-1 {
|
||||||
|
border-bottom: 1px solid #777;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
}
|
||||||
|
.title.is-2 {
|
||||||
|
border-bottom: 1px solid #777;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
}
|
||||||
|
.title.is-3 {
|
||||||
|
}
|
||||||
|
.title.is-4 {
|
||||||
|
}
|
||||||
|
.title.is-5 {
|
||||||
|
}
|
||||||
|
.title.is-6 {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown > *:first-child {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
105
src/app/common/elements/markdown.tsx
Normal file
105
src/app/common/elements/markdown.tsx
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as mobxReact from "mobx-react";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
|
import cn from "classnames";
|
||||||
|
import { GlobalModel } from "../../../model/model";
|
||||||
|
|
||||||
|
import "./markdown.less";
|
||||||
|
|
||||||
|
function LinkRenderer(props: any): any {
|
||||||
|
let newUrl = "https://extern?" + encodeURIComponent(props.href);
|
||||||
|
return (
|
||||||
|
<a href={newUrl} target="_blank" rel={"noopener"}>
|
||||||
|
{props.children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function HeaderRenderer(props: any, hnum: number): any {
|
||||||
|
return <div className={cn("title", "is-" + hnum)}>{props.children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CodeRenderer(props: any): any {
|
||||||
|
return <code className={cn({ inline: props.inline })}>{props.children}</code>;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mobxReact.observer
|
||||||
|
class CodeBlockMarkdown extends React.Component<{ children: React.ReactNode; codeSelectSelectedIndex?: number }, {}> {
|
||||||
|
blockIndex: number;
|
||||||
|
blockRef: React.RefObject<HTMLPreElement>;
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.blockRef = React.createRef();
|
||||||
|
this.blockIndex = GlobalModel.inputModel.addCodeBlockToCodeSelect(this.blockRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let clickHandler: (e: React.MouseEvent<HTMLElement>, blockIndex: number) => void;
|
||||||
|
let inputModel = GlobalModel.inputModel;
|
||||||
|
clickHandler = (e: React.MouseEvent<HTMLElement>, blockIndex: number) => {
|
||||||
|
inputModel.setCodeSelectSelectedCodeBlock(blockIndex);
|
||||||
|
};
|
||||||
|
let selected = this.blockIndex == this.props.codeSelectSelectedIndex;
|
||||||
|
return (
|
||||||
|
<pre
|
||||||
|
ref={this.blockRef}
|
||||||
|
className={cn({ selected: selected })}
|
||||||
|
onClick={(event) => clickHandler(event, this.blockIndex)}
|
||||||
|
>
|
||||||
|
{this.props.children}
|
||||||
|
</pre>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mobxReact.observer
|
||||||
|
class Markdown extends React.Component<
|
||||||
|
{ text: string; style?: any; extraClassName?: string; codeSelect?: boolean },
|
||||||
|
{}
|
||||||
|
> {
|
||||||
|
CodeBlockRenderer(props: any, codeSelect: boolean, codeSelectIndex: number): any {
|
||||||
|
if (codeSelect) {
|
||||||
|
return <CodeBlockMarkdown codeSelectSelectedIndex={codeSelectIndex}>{props.children}</CodeBlockMarkdown>;
|
||||||
|
} else {
|
||||||
|
const clickHandler = (e: React.MouseEvent<HTMLElement>) => {
|
||||||
|
let blockText = (e.target as HTMLElement).innerText;
|
||||||
|
if (blockText) {
|
||||||
|
blockText = blockText.replace(/\n$/, ""); // remove trailing newline
|
||||||
|
navigator.clipboard.writeText(blockText);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return <pre onClick={(event) => clickHandler(event)}>{props.children}</pre>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let text = this.props.text;
|
||||||
|
let codeSelect = this.props.codeSelect;
|
||||||
|
let curCodeSelectIndex = GlobalModel.inputModel.getCodeSelectSelectedIndex();
|
||||||
|
let markdownComponents = {
|
||||||
|
a: LinkRenderer,
|
||||||
|
h1: (props) => HeaderRenderer(props, 1),
|
||||||
|
h2: (props) => HeaderRenderer(props, 2),
|
||||||
|
h3: (props) => HeaderRenderer(props, 3),
|
||||||
|
h4: (props) => HeaderRenderer(props, 4),
|
||||||
|
h5: (props) => HeaderRenderer(props, 5),
|
||||||
|
h6: (props) => HeaderRenderer(props, 6),
|
||||||
|
code: (props) => CodeRenderer(props),
|
||||||
|
pre: (props) => this.CodeBlockRenderer(props, codeSelect, curCodeSelectIndex),
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className={cn("markdown content", this.props.extraClassName)} style={this.props.style}>
|
||||||
|
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||||
|
{text}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Markdown };
|
79
src/app/common/elements/modal.less
Normal file
79
src/app/common/elements/modal.less
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
@import "../../../app/common/themes/themes.less";
|
||||||
|
|
||||||
|
.wave-modal-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 500;
|
||||||
|
|
||||||
|
.wave-modal-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(21, 23, 21, 0.7);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-modal {
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #151715;
|
||||||
|
box-shadow: 0px 3px 5px 0px rgba(0, 0, 0, 0.35), 0px 10px 24px 0px rgba(0, 0, 0, 0.45),
|
||||||
|
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||||
|
|
||||||
|
.wave-modal-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.wave-modal-header {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 14px 12px 20px;
|
||||||
|
justify-content: space-between;
|
||||||
|
line-height: 20px;
|
||||||
|
border-bottom: 1px solid rgba(250, 250, 250, 0.1);
|
||||||
|
|
||||||
|
.wave-modal-title {
|
||||||
|
color: #eceeec;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
i {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-modal-body {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-modal-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 20px 20px;
|
||||||
|
|
||||||
|
button:last-child {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
81
src/app/common/elements/modal.tsx
Normal file
81
src/app/common/elements/modal.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as mobx from "mobx";
|
||||||
|
import { If } from "tsx-control-statements/components";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import { Button } from "./button";
|
||||||
|
import { IconButton } from "./iconbutton";
|
||||||
|
|
||||||
|
import "./modal.less";
|
||||||
|
|
||||||
|
type OV<V> = mobx.IObservableValue<V>;
|
||||||
|
|
||||||
|
interface ModalHeaderProps {
|
||||||
|
onClose?: () => void;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalHeader: React.FC<ModalHeaderProps> = ({ onClose, title }) => (
|
||||||
|
<div className="wave-modal-header">
|
||||||
|
{<div className="wave-modal-title">{title}</div>}
|
||||||
|
<If condition={onClose}>
|
||||||
|
<IconButton theme="secondary" variant="ghost" onClick={onClose}>
|
||||||
|
<i className="fa-sharp fa-solid fa-xmark"></i>
|
||||||
|
</IconButton>
|
||||||
|
</If>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface ModalFooterProps {
|
||||||
|
onCancel?: () => void;
|
||||||
|
onOk?: () => void;
|
||||||
|
cancelLabel?: string;
|
||||||
|
okLabel?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalFooter: React.FC<ModalFooterProps> = ({ onCancel, onOk, cancelLabel = "Cancel", okLabel = "Ok" }) => (
|
||||||
|
<div className="wave-modal-footer">
|
||||||
|
{onCancel && (
|
||||||
|
<Button theme="secondary" onClick={onCancel}>
|
||||||
|
{cancelLabel}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{onOk && <Button onClick={onOk}>{okLabel}</Button>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface ModalProps {
|
||||||
|
className?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
onClickBackdrop?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Modal extends React.Component<ModalProps> {
|
||||||
|
static Header = ModalHeader;
|
||||||
|
static Footer = ModalFooter;
|
||||||
|
|
||||||
|
renderBackdrop(onClick: (() => void) | undefined) {
|
||||||
|
return <div className="wave-modal-backdrop" onClick={onClick}></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderModal() {
|
||||||
|
const { className, children } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="wave-modal-container">
|
||||||
|
{this.renderBackdrop(this.props.onClickBackdrop)}
|
||||||
|
<div className={`wave-modal ${className}`}>
|
||||||
|
<div className="wave-modal-content">{children}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return ReactDOM.createPortal(this.renderModal(), document.getElementById("app"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Modal };
|
39
src/app/common/elements/numberfield.tsx
Normal file
39
src/app/common/elements/numberfield.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { boundMethod } from "autobind-decorator";
|
||||||
|
|
||||||
|
import { TextField } from "./textfield";
|
||||||
|
|
||||||
|
class NumberField extends TextField {
|
||||||
|
@boundMethod
|
||||||
|
handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
const { required, onChange } = this.props;
|
||||||
|
const inputValue = e.target.value;
|
||||||
|
|
||||||
|
// Allow only numeric input
|
||||||
|
if (inputValue === "" || /^\d*$/.test(inputValue)) {
|
||||||
|
// Update the internal state only if the component is not controlled.
|
||||||
|
if (this.props.value === undefined) {
|
||||||
|
const isError = required ? inputValue.trim() === "" : false;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
internalValue: inputValue,
|
||||||
|
error: isError,
|
||||||
|
hasContent: Boolean(inputValue),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange && onChange(inputValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
// Use the render method from TextField but add the onKeyDown handler
|
||||||
|
const renderedTextField = super.render();
|
||||||
|
return React.cloneElement(renderedTextField);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { NumberField };
|
30
src/app/common/elements/passwordfield.less
Normal file
30
src/app/common/elements/passwordfield.less
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
@import "../../../app/common/themes/themes.less";
|
||||||
|
|
||||||
|
.wave-password {
|
||||||
|
.wave-textfield-inner-eye {
|
||||||
|
position: absolute;
|
||||||
|
right: 16px;
|
||||||
|
top: 52%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
transition: transform 0.3s;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-input-decoration {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-input-decoration.end-position {
|
||||||
|
margin-right: 47px;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-input-decoration.start-position {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
106
src/app/common/elements/passwordfield.tsx
Normal file
106
src/app/common/elements/passwordfield.tsx
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as mobxReact from "mobx-react";
|
||||||
|
import { boundMethod } from "autobind-decorator";
|
||||||
|
import cn from "classnames";
|
||||||
|
import { If } from "tsx-control-statements/components";
|
||||||
|
import { TextFieldState, TextField } from "./textfield";
|
||||||
|
|
||||||
|
import "./passwordfield.less";
|
||||||
|
|
||||||
|
interface PasswordFieldState extends TextFieldState {
|
||||||
|
passwordVisible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mobxReact.observer
|
||||||
|
class PasswordField extends TextField {
|
||||||
|
state: PasswordFieldState;
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
...this.state,
|
||||||
|
passwordVisible: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
togglePasswordVisibility() {
|
||||||
|
//@ts-ignore
|
||||||
|
this.setState((prevState) => ({
|
||||||
|
//@ts-ignore
|
||||||
|
passwordVisible: !prevState.passwordVisible,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
// Call the parent handleInputChange method
|
||||||
|
super.handleInputChange(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { decoration, className, placeholder, maxLength, label } = this.props;
|
||||||
|
const { focused, internalValue, error, passwordVisible } = this.state;
|
||||||
|
const inputValue = this.props.value ?? internalValue;
|
||||||
|
|
||||||
|
// The input should always receive the real value
|
||||||
|
const inputProps = {
|
||||||
|
className: cn("wave-textfield-inner-input", { "offset-left": decoration?.startDecoration }),
|
||||||
|
ref: this.inputRef,
|
||||||
|
id: label,
|
||||||
|
value: inputValue, // Always use the real value here
|
||||||
|
onChange: this.handleInputChange,
|
||||||
|
onFocus: this.handleFocus,
|
||||||
|
onBlur: this.handleBlur,
|
||||||
|
placeholder: placeholder,
|
||||||
|
maxLength: maxLength,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(`wave-textfield wave-password ${className || ""}`, {
|
||||||
|
focused: focused,
|
||||||
|
error: error,
|
||||||
|
"no-label": !label,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{decoration?.startDecoration && <>{decoration.startDecoration}</>}
|
||||||
|
<div className="wave-textfield-inner">
|
||||||
|
<label
|
||||||
|
className={cn("wave-textfield-inner-label", {
|
||||||
|
float: this.state.hasContent || this.state.focused || placeholder,
|
||||||
|
"offset-left": decoration?.startDecoration,
|
||||||
|
})}
|
||||||
|
htmlFor={label}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
<If condition={passwordVisible}>
|
||||||
|
<input {...inputProps} type="text" />
|
||||||
|
</If>
|
||||||
|
<If condition={!passwordVisible}>
|
||||||
|
<input {...inputProps} type="password" />
|
||||||
|
</If>
|
||||||
|
<div
|
||||||
|
className="wave-textfield-inner-eye"
|
||||||
|
onClick={this.togglePasswordVisibility}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
<If condition={passwordVisible}>
|
||||||
|
<i className="fa-sharp fa-solid fa-eye"></i>
|
||||||
|
</If>
|
||||||
|
<If condition={!passwordVisible}>
|
||||||
|
<i className="fa-sharp fa-solid fa-eye-slash"></i>
|
||||||
|
</If>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{decoration?.endDecoration && <>{decoration.endDecoration}</>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { PasswordField };
|
9
src/app/common/elements/resizablesidebar.less
Normal file
9
src/app/common/elements/resizablesidebar.less
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
@import "../../../app/common/themes/themes.less";
|
||||||
|
|
||||||
|
.sidebar-handle {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 5px;
|
||||||
|
cursor: col-resize;
|
||||||
|
}
|
171
src/app/common/elements/resizablesidebar.tsx
Normal file
171
src/app/common/elements/resizablesidebar.tsx
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as mobxReact from "mobx-react";
|
||||||
|
import * as mobx from "mobx";
|
||||||
|
import { boundMethod } from "autobind-decorator";
|
||||||
|
import cn from "classnames";
|
||||||
|
import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
|
||||||
|
import { MagicLayout } from "../../magiclayout";
|
||||||
|
|
||||||
|
import "./resizablesidebar.less";
|
||||||
|
|
||||||
|
type OV<V> = mobx.IObservableValue<V>;
|
||||||
|
|
||||||
|
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={{
|
||||||
|
[this.props.position === "left" ? "right" : "left"]: 0,
|
||||||
|
}}
|
||||||
|
onMouseDown={this.startResizing}
|
||||||
|
onDoubleClick={this.toggleCollapsed}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ResizableSidebar };
|
36
src/app/common/elements/settingserror.tsx
Normal file
36
src/app/common/elements/settingserror.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as mobxReact from "mobx-react";
|
||||||
|
import * as mobx from "mobx";
|
||||||
|
import { boundMethod } from "autobind-decorator";
|
||||||
|
|
||||||
|
type OV<V> = mobx.IObservableValue<V>;
|
||||||
|
|
||||||
|
@mobxReact.observer
|
||||||
|
class SettingsError extends React.Component<{ errorMessage: OV<string> }, {}> {
|
||||||
|
@boundMethod
|
||||||
|
dismissError(): void {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.props.errorMessage.set(null);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.props.errorMessage.get() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="settings-field settings-error">
|
||||||
|
<div>Error: {this.props.errorMessage.get()}</div>
|
||||||
|
<div className="flex-spacer" />
|
||||||
|
<div onClick={this.dismissError} className="error-dismiss">
|
||||||
|
<i className="fa-sharp fa-solid fa-xmark" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { SettingsError };
|
28
src/app/common/elements/showwaveshellinstallprompt.tsx
Normal file
28
src/app/common/elements/showwaveshellinstallprompt.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import { GlobalModel } from "../../../model/model";
|
||||||
|
import * as appconst from "../../appconst";
|
||||||
|
|
||||||
|
function ShowWaveShellInstallPrompt(callbackFn: () => void) {
|
||||||
|
let message: string = `
|
||||||
|
In order to use Wave's advanced features like unified history and persistent sessions, Wave installs a small, open-source helper program called WaveShell on your remote machine. WaveShell does not open any external ports and only communicates with your *local* Wave terminal instance over ssh. For more information please see [the docs](https://docs.waveterm.dev/reference/waveshell).
|
||||||
|
`;
|
||||||
|
message = message.trim();
|
||||||
|
let prtn = GlobalModel.showAlert({
|
||||||
|
message: message,
|
||||||
|
confirm: true,
|
||||||
|
markdown: true,
|
||||||
|
confirmflag: appconst.ConfirmKey_HideShellPrompt,
|
||||||
|
});
|
||||||
|
prtn.then((confirm) => {
|
||||||
|
if (!confirm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (callbackFn) {
|
||||||
|
callbackFn();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ShowWaveShellInstallPrompt };
|
30
src/app/common/elements/status.less
Normal file
30
src/app/common/elements/status.less
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
@import "../../../app/common/themes/themes.less";
|
||||||
|
|
||||||
|
.wave-status-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
height: 6px;
|
||||||
|
width: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot.green {
|
||||||
|
background-color: @status-connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot.red {
|
||||||
|
background-color: @status-error;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot.gray {
|
||||||
|
background-color: @status-disconnected;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot.yellow {
|
||||||
|
background-color: @status-connecting;
|
||||||
|
}
|
||||||
|
}
|
34
src/app/common/elements/status.tsx
Normal file
34
src/app/common/elements/status.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { boundMethod } from "autobind-decorator";
|
||||||
|
|
||||||
|
import "./status.less";
|
||||||
|
|
||||||
|
interface StatusProps {
|
||||||
|
status: "green" | "red" | "gray" | "yellow";
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Status extends React.Component<StatusProps> {
|
||||||
|
@boundMethod
|
||||||
|
renderDot() {
|
||||||
|
const { status } = this.props;
|
||||||
|
|
||||||
|
return <div className={`dot ${status}`} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { text } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="wave-status-container">
|
||||||
|
{this.renderDot()}
|
||||||
|
<span>{text}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Status };
|
82
src/app/common/elements/textfield.less
Normal file
82
src/app/common/elements/textfield.less
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
@import "../../../app/common/themes/themes.less";
|
||||||
|
|
||||||
|
.wave-textfield {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
position: relative;
|
||||||
|
height: 44px;
|
||||||
|
min-width: 412px;
|
||||||
|
gap: 6px;
|
||||||
|
border: 1px solid rgba(241, 246, 243, 0.15);
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
|
||||||
|
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.focused {
|
||||||
|
border-color: @term-green;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
border-color: @term-red;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
&-label {
|
||||||
|
position: absolute;
|
||||||
|
left: 16px;
|
||||||
|
top: 16px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
color: @text-secondary;
|
||||||
|
line-height: 10px;
|
||||||
|
|
||||||
|
&.float {
|
||||||
|
font-size: 10px;
|
||||||
|
top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.offset-left {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-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;
|
||||||
|
|
||||||
|
&.offset-left {
|
||||||
|
padding: 5px 16px 5px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.no-label {
|
||||||
|
height: 34px;
|
||||||
|
|
||||||
|
input {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
173
src/app/common/elements/textfield.tsx
Normal file
173
src/app/common/elements/textfield.tsx
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { boundMethod } from "autobind-decorator";
|
||||||
|
import cn from "classnames";
|
||||||
|
import { If } from "tsx-control-statements/components";
|
||||||
|
|
||||||
|
import "./textfield.less";
|
||||||
|
|
||||||
|
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;
|
||||||
|
maxLength?: number;
|
||||||
|
autoFocus?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TextFieldState {
|
||||||
|
focused: boolean;
|
||||||
|
internalValue: string;
|
||||||
|
error: boolean;
|
||||||
|
showHelpText: boolean;
|
||||||
|
hasContent: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to handle focus at the component level
|
||||||
|
@boundMethod
|
||||||
|
handleComponentFocus() {
|
||||||
|
if (this.inputRef.current && !this.inputRef.current.contains(document.activeElement)) {
|
||||||
|
this.inputRef.current.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to handle blur at the component level
|
||||||
|
@boundMethod
|
||||||
|
handleComponentBlur() {
|
||||||
|
if (this.inputRef.current?.contains(document.activeElement)) {
|
||||||
|
this.inputRef.current.blur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
const { required, onChange } = 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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange && onChange(inputValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { label, value, placeholder, decoration, className, maxLength, autoFocus, disabled } = this.props;
|
||||||
|
const { focused, internalValue, error } = this.state;
|
||||||
|
|
||||||
|
// Decide if the input should behave as controlled or uncontrolled
|
||||||
|
const inputValue = value ?? internalValue;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("wave-textfield", className, {
|
||||||
|
focused: focused,
|
||||||
|
error: error,
|
||||||
|
disabled: disabled,
|
||||||
|
"no-label": !label,
|
||||||
|
})}
|
||||||
|
onFocus={this.handleComponentFocus}
|
||||||
|
onBlur={this.handleComponentBlur}
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{decoration?.startDecoration && <>{decoration.startDecoration}</>}
|
||||||
|
<div className="wave-textfield-inner">
|
||||||
|
<If condition={label}>
|
||||||
|
<label
|
||||||
|
className={cn("wave-textfield-inner-label", {
|
||||||
|
float: this.state.hasContent || this.state.focused || placeholder,
|
||||||
|
"offset-left": decoration?.startDecoration,
|
||||||
|
})}
|
||||||
|
htmlFor={label}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
</If>
|
||||||
|
<input
|
||||||
|
className={cn("wave-textfield-inner-input", { "offset-left": decoration?.startDecoration })}
|
||||||
|
ref={this.inputRef}
|
||||||
|
id={label}
|
||||||
|
value={inputValue}
|
||||||
|
onChange={this.handleInputChange}
|
||||||
|
onFocus={this.handleFocus}
|
||||||
|
onBlur={this.handleBlur}
|
||||||
|
placeholder={placeholder}
|
||||||
|
maxLength={maxLength}
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{decoration?.endDecoration && <>{decoration.endDecoration}</>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { TextField };
|
||||||
|
export type { TextFieldProps, TextFieldDecorationProps, TextFieldState };
|
47
src/app/common/elements/toggle.less
Normal file
47
src/app/common/elements/toggle.less
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
@import "../../../app/common/themes/themes.less";
|
||||||
|
|
||||||
|
.checkbox-toggle {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 40px;
|
||||||
|
height: 22px;
|
||||||
|
|
||||||
|
input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: #333;
|
||||||
|
transition: 0.5s;
|
||||||
|
border-radius: 33px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
left: 2px;
|
||||||
|
bottom: 2px;
|
||||||
|
background-color: @term-white;
|
||||||
|
transition: 0.5s;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider {
|
||||||
|
background-color: @term-green;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider:before {
|
||||||
|
transform: translateX(18px);
|
||||||
|
}
|
||||||
|
}
|
28
src/app/common/elements/toggle.tsx
Normal file
28
src/app/common/elements/toggle.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { boundMethod } from "autobind-decorator";
|
||||||
|
|
||||||
|
import "./toggle.less";
|
||||||
|
|
||||||
|
class Toggle extends React.Component<{ checked: boolean; onChange: (value: boolean) => void }, {}> {
|
||||||
|
@boundMethod
|
||||||
|
handleChange(e: any): void {
|
||||||
|
let { onChange } = this.props;
|
||||||
|
if (onChange != null) {
|
||||||
|
onChange(e.target.checked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<label className="checkbox-toggle">
|
||||||
|
<input type="checkbox" checked={this.props.checked} onChange={this.handleChange} />
|
||||||
|
<span className="slider" />
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Toggle };
|
23
src/app/common/elements/tooltip.less
Normal file
23
src/app/common/elements/tooltip.less
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
@import "../../../app/common/themes/themes.less";
|
||||||
|
|
||||||
|
.wave-tooltip {
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1000;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #777;
|
||||||
|
background-color: #444;
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 300px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
display: inline;
|
||||||
|
font-size: 13px;
|
||||||
|
fill: @base-color;
|
||||||
|
padding-top: 0.2em;
|
||||||
|
}
|
||||||
|
}
|
84
src/app/common/elements/tooltip.tsx
Normal file
84
src/app/common/elements/tooltip.tsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as mobxReact from "mobx-react";
|
||||||
|
import { boundMethod } from "autobind-decorator";
|
||||||
|
import cn from "classnames";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
|
||||||
|
import "./tooltip.less";
|
||||||
|
|
||||||
|
interface TooltipProps {
|
||||||
|
message: React.ReactNode;
|
||||||
|
icon?: React.ReactNode; // Optional icon property
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TooltipState {
|
||||||
|
isVisible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mobxReact.observer
|
||||||
|
class Tooltip extends React.Component<TooltipProps, TooltipState> {
|
||||||
|
iconRef: React.RefObject<HTMLDivElement>;
|
||||||
|
|
||||||
|
constructor(props: TooltipProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
isVisible: false,
|
||||||
|
};
|
||||||
|
this.iconRef = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
showBubble() {
|
||||||
|
this.setState({ isVisible: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
hideBubble() {
|
||||||
|
this.setState({ isVisible: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
calculatePosition() {
|
||||||
|
// Get the position of the icon element
|
||||||
|
const iconElement = this.iconRef.current;
|
||||||
|
if (iconElement) {
|
||||||
|
const rect = iconElement.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
top: `${rect.bottom + window.scrollY - 29}px`,
|
||||||
|
left: `${rect.left + window.scrollX + rect.width / 2 - 17.5}px`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
renderBubble() {
|
||||||
|
if (!this.state.isVisible) return null;
|
||||||
|
|
||||||
|
const style = this.calculatePosition();
|
||||||
|
|
||||||
|
return ReactDOM.createPortal(
|
||||||
|
<div className={cn("wave-tooltip", this.props.className)} style={style}>
|
||||||
|
{this.props.icon && <div className="wave-tooltip-icon">{this.props.icon}</div>}
|
||||||
|
<div className="wave-tooltip-message">{this.props.message}</div>
|
||||||
|
</div>,
|
||||||
|
document.getElementById("app")!
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div onMouseEnter={this.showBubble} onMouseLeave={this.hideBubble} ref={this.iconRef}>
|
||||||
|
{this.props.children}
|
||||||
|
{this.renderBubble()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Tooltip };
|
@ -6,7 +6,7 @@ import * as mobxReact from "mobx-react";
|
|||||||
import * as mobx from "mobx";
|
import * as mobx from "mobx";
|
||||||
import { boundMethod } from "autobind-decorator";
|
import { boundMethod } from "autobind-decorator";
|
||||||
import { GlobalModel } from "../../../model/model";
|
import { GlobalModel } from "../../../model/model";
|
||||||
import { Modal, LinkButton } from "../common";
|
import { Modal, LinkButton } from "../elements";
|
||||||
import * as util from "../../../util/util";
|
import * as util from "../../../util/util";
|
||||||
|
|
||||||
import logo from "../../assets/waveterm-logo-with-bg.svg";
|
import logo from "../../assets/waveterm-logo-with-bg.svg";
|
||||||
|
@ -5,7 +5,7 @@ import * as React from "react";
|
|||||||
import * as mobxReact from "mobx-react";
|
import * as mobxReact from "mobx-react";
|
||||||
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 { Markdown, Modal, Button, Checkbox } from "../common";
|
import { Markdown, Modal, Button, Checkbox } from "../elements";
|
||||||
import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
|
import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
|
||||||
|
|
||||||
import "./alert.less";
|
import "./alert.less";
|
||||||
|
@ -6,7 +6,7 @@ import * as mobxReact from "mobx-react";
|
|||||||
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 { GlobalModel } from "../../../model/model";
|
import { GlobalModel } from "../../../model/model";
|
||||||
import { Modal, Button } from "../common";
|
import { Modal, Button } from "../elements";
|
||||||
|
|
||||||
import "./clientstop.less";
|
import "./clientstop.less";
|
||||||
|
|
||||||
|
@ -8,9 +8,17 @@ import { boundMethod } from "autobind-decorator";
|
|||||||
import { If } from "tsx-control-statements/components";
|
import { If } from "tsx-control-statements/components";
|
||||||
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../model/model";
|
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../model/model";
|
||||||
import * as T from "../../../types/types";
|
import * as T from "../../../types/types";
|
||||||
import { Modal, TextField, NumberField, InputDecoration, Dropdown, PasswordField, Tooltip, ShowWaveShellInstallPrompt } from "../common";
|
import {
|
||||||
|
Modal,
|
||||||
|
TextField,
|
||||||
|
NumberField,
|
||||||
|
InputDecoration,
|
||||||
|
Dropdown,
|
||||||
|
PasswordField,
|
||||||
|
Tooltip,
|
||||||
|
ShowWaveShellInstallPrompt,
|
||||||
|
} from "../elements";
|
||||||
import * as util from "../../../util/util";
|
import * as util from "../../../util/util";
|
||||||
import * as appconst from "../../appconst";
|
|
||||||
|
|
||||||
import "./createremoteconn.less";
|
import "./createremoteconn.less";
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import * as mobxReact from "mobx-react";
|
|||||||
import * as mobx from "mobx";
|
import * as mobx from "mobx";
|
||||||
import { boundMethod } from "autobind-decorator";
|
import { boundMethod } from "autobind-decorator";
|
||||||
import { GlobalModel } from "../../../model/model";
|
import { GlobalModel } from "../../../model/model";
|
||||||
import { Modal, Button } from "../common";
|
import { Modal, Button } from "../elements";
|
||||||
|
|
||||||
import "./disconnected.less";
|
import "./disconnected.less";
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import { If } from "tsx-control-statements/components";
|
|||||||
import { boundMethod } from "autobind-decorator";
|
import { boundMethod } from "autobind-decorator";
|
||||||
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../model/model";
|
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../model/model";
|
||||||
import * as T from "../../../types/types";
|
import * as T from "../../../types/types";
|
||||||
import { Modal, TextField, InputDecoration, Dropdown, PasswordField, Tooltip } from "../common";
|
import { Modal, TextField, InputDecoration, Dropdown, PasswordField, Tooltip } from "../elements";
|
||||||
import * as util from "../../../util/util";
|
import * as util from "../../../util/util";
|
||||||
|
|
||||||
import "./editremoteconn.less";
|
import "./editremoteconn.less";
|
||||||
|
@ -6,7 +6,7 @@ import * as mobxReact from "mobx-react";
|
|||||||
import * as mobx from "mobx";
|
import * as mobx from "mobx";
|
||||||
import { boundMethod } from "autobind-decorator";
|
import { boundMethod } from "autobind-decorator";
|
||||||
import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
|
import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
|
||||||
import { SettingsError, Modal, Dropdown } from "../common";
|
import { SettingsError, Modal, Dropdown } from "../elements";
|
||||||
import { LineType, RendererPluginType } from "../../../types/types";
|
import { LineType, RendererPluginType } from "../../../types/types";
|
||||||
import { PluginModel } from "../../../plugins/plugins";
|
import { PluginModel } from "../../../plugins/plugins";
|
||||||
import { commandRtnHandler } from "../../../util/util";
|
import { commandRtnHandler } from "../../../util/util";
|
||||||
|
@ -8,7 +8,7 @@ import { boundMethod } from "autobind-decorator";
|
|||||||
import { If, For } from "tsx-control-statements/components";
|
import { If, For } from "tsx-control-statements/components";
|
||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import { GlobalModel, GlobalCommandRunner, TabColors, TabIcons, Screen } from "../../../model/model";
|
import { GlobalModel, GlobalCommandRunner, TabColors, TabIcons, Screen } from "../../../model/model";
|
||||||
import { Toggle, InlineSettingsTextEdit, SettingsError, Modal, Dropdown, Tooltip } from "../common";
|
import { Toggle, InlineSettingsTextEdit, SettingsError, Modal, Dropdown, Tooltip } from "../elements";
|
||||||
import { RemoteType } from "../../../types/types";
|
import { RemoteType } from "../../../types/types";
|
||||||
import * as util from "../../../util/util";
|
import * as util from "../../../util/util";
|
||||||
import { commandRtnHandler } from "../../../util/util";
|
import { commandRtnHandler } from "../../../util/util";
|
||||||
|
@ -14,6 +14,15 @@
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
.settings-label > div:first-child {
|
||||||
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-settings-tooltip i {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 0.5px;
|
||||||
|
}
|
||||||
|
@ -6,7 +6,7 @@ import * as mobxReact from "mobx-react";
|
|||||||
import * as mobx from "mobx";
|
import * as mobx from "mobx";
|
||||||
import { boundMethod } from "autobind-decorator";
|
import { boundMethod } from "autobind-decorator";
|
||||||
import { GlobalModel, GlobalCommandRunner, Session } from "../../../model/model";
|
import { GlobalModel, GlobalCommandRunner, Session } from "../../../model/model";
|
||||||
import { Toggle, InlineSettingsTextEdit, SettingsError, InfoMessage, Modal } from "../common";
|
import { Toggle, InlineSettingsTextEdit, SettingsError, Modal, Tooltip } from "../elements";
|
||||||
import * as util from "../../../util/util";
|
import * as util from "../../../util/util";
|
||||||
import { commandRtnHandler } from "../../../util/util";
|
import { commandRtnHandler } from "../../../util/util";
|
||||||
|
|
||||||
@ -111,10 +111,14 @@ class SessionSettingsModal extends React.Component<{}, {}> {
|
|||||||
<div className="settings-field">
|
<div className="settings-field">
|
||||||
<div className="settings-label">
|
<div className="settings-label">
|
||||||
<div>Archived</div>
|
<div>Archived</div>
|
||||||
<InfoMessage width={400}>
|
<Tooltip
|
||||||
Archive will hide the workspace from the active menu. Commands and output will be
|
className="session-settings-tooltip"
|
||||||
retained in history.
|
message="Archive will hide the workspace from the active menu. Commands and output will be
|
||||||
</InfoMessage>
|
retained in history."
|
||||||
|
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||||
|
>
|
||||||
|
{<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-input">
|
<div className="settings-input">
|
||||||
<Toggle checked={this.session.archived.get()} onChange={this.handleChangeArchived} />
|
<Toggle checked={this.session.archived.get()} onChange={this.handleChangeArchived} />
|
||||||
@ -123,9 +127,13 @@ class SessionSettingsModal extends React.Component<{}, {}> {
|
|||||||
<div className="settings-field">
|
<div className="settings-field">
|
||||||
<div className="settings-label">
|
<div className="settings-label">
|
||||||
<div>Actions</div>
|
<div>Actions</div>
|
||||||
<InfoMessage width={400}>
|
<Tooltip
|
||||||
Delete will remove the workspace, removing all commands and output from history.
|
className="session-settings-tooltip"
|
||||||
</InfoMessage>
|
message="Delete will remove the workspace, removing all commands and output from history."
|
||||||
|
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||||
|
>
|
||||||
|
{<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-input">
|
<div className="settings-input">
|
||||||
<div
|
<div
|
||||||
|
@ -8,7 +8,7 @@ import { boundMethod } from "autobind-decorator";
|
|||||||
import { For } from "tsx-control-statements/components";
|
import { For } from "tsx-control-statements/components";
|
||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
|
import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
|
||||||
import { Modal, TextField, InputDecoration, Tooltip } from "../common";
|
import { Modal, TextField, InputDecoration, Tooltip } from "../elements";
|
||||||
import * as util from "../../../util/util";
|
import * as util from "../../../util/util";
|
||||||
import { Screen } from "../../../model/model";
|
import { Screen } from "../../../model/model";
|
||||||
import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg";
|
import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg";
|
||||||
|
@ -5,7 +5,7 @@ import * as React from "react";
|
|||||||
import * as mobxReact from "mobx-react";
|
import * as mobxReact from "mobx-react";
|
||||||
import { boundMethod } from "autobind-decorator";
|
import { boundMethod } from "autobind-decorator";
|
||||||
import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
|
import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
|
||||||
import { Toggle, Modal, Button } from "../common";
|
import { Toggle, Modal, Button } from "../elements";
|
||||||
import * as util from "../../../util/util";
|
import * as util from "../../../util/util";
|
||||||
import { ClientDataType } from "../../../types/types";
|
import { ClientDataType } from "../../../types/types";
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import { If, For } from "tsx-control-statements/components";
|
|||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../model/model";
|
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../model/model";
|
||||||
import * as T from "../../../types/types";
|
import * as T from "../../../types/types";
|
||||||
import { Modal, Tooltip, Button, Status } from "../common";
|
import { Modal, Tooltip, Button, Status } from "../elements";
|
||||||
import * as util from "../../../util/util";
|
import * as util from "../../../util/util";
|
||||||
import * as textmeasure from "../../../util/textmeasure";
|
import * as textmeasure from "../../../util/textmeasure";
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import { boundMethod } from "autobind-decorator";
|
|||||||
import { If, For } from "tsx-control-statements/components";
|
import { If, For } from "tsx-control-statements/components";
|
||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import { GlobalModel, RemotesModel, GlobalCommandRunner } from "../../model/model";
|
import { GlobalModel, RemotesModel, GlobalCommandRunner } from "../../model/model";
|
||||||
import { Button, IconButton, Status, ShowWaveShellInstallPrompt } from "../common/common";
|
import { Button, Status, ShowWaveShellInstallPrompt } from "../common/elements";
|
||||||
import * as T from "../../types/types";
|
import * as T from "../../types/types";
|
||||||
import * as util from "../../util/util";
|
import * as util from "../../util/util";
|
||||||
import * as appconst from "../appconst";
|
import * as appconst from "../appconst";
|
||||||
|
@ -14,7 +14,7 @@ import dayjs from "dayjs";
|
|||||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||||
import { Line } from "../line/linecomps";
|
import { Line } from "../line/linecomps";
|
||||||
import { CmdStrCode } from "../common/common";
|
import { CmdStrCode } from "../common/elements";
|
||||||
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../util/keyutil";
|
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../util/keyutil";
|
||||||
|
|
||||||
import { ReactComponent as FavoritesIcon } from "../assets/icons/favourites.svg";
|
import { ReactComponent as FavoritesIcon } from "../assets/icons/favourites.svg";
|
||||||
|
@ -23,7 +23,7 @@ import type {
|
|||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
|
|
||||||
import type { LineContainerModel } from "../../model/model";
|
import type { LineContainerModel } from "../../model/model";
|
||||||
import { renderCmdText } from "../common/common";
|
import { renderCmdText } from "../common/elements";
|
||||||
import { SimpleBlobRenderer } from "../../plugins/core/basicrenderer";
|
import { SimpleBlobRenderer } from "../../plugins/core/basicrenderer";
|
||||||
import { IncrementalRenderer } from "../../plugins/core/incrementalrenderer";
|
import { IncrementalRenderer } from "../../plugins/core/incrementalrenderer";
|
||||||
import { TerminalRenderer } from "../../plugins/terminal/terminal";
|
import { TerminalRenderer } from "../../plugins/terminal/terminal";
|
||||||
|
@ -7,7 +7,7 @@ import * as mobx from "mobx";
|
|||||||
import { boundMethod } from "autobind-decorator";
|
import { boundMethod } from "autobind-decorator";
|
||||||
import { GlobalModel } from "../../model/model";
|
import { GlobalModel } from "../../model/model";
|
||||||
import { PluginModel } from "../../plugins/plugins";
|
import { PluginModel } from "../../plugins/plugins";
|
||||||
import { Markdown } from "../common/common";
|
import { Markdown } from "../common/elements";
|
||||||
|
|
||||||
import { ReactComponent as XmarkIcon } from "../assets/icons/line/xmark.svg";
|
import { ReactComponent as XmarkIcon } from "../assets/icons/line/xmark.svg";
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ import { ReactComponent as SettingsIcon } from "../assets/icons/settings.svg";
|
|||||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||||
import { GlobalModel, GlobalCommandRunner, Session, VERSION } from "../../model/model";
|
import { GlobalModel, GlobalCommandRunner, Session, VERSION } from "../../model/model";
|
||||||
import { isBlank, openLink } from "../../util/util";
|
import { isBlank, openLink } from "../../util/util";
|
||||||
import { ResizableSidebar } from "../common/common";
|
import { ResizableSidebar } from "../common/elements";
|
||||||
import * as constants from "../appconst";
|
import * as constants from "../appconst";
|
||||||
|
|
||||||
import "./sidebar.less";
|
import "./sidebar.less";
|
||||||
|
@ -12,7 +12,7 @@ import { Prompt } from "../../common/prompt/prompt";
|
|||||||
import { TextAreaInput } from "./textareainput";
|
import { TextAreaInput } from "./textareainput";
|
||||||
import { If, For } from "tsx-control-statements/components";
|
import { If, For } from "tsx-control-statements/components";
|
||||||
import type { OpenAICmdInfoChatMessageType } from "../../../types/types";
|
import type { OpenAICmdInfoChatMessageType } from "../../../types/types";
|
||||||
import { Markdown } from "../../common/common";
|
import { Markdown } from "../../common/elements";
|
||||||
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../../util/keyutil";
|
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../../util/keyutil";
|
||||||
|
|
||||||
@mobxReact.observer
|
@mobxReact.observer
|
||||||
|
@ -11,7 +11,7 @@ import dayjs from "dayjs";
|
|||||||
import type { RemoteType, RemoteInstanceType, RemotePtrType } from "../../../types/types";
|
import type { RemoteType, RemoteInstanceType, RemotePtrType } from "../../../types/types";
|
||||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||||
import { GlobalModel, GlobalCommandRunner, Screen } from "../../../model/model";
|
import { GlobalModel, GlobalCommandRunner, Screen } from "../../../model/model";
|
||||||
import { renderCmdText } from "../../common/common";
|
import { renderCmdText } from "../../common/elements";
|
||||||
import { TextAreaInput } from "./textareainput";
|
import { TextAreaInput } from "./textareainput";
|
||||||
import { InfoMsg } from "./infomsg";
|
import { InfoMsg } from "./infomsg";
|
||||||
import { HistoryInfo } from "./historyinfo";
|
import { HistoryInfo } from "./historyinfo";
|
||||||
|
@ -10,16 +10,24 @@ import { If, For } from "tsx-control-statements/components";
|
|||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import { debounce } from "throttle-debounce";
|
import { debounce } from "throttle-debounce";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { GlobalCommandRunner, TabColors, TabIcons, ForwardLineContainer, GlobalModel, ScreenLines, Screen, Session } from "../../../model/model";
|
import {
|
||||||
|
GlobalCommandRunner,
|
||||||
|
TabColors,
|
||||||
|
TabIcons,
|
||||||
|
ForwardLineContainer,
|
||||||
|
GlobalModel,
|
||||||
|
ScreenLines,
|
||||||
|
Screen,
|
||||||
|
Session,
|
||||||
|
} from "../../../model/model";
|
||||||
import type { LineType, RenderModeType, LineFactoryProps } from "../../../types/types";
|
import type { LineType, RenderModeType, LineFactoryProps } from "../../../types/types";
|
||||||
import * as T from "../../../types/types";
|
import * as T from "../../../types/types";
|
||||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||||
import { Button } from "../../common/common";
|
import { Button, TextField, Dropdown } from "../../common/elements";
|
||||||
import { getRemoteStr } from "../../common/prompt/prompt";
|
import { getRemoteStr } from "../../common/prompt/prompt";
|
||||||
import { Line } from "../../line/linecomps";
|
import { Line } from "../../line/linecomps";
|
||||||
import { LinesView } from "../../line/linesview";
|
import { LinesView } from "../../line/linesview";
|
||||||
import * as util from "../../../util/util";
|
import * as util from "../../../util/util";
|
||||||
import { TextField, Dropdown } from "../../common/common";
|
|
||||||
import { ReactComponent as EllipseIcon } from "../../assets/icons/ellipse.svg";
|
import { ReactComponent as EllipseIcon } from "../../assets/icons/ellipse.svg";
|
||||||
import { ReactComponent as Check12Icon } from "../../assets/icons/check12.svg";
|
import { ReactComponent as Check12Icon } from "../../assets/icons/check12.svg";
|
||||||
import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg";
|
import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg";
|
||||||
|
@ -8,7 +8,7 @@ import { boundMethod } from "autobind-decorator";
|
|||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import { GlobalModel, GlobalCommandRunner, Screen } from "../../../model/model";
|
import { GlobalModel, GlobalCommandRunner, Screen } from "../../../model/model";
|
||||||
import { ActionsIcon, StatusIndicator, CenteredIcon } from "../../common/icons/icons";
|
import { ActionsIcon, StatusIndicator, CenteredIcon } from "../../common/icons/icons";
|
||||||
import { renderCmdText } from "../../common/common";
|
import { renderCmdText } from "../../common/elements";
|
||||||
import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg";
|
import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg";
|
||||||
import * as constants from "../../appconst";
|
import * as constants from "../../appconst";
|
||||||
import { Reorder } from "framer-motion";
|
import { Reorder } from "framer-motion";
|
||||||
|
@ -5,7 +5,7 @@ import * as React from "react";
|
|||||||
import * as T from "../../types/types";
|
import * as T from "../../types/types";
|
||||||
import Editor, { Monaco } from "@monaco-editor/react";
|
import Editor, { Monaco } from "@monaco-editor/react";
|
||||||
import type * as MonacoTypes from "monaco-editor/esm/vs/editor/editor.api";
|
import type * as MonacoTypes from "monaco-editor/esm/vs/editor/editor.api";
|
||||||
import { Markdown } from "../../app/common/common";
|
import { Markdown } from "../../app/common/elements";
|
||||||
import { GlobalModel, GlobalCommandRunner } from "../../model/model";
|
import { GlobalModel, GlobalCommandRunner } from "../../model/model";
|
||||||
import Split from "react-split-it";
|
import Split from "react-split-it";
|
||||||
import loader from "@monaco-editor/loader";
|
import loader from "@monaco-editor/loader";
|
||||||
|
@ -6,7 +6,7 @@ import * as mobx from "mobx";
|
|||||||
import * as mobxReact from "mobx-react";
|
import * as mobxReact from "mobx-react";
|
||||||
import * as T from "../../types/types";
|
import * as T from "../../types/types";
|
||||||
import { sprintf } from "sprintf-js";
|
import { sprintf } from "sprintf-js";
|
||||||
import { Markdown } from "../../app/common/common";
|
import { Markdown } from "../../app/common/elements";
|
||||||
|
|
||||||
import "./markdown.less";
|
import "./markdown.less";
|
||||||
|
|
||||||
@ -17,7 +17,13 @@ const DefaultMaxMarkdownWidth = 1000;
|
|||||||
|
|
||||||
@mobxReact.observer
|
@mobxReact.observer
|
||||||
class SimpleMarkdownRenderer extends React.Component<
|
class SimpleMarkdownRenderer extends React.Component<
|
||||||
{ data: T.ExtBlob; context: T.RendererContext; opts: T.RendererOpts; savedHeight: number, lineState: T.LineStateType },
|
{
|
||||||
|
data: T.ExtBlob;
|
||||||
|
context: T.RendererContext;
|
||||||
|
opts: T.RendererOpts;
|
||||||
|
savedHeight: number;
|
||||||
|
lineState: T.LineStateType;
|
||||||
|
},
|
||||||
{}
|
{}
|
||||||
> {
|
> {
|
||||||
markdownText: OV<string> = mobx.observable.box(null, { name: "markdownText" });
|
markdownText: OV<string> = mobx.observable.box(null, { name: "markdownText" });
|
||||||
@ -74,7 +80,10 @@ class SimpleMarkdownRenderer extends React.Component<
|
|||||||
maxHeight: opts.maxSize.height,
|
maxHeight: opts.maxSize.height,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Markdown text={this.markdownText.get()} style={{ maxHeight: opts.maxSize.height, maxWidth: DefaultMaxMarkdownWidth }} />
|
<Markdown
|
||||||
|
text={this.markdownText.get()}
|
||||||
|
style={{ maxHeight: opts.maxSize.height, maxWidth: DefaultMaxMarkdownWidth }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -8,7 +8,7 @@ import * as T from "../../types/types";
|
|||||||
import { debounce } from "throttle-debounce";
|
import { debounce } from "throttle-debounce";
|
||||||
import { boundMethod } from "autobind-decorator";
|
import { boundMethod } from "autobind-decorator";
|
||||||
import { PacketDataBuffer } from "../core/ptydata";
|
import { PacketDataBuffer } from "../core/ptydata";
|
||||||
import { Markdown } from "../../app/common/common";
|
import { Markdown } from "../../app/common/elements";
|
||||||
|
|
||||||
import "./openai.less";
|
import "./openai.less";
|
||||||
|
|
||||||
@ -207,7 +207,7 @@ class OpenAIRenderer extends React.Component<{ model: OpenAIRendererModel }> {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
maxHeight: opts.maxSize.height,
|
maxHeight: opts.maxSize.height,
|
||||||
paddingRight: 5
|
paddingRight: 5,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Markdown text={message} style={{ maxHeight: opts.maxSize.height }} />
|
<Markdown text={message} style={{ maxHeight: opts.maxSize.height }} />
|
||||||
@ -238,16 +238,16 @@ class OpenAIRenderer extends React.Component<{ model: OpenAIRendererModel }> {
|
|||||||
if (model.loading.get() && model.savedHeight >= 0 && model.isDone) {
|
if (model.loading.get() && model.savedHeight >= 0 && model.isDone) {
|
||||||
styleVal = {
|
styleVal = {
|
||||||
height: model.savedHeight,
|
height: model.savedHeight,
|
||||||
maxHeight: model.opts.maxSize.height
|
maxHeight: model.opts.maxSize.height,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
let maxWidth = model.opts.maxSize.width
|
let maxWidth = model.opts.maxSize.width;
|
||||||
if (maxWidth > 1000) {
|
if (maxWidth > 1000) {
|
||||||
maxWidth = 1000
|
maxWidth = 1000;
|
||||||
}
|
}
|
||||||
styleVal = {
|
styleVal = {
|
||||||
maxWidth: maxWidth,
|
maxWidth: maxWidth,
|
||||||
maxHeight: model.opts.maxSize.height
|
maxHeight: model.opts.maxSize.height,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
let version = model.version.get();
|
let version = model.version.get();
|
||||||
|
@ -11,6 +11,9 @@ type RemoteStatusTypeStrs = "connected" | "connecting" | "disconnected" | "error
|
|||||||
type LineContainerStrs = "main" | "sidebar" | "history";
|
type LineContainerStrs = "main" | "sidebar" | "history";
|
||||||
|
|
||||||
type OV<V> = mobx.IObservableValue<V>;
|
type OV<V> = mobx.IObservableValue<V>;
|
||||||
|
type OArr<V> = mobx.IObservableArray<V>;
|
||||||
|
type OMap<K, V> = mobx.ObservableMap<K, V>;
|
||||||
|
type CV<V> = mobx.IComputedValue<V>;
|
||||||
|
|
||||||
type SessionDataType = {
|
type SessionDataType = {
|
||||||
sessionid: string;
|
sessionid: string;
|
||||||
@ -832,6 +835,7 @@ export type {
|
|||||||
CmdInputTextPacketType,
|
CmdInputTextPacketType,
|
||||||
OpenAICmdInfoChatMessageType,
|
OpenAICmdInfoChatMessageType,
|
||||||
ScreenStatusIndicatorUpdateType,
|
ScreenStatusIndicatorUpdateType,
|
||||||
|
OV,
|
||||||
};
|
};
|
||||||
|
|
||||||
export { StatusIndicatorLevel };
|
export { StatusIndicatorLevel };
|
||||||
|
@ -156,16 +156,12 @@ func (p *PacketParser) trySendRpcResponse(pk PacketType) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
p.Lock.Lock()
|
p.Lock.Lock()
|
||||||
defer p.Lock.Unlock()
|
|
||||||
entry := p.RpcMap[respId]
|
entry := p.RpcMap[respId]
|
||||||
|
p.Lock.Unlock()
|
||||||
if entry == nil {
|
if entry == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// nonblocking send
|
entry.RespCh <- respPk
|
||||||
select {
|
|
||||||
case entry.RespCh <- respPk:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -27,6 +28,7 @@ import (
|
|||||||
"github.com/kevinburke/ssh_config"
|
"github.com/kevinburke/ssh_config"
|
||||||
"github.com/wavetermdev/waveterm/waveshell/pkg/base"
|
"github.com/wavetermdev/waveterm/waveshell/pkg/base"
|
||||||
"github.com/wavetermdev/waveterm/waveshell/pkg/packet"
|
"github.com/wavetermdev/waveterm/waveshell/pkg/packet"
|
||||||
|
"github.com/wavetermdev/waveterm/waveshell/pkg/server"
|
||||||
"github.com/wavetermdev/waveterm/waveshell/pkg/shellenv"
|
"github.com/wavetermdev/waveterm/waveshell/pkg/shellenv"
|
||||||
"github.com/wavetermdev/waveterm/waveshell/pkg/shellutil"
|
"github.com/wavetermdev/waveterm/waveshell/pkg/shellutil"
|
||||||
"github.com/wavetermdev/waveterm/waveshell/pkg/shexec"
|
"github.com/wavetermdev/waveterm/waveshell/pkg/shexec"
|
||||||
@ -199,6 +201,8 @@ func init() {
|
|||||||
registerCmdFn("remote:reset", RemoteResetCommand)
|
registerCmdFn("remote:reset", RemoteResetCommand)
|
||||||
registerCmdFn("remote:parse", RemoteConfigParseCommand)
|
registerCmdFn("remote:parse", RemoteConfigParseCommand)
|
||||||
|
|
||||||
|
registerCmdFn("copyfile", CopyFileCommand)
|
||||||
|
|
||||||
registerCmdFn("screen:resize", ScreenResizeCommand)
|
registerCmdFn("screen:resize", ScreenResizeCommand)
|
||||||
|
|
||||||
registerCmdFn("line", LineCommand)
|
registerCmdFn("line", LineCommand)
|
||||||
@ -697,6 +701,8 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.
|
|||||||
newPk, rtnErr := EvalMetaCommand(ctxWithHistory, pk)
|
newPk, rtnErr := EvalMetaCommand(ctxWithHistory, pk)
|
||||||
if rtnErr == nil {
|
if rtnErr == nil {
|
||||||
update, rtnErr = HandleCommand(ctxWithHistory, newPk)
|
update, rtnErr = HandleCommand(ctxWithHistory, newPk)
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("error in Eval Meta Command: %v", rtnErr)
|
||||||
}
|
}
|
||||||
if !resolveBool(pk.Kwargs["nohist"], false) {
|
if !resolveBool(pk.Kwargs["nohist"], false) {
|
||||||
// TODO should this be "pk" or "newPk" (2nd arg)
|
// TODO should this be "pk" or "newPk" (2nd arg)
|
||||||
@ -1102,6 +1108,553 @@ func SidebarRemoveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType)
|
|||||||
return &sstore.ModelUpdate{Screens: []*sstore.ScreenType{screen}}, nil
|
return &sstore.ModelUpdate{Screens: []*sstore.ScreenType{screen}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func prettyPrintByteSize(size int64) string {
|
||||||
|
gbSize := float64(size) / float64(1073741824)
|
||||||
|
if gbSize > 1 {
|
||||||
|
return fmt.Sprintf("%.2f Gigabytes", gbSize)
|
||||||
|
}
|
||||||
|
mbSize := float64(size) / float64(1048576)
|
||||||
|
if mbSize > 1 {
|
||||||
|
return fmt.Sprintf("%.2f Megabytes", mbSize)
|
||||||
|
}
|
||||||
|
kbSize := float64(size) / float64(1024)
|
||||||
|
if kbSize > 1 {
|
||||||
|
return fmt.Sprintf("%.2f Kilobytes", kbSize)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%v Bytes", size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// this can only be called in a defer func, because recover() only works inside of a defe
|
||||||
|
func deferWriteCmdStatus(ctx context.Context, cmd *sstore.CmdType, startTime time.Time, exitSuccess bool, outputPos int64) {
|
||||||
|
r := recover()
|
||||||
|
if r != nil {
|
||||||
|
panicMsg := fmt.Sprintf("panic: %v", r)
|
||||||
|
log.Printf("panic: %v\n", panicMsg)
|
||||||
|
writeStringToPty(ctx, cmd, panicMsg, &outputPos)
|
||||||
|
}
|
||||||
|
duration := time.Since(startTime)
|
||||||
|
cmdStatus := sstore.CmdStatusDone
|
||||||
|
var exitCode int
|
||||||
|
if !exitSuccess {
|
||||||
|
cmdStatus = sstore.CmdStatusError
|
||||||
|
exitCode = 1
|
||||||
|
}
|
||||||
|
ck := base.MakeCommandKey(cmd.ScreenId, cmd.LineId)
|
||||||
|
donePk := packet.MakeCmdDonePacket(ck)
|
||||||
|
donePk.Ts = time.Now().UnixMilli()
|
||||||
|
donePk.ExitCode = exitCode
|
||||||
|
donePk.DurationMs = duration.Milliseconds()
|
||||||
|
update, err := sstore.UpdateCmdDoneInfo(context.Background(), ck, donePk, cmdStatus)
|
||||||
|
if err != nil {
|
||||||
|
// nothing to do
|
||||||
|
log.Printf("error updating cmddoneinfo (in openai): %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sstore.MainBus.SendScreenUpdate(cmd.ScreenId, update)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkForWriteReady(ctx context.Context, iter *packet.RpcResponseIter) (string, error) {
|
||||||
|
readyIf, err := iter.Next(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error getting write ready response: %v\r\n", err)
|
||||||
|
}
|
||||||
|
readyPk, ok := readyIf.(*packet.WriteFileReadyPacketType)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("bad write ready packet received %v", readyIf)
|
||||||
|
}
|
||||||
|
if readyPk.Error != "" {
|
||||||
|
return "", fmt.Errorf("ready error: %v", readyPk.Error)
|
||||||
|
}
|
||||||
|
return readyPk.RespId, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkForWriteFinished(ctx context.Context, iter *packet.RpcResponseIter) error {
|
||||||
|
doneIf, err := iter.Next(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while getting done response: %v", err)
|
||||||
|
}
|
||||||
|
writeDonePk, ok := doneIf.(*packet.WriteFileDonePacketType)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("bad done packet received: %T", doneIf)
|
||||||
|
}
|
||||||
|
if writeDonePk.Error != "" {
|
||||||
|
return fmt.Errorf("done error: %v", writeDonePk.Error)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func doCopyLocalFileToRemote(ctx context.Context, cmd *sstore.CmdType, remote_msh *remote.MShellProc, localPath string, destPath string, outputPos int64) {
|
||||||
|
var exitSuccess bool
|
||||||
|
startTime := time.Now()
|
||||||
|
defer func() {
|
||||||
|
deferWriteCmdStatus(ctx, cmd, startTime, exitSuccess, outputPos)
|
||||||
|
}()
|
||||||
|
localFile, err := os.Open(localPath)
|
||||||
|
if err != nil {
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("Error, unable to open file %v: %v\r\n", localFile, localPath), &outputPos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer localFile.Close()
|
||||||
|
writePk := packet.MakeWriteFilePacket()
|
||||||
|
writePk.ReqId = uuid.New().String()
|
||||||
|
writePk.Path = destPath
|
||||||
|
iter, err := remote_msh.WriteFile(ctx, writePk)
|
||||||
|
if err != nil {
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("Error starting file write: %v\r\n", err), &outputPos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer iter.Close()
|
||||||
|
_, err = checkForWriteReady(ctx, iter)
|
||||||
|
if err != nil {
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("Write ready packet error: %v\r\n", err), &outputPos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fileStat, err := localFile.Stat()
|
||||||
|
if err != nil {
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("error: could not get file stat: %v", err), &outputPos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fileSizeBytes := fileStat.Size()
|
||||||
|
bytesWritten := int64(0)
|
||||||
|
lastFileTransferPercentage := float64(0)
|
||||||
|
fileTransferPercentage := float64(0)
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("Source File Size: %s\r\n", prettyPrintByteSize(fileSizeBytes)), &outputPos)
|
||||||
|
writeStringToPty(ctx, cmd, "[", &outputPos)
|
||||||
|
var buffer [server.MaxFileDataPacketSize]byte
|
||||||
|
bufSlice := buffer[:]
|
||||||
|
for {
|
||||||
|
dataPk := packet.MakeFileDataPacket(writePk.ReqId)
|
||||||
|
bytesRead, err := io.ReadFull(localFile, bufSlice)
|
||||||
|
if err == io.ErrUnexpectedEOF || err == io.EOF {
|
||||||
|
dataPk.Eof = true
|
||||||
|
} else if err != nil {
|
||||||
|
dataErr := fmt.Sprintf("error reading file data: %v", err)
|
||||||
|
dataPk.Error = dataErr
|
||||||
|
remote_msh.SendFileData(dataPk)
|
||||||
|
writeStringToPty(ctx, cmd, dataErr, &outputPos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if bytesRead > 0 {
|
||||||
|
dataPk.Data = make([]byte, bytesRead)
|
||||||
|
copy(dataPk.Data, bufSlice[0:bytesRead])
|
||||||
|
bytesWritten += int64(len(dataPk.Data))
|
||||||
|
fileTransferPercentage = float64(bytesWritten) / float64(fileSizeBytes)
|
||||||
|
|
||||||
|
if fileTransferPercentage-lastFileTransferPercentage > float64(0.05) {
|
||||||
|
writeStringToPty(ctx, cmd, "-", &outputPos)
|
||||||
|
lastFileTransferPercentage = fileTransferPercentage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
remote_msh.SendFileData(dataPk)
|
||||||
|
if dataPk.Eof {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = checkForWriteFinished(ctx, iter)
|
||||||
|
if err != nil {
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("Write finished packet error %v", err), &outputPos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeStringToPty(ctx, cmd, "] done. \r\n", &outputPos)
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("Finished transferring. Transferred %v bytes\r\n", fileSizeBytes), &outputPos)
|
||||||
|
exitSuccess = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStatusBarString(filePercentageInt int) string {
|
||||||
|
statusBarString := "\x1b[2k\r["
|
||||||
|
for count := 0; count < 20; count++ {
|
||||||
|
if (filePercentageInt - count*5) > 0 {
|
||||||
|
statusBarString += "-"
|
||||||
|
} else {
|
||||||
|
statusBarString += " "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if filePercentageInt < 100 {
|
||||||
|
statusBarString += fmt.Sprintf("] %v%%", filePercentageInt)
|
||||||
|
} else {
|
||||||
|
statusBarString += "]"
|
||||||
|
}
|
||||||
|
return statusBarString
|
||||||
|
}
|
||||||
|
|
||||||
|
func doCopyRemoteFileToRemote(ctx context.Context, cmd *sstore.CmdType, sourceMsh *remote.MShellProc, destMsh *remote.MShellProc, sourcePath string, destPath string, outputPos int64) {
|
||||||
|
var exitSuccess bool
|
||||||
|
startTime := time.Now()
|
||||||
|
defer func() {
|
||||||
|
deferWriteCmdStatus(ctx, cmd, startTime, exitSuccess, outputPos)
|
||||||
|
}()
|
||||||
|
streamPk := packet.MakeStreamFilePacket()
|
||||||
|
streamPk.ReqId = uuid.New().String()
|
||||||
|
streamPk.Path = sourcePath
|
||||||
|
sourceStreamIter, err := sourceMsh.StreamFile(ctx, streamPk)
|
||||||
|
if err != nil {
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("Error getting file data packet: %v\r\n", err), &outputPos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer sourceStreamIter.Close()
|
||||||
|
respIf, err := sourceStreamIter.Next(ctx)
|
||||||
|
if err != nil {
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("Error getting next packet: %v\r\n", err), &outputPos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, ok := respIf.(*packet.StreamFileResponseType)
|
||||||
|
if !ok {
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("Error in getting packet response: %v\r\n", err), &outputPos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if resp == nil || resp.Error != "" {
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("Response packet has error: %v\r\n", err), &outputPos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fileSizeBytes := resp.Info.Size
|
||||||
|
if fileSizeBytes == 0 {
|
||||||
|
writeStringToPty(ctx, cmd, "Source file does not exist or is empty - exiting\r\n", &outputPos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("Source File Size: %v\r\n", prettyPrintByteSize(fileSizeBytes)), &outputPos)
|
||||||
|
writePk := packet.MakeWriteFilePacket()
|
||||||
|
writePk.ReqId = uuid.New().String()
|
||||||
|
writePk.Path = destPath
|
||||||
|
destWriteIter, err := destMsh.WriteFile(ctx, writePk)
|
||||||
|
if err != nil {
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("Error starting file write: %v\r\n", err), &outputPos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer destWriteIter.Close()
|
||||||
|
_, err = checkForWriteReady(ctx, destWriteIter)
|
||||||
|
if err != nil {
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("Write ready packet error: %v\r\n", err), &outputPos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bytesWritten := int64(0)
|
||||||
|
lastFilePercentageInt := int(0)
|
||||||
|
fileTransferPercentage := float64(0)
|
||||||
|
writeStringToPty(ctx, cmd, "[", &outputPos)
|
||||||
|
for {
|
||||||
|
dataPkIf, err := sourceStreamIter.Next(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error in read-file while getting data: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if dataPkIf == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
dataPk, ok := dataPkIf.(*packet.FileDataPacketType)
|
||||||
|
if !ok {
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("error in read-file, invalid data packet type: %T\r\n", dataPkIf), &outputPos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if dataPk.Error != "" {
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("in read-file, data packet error: %s\r\n", dataPk.Error), &outputPos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeDataPk := packet.MakeFileDataPacket(writePk.ReqId)
|
||||||
|
writeDataPk.Eof = dataPk.Eof
|
||||||
|
writeDataPk.Error = dataPk.Error
|
||||||
|
writeDataPk.Type = dataPk.Type
|
||||||
|
writeDataPk.Data = make([]byte, int64(len(dataPk.Data)))
|
||||||
|
copy(writeDataPk.Data, dataPk.Data)
|
||||||
|
err = destMsh.SendFileData(writeDataPk)
|
||||||
|
if err != nil {
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("error sending file to dest: %v\r\n", err), &outputPos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bytesWritten += int64(len(dataPk.Data))
|
||||||
|
fileTransferPercentage = float64(bytesWritten) / float64(fileSizeBytes)
|
||||||
|
filePercentageInt := int(fileTransferPercentage * 100)
|
||||||
|
if filePercentageInt-lastFilePercentageInt > 5 {
|
||||||
|
statusBarString := getStatusBarString(filePercentageInt)
|
||||||
|
writeStringToPty(ctx, cmd, statusBarString, &outputPos)
|
||||||
|
lastFilePercentageInt = filePercentageInt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = checkForWriteFinished(ctx, destWriteIter)
|
||||||
|
if err != nil {
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("\r\nWrite finished packet error %v", err), &outputPos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeStringToPty(ctx, cmd, getStatusBarString(100), &outputPos)
|
||||||
|
writeStringToPty(ctx, cmd, " done. \r\n", &outputPos)
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("Finished transferring. Transferred %v bytes\r\n", bytesWritten), &outputPos)
|
||||||
|
exitSuccess = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func doCopyLocalFileToLocal(ctx context.Context, cmd *sstore.CmdType, sourcePath string, destPath string, outputPos int64) {
|
||||||
|
var exitSuccess bool
|
||||||
|
var bytesWritten int64
|
||||||
|
startTime := time.Now()
|
||||||
|
defer func() {
|
||||||
|
deferWriteCmdStatus(ctx, cmd, startTime, exitSuccess, outputPos)
|
||||||
|
}()
|
||||||
|
sourceFile, err := os.Open(sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("error opening source file %v", err), &outputPos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer sourceFile.Close()
|
||||||
|
sourceFileStat, err := sourceFile.Stat()
|
||||||
|
if err != nil {
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("error getting filestat %v", err), &outputPos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fileSizeBytes := sourceFileStat.Size()
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("Source File Size: %v\r\n", prettyPrintByteSize(fileSizeBytes)), &outputPos)
|
||||||
|
destFile, err := os.Create(destPath)
|
||||||
|
if err != nil {
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("error creating dest file %v", err), &outputPos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer destFile.Close()
|
||||||
|
bytesWritten, err = io.Copy(destFile, sourceFile)
|
||||||
|
if err != nil {
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("error copying files %v", err), &outputPos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("Finished transferring. Transferred %v bytes\r\n", bytesWritten), &outputPos)
|
||||||
|
exitSuccess = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func doCopyRemoteFileToLocal(ctx context.Context, cmd *sstore.CmdType, remote_msh *remote.MShellProc, sourcePath string, localPath string, outputPos int64) {
|
||||||
|
var exitSuccess bool
|
||||||
|
startTime := time.Now()
|
||||||
|
defer func() {
|
||||||
|
deferWriteCmdStatus(ctx, cmd, startTime, exitSuccess, outputPos)
|
||||||
|
}()
|
||||||
|
streamPk := packet.MakeStreamFilePacket()
|
||||||
|
streamPk.ReqId = uuid.New().String()
|
||||||
|
streamPk.Path = sourcePath
|
||||||
|
iter, err := remote_msh.StreamFile(ctx, streamPk)
|
||||||
|
if err != nil {
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("Error getting file data packet: %v\r\n", err), &outputPos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer iter.Close()
|
||||||
|
respIf, err := iter.Next(ctx)
|
||||||
|
if err != nil {
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("Error getting next packet: %v\r\n", err), &outputPos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, ok := respIf.(*packet.StreamFileResponseType)
|
||||||
|
if !ok {
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("Error in getting packet response: %v\r\n", err), &outputPos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if resp == nil || resp.Error != "" {
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("Response packet has error: %v\r\n", err), &outputPos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fileSizeBytes := resp.Info.Size
|
||||||
|
if fileSizeBytes == 0 {
|
||||||
|
writeStringToPty(ctx, cmd, "Source file doesn't exist or file is empty - exiting\r\n", &outputPos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("Source File Size: %s\r\n", prettyPrintByteSize(fileSizeBytes)), &outputPos)
|
||||||
|
localFile, err := os.Create(localPath)
|
||||||
|
if err != nil {
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("Error creating file on local %v\r\n", err), &outputPos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer localFile.Close()
|
||||||
|
bytesWritten := int64(0)
|
||||||
|
lastFileTransferPercentage := float64(0)
|
||||||
|
fileTransferPercentage := float64(0)
|
||||||
|
writeStringToPty(ctx, cmd, "[", &outputPos)
|
||||||
|
for {
|
||||||
|
dataPkIf, err := iter.Next(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error in read-file while getting data: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if dataPkIf == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
dataPk, ok := dataPkIf.(*packet.FileDataPacketType)
|
||||||
|
if !ok {
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("error in read-file, invalid data packet type: %T\r\n", dataPkIf), &outputPos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if dataPk.Error != "" {
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("in read-file, data packet error: %s", dataPk.Error), &outputPos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
localFile.Write(dataPk.Data)
|
||||||
|
bytesWritten += int64(len(dataPk.Data))
|
||||||
|
fileTransferPercentage = float64(bytesWritten) / float64(fileSizeBytes)
|
||||||
|
|
||||||
|
if fileTransferPercentage-lastFileTransferPercentage > float64(0.05) {
|
||||||
|
writeStringToPty(ctx, cmd, "-", &outputPos)
|
||||||
|
lastFileTransferPercentage = fileTransferPercentage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeStringToPty(ctx, cmd, "] done. \r\n", &outputPos)
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("Finished transferring. Transferred %v bytes\n", fileSizeBytes), &outputPos)
|
||||||
|
exitSuccess = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeStringToPty(ctx context.Context, cmd *sstore.CmdType, outputString string, outputPos *int64) {
|
||||||
|
outBytes := []byte(outputString)
|
||||||
|
update, err := sstore.AppendToCmdPtyBlob(ctx, cmd.ScreenId, cmd.LineId, outBytes, *outputPos)
|
||||||
|
*outputPos += int64(len(outBytes))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error writing to pty: %v", err)
|
||||||
|
}
|
||||||
|
sstore.MainBus.SendScreenUpdate(cmd.ScreenId, update)
|
||||||
|
err = sstore.SetStatusIndicatorLevel(ctx, cmd.ScreenId, sstore.StatusIndicatorLevel_Output, false)
|
||||||
|
if err != nil {
|
||||||
|
// This is not a fatal error, so just log it
|
||||||
|
log.Printf("error setting status indicator level to output in writeStringToPty: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCopyFileParam(info string) (remote string, path string, err error) {
|
||||||
|
stringsList := strings.Split(info, ":")
|
||||||
|
if len(stringsList) == 1 {
|
||||||
|
// use cur remote
|
||||||
|
return "", stringsList[0], nil
|
||||||
|
} else if len(stringsList) == 2 {
|
||||||
|
remote := strings.Trim(stringsList[0], "[] ")
|
||||||
|
return remote, stringsList[1], nil
|
||||||
|
} else {
|
||||||
|
return "error", "error", fmt.Errorf("malformed arguments")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CopyFileCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
||||||
|
if len(pk.Args) == 0 {
|
||||||
|
return nil, fmt.Errorf("usage: /copyfile [file to copy] local=[path to copy to on local]")
|
||||||
|
}
|
||||||
|
ids, err := resolveUiIds(ctx, pk, R_Screen|R_Session|R_RemoteConnected)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to resolve connected remote id: %v", err)
|
||||||
|
}
|
||||||
|
sourceInfo := pk.Args[0]
|
||||||
|
sourceRemote, sourcePath, err := parseCopyFileParam(sourceInfo)
|
||||||
|
var sourceRemoteId *ResolvedRemote
|
||||||
|
var destRemoteId *ResolvedRemote
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error: malformed arguments - usage: [remote]:path ")
|
||||||
|
} else if sourceRemote == "" {
|
||||||
|
// use cur remote
|
||||||
|
sourceRemote = ConnectedRemote
|
||||||
|
sourceRemoteId = ids.Remote
|
||||||
|
if ids.Remote.RemoteCopy.IsLocal() {
|
||||||
|
sourceRemote = LocalRemote
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pk.Kwargs["remote"] = sourceRemote
|
||||||
|
sourceIds, err := resolveUiIds(ctx, pk, R_Remote)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error resolving remote id %v", err)
|
||||||
|
}
|
||||||
|
sourceRemoteId = sourceIds.Remote
|
||||||
|
}
|
||||||
|
destInfo := pk.Args[1]
|
||||||
|
destRemote, destPath, err := parseCopyFileParam(destInfo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error: malformed arguments - usage: [remote]:path ")
|
||||||
|
} else if destRemote == "" {
|
||||||
|
destRemote = ConnectedRemote
|
||||||
|
destRemoteId = ids.Remote
|
||||||
|
if ids.Remote.RemoteCopy.IsLocal() {
|
||||||
|
destRemote = LocalRemote
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pk.Kwargs["remote"] = destRemote
|
||||||
|
destIds, err := resolveUiIds(ctx, pk, R_Remote)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error resolving remote id %v", err)
|
||||||
|
}
|
||||||
|
destRemoteId = destIds.Remote
|
||||||
|
}
|
||||||
|
if destPath == "" {
|
||||||
|
return nil, fmt.Errorf("error: malformed arguments - usage: [remote]:path ")
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceFullPath string
|
||||||
|
var destFullPath string
|
||||||
|
sourceMsh := sourceRemoteId.MShell
|
||||||
|
if sourceMsh == nil {
|
||||||
|
return nil, fmt.Errorf("failure getting source remote mshell")
|
||||||
|
}
|
||||||
|
sourceRRState := sourceMsh.GetRemoteRuntimeState()
|
||||||
|
sourcePathWithHome, err := sourceRRState.ExpandHomeDir(sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("expand home dir err: %v", err)
|
||||||
|
}
|
||||||
|
sourceFullPath = sourcePathWithHome
|
||||||
|
if (sourceRemote == ConnectedRemote || sourceRemote == LocalRemote) && !filepath.IsAbs(sourcePathWithHome) && sourceRemoteId.FeState != nil {
|
||||||
|
sourceCwd := sourceRemoteId.FeState["cwd"]
|
||||||
|
if sourceCwd != "" {
|
||||||
|
sourceFullPath = filepath.Join(sourceCwd, sourcePathWithHome)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if destPath[len(destPath)-1:] == "/" {
|
||||||
|
sourceFileName := filepath.Base(sourceFullPath)
|
||||||
|
destPath = filepath.Join(destPath, sourceFileName)
|
||||||
|
}
|
||||||
|
destMsh := destRemoteId.MShell
|
||||||
|
if destMsh == nil {
|
||||||
|
return nil, fmt.Errorf("failure getting dest remote mshell")
|
||||||
|
}
|
||||||
|
destRRState := destMsh.GetRemoteRuntimeState()
|
||||||
|
destPathWithHome, err := destRRState.ExpandHomeDir(destPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("expand home dir err: %v", err)
|
||||||
|
}
|
||||||
|
destFullPath = destPathWithHome
|
||||||
|
if (destRemote == ConnectedRemote || destRemote == LocalRemote) && !filepath.IsAbs(destPathWithHome) && destRemoteId.FeState != nil {
|
||||||
|
destCwd := destRemoteId.FeState["cwd"]
|
||||||
|
if destCwd != "" {
|
||||||
|
destFullPath = filepath.Join(destCwd, destPathWithHome)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var outputPos int64
|
||||||
|
outputStr := fmt.Sprintf("Copying [%v]:%v to [%v]:%v\r\n", sourceRemoteId.DisplayName, sourceFullPath, destRemoteId.DisplayName, destFullPath)
|
||||||
|
termopts := sstore.TermOpts{Rows: shellutil.DefaultTermRows, Cols: shellutil.DefaultTermCols, FlexRows: true, MaxPtySize: remote.DefaultMaxPtySize}
|
||||||
|
cmd, err := makeDynCmd(ctx, "copy file", ids, pk.GetRawStr(), termopts)
|
||||||
|
writeStringToPty(ctx, cmd, outputStr, &outputPos)
|
||||||
|
if err != nil {
|
||||||
|
// TODO tricky error since the command was a success, but we can't show the output
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
update, err := addLineForCmd(ctx, "/copy file", false, ids, cmd, "", nil)
|
||||||
|
if err != nil {
|
||||||
|
// TODO tricky error since the command was a success, but we can't show the output
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
update.Interactive = pk.Interactive
|
||||||
|
if destRemote != ConnectedRemote && destRemoteId != nil && !destRemoteId.RState.IsConnected() {
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("Attempting to autoconnect to remote %v\r\n", destRemote), &outputPos)
|
||||||
|
err = destRemoteId.MShell.TryAutoConnect()
|
||||||
|
if err != nil {
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("Couldn't connect to remote %v\r\n", sourceRemote), &outputPos)
|
||||||
|
} else {
|
||||||
|
writeStringToPty(ctx, cmd, "Auto connect successful\r\n", &outputPos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sourceRemote != LocalRemote && sourceRemoteId != nil && !sourceRemoteId.RState.IsConnected() {
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("Attempting to autoconnect to remote %v\r\n", sourceRemote), &outputPos)
|
||||||
|
err = sourceRemoteId.MShell.TryAutoConnect()
|
||||||
|
if err != nil {
|
||||||
|
writeStringToPty(ctx, cmd, fmt.Sprintf("Couldn't connect to remote %v\r\n", sourceRemote), &outputPos)
|
||||||
|
} else {
|
||||||
|
writeStringToPty(ctx, cmd, "Auto connect successful\r\n", &outputPos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sstore.MainBus.SendScreenUpdate(cmd.ScreenId, update)
|
||||||
|
update = &sstore.ModelUpdate{}
|
||||||
|
if destRemote == LocalRemote && sourceRemote == LocalRemote {
|
||||||
|
go doCopyLocalFileToLocal(context.Background(), cmd, sourceFullPath, destFullPath, outputPos)
|
||||||
|
} else if destRemote == LocalRemote && sourceRemote != LocalRemote {
|
||||||
|
go doCopyRemoteFileToLocal(context.Background(), cmd, sourceMsh, sourceFullPath, destFullPath, outputPos)
|
||||||
|
} else if destRemote != LocalRemote && sourceRemote == LocalRemote {
|
||||||
|
go doCopyLocalFileToRemote(context.Background(), cmd, destMsh, sourceFullPath, destFullPath, outputPos)
|
||||||
|
} else if destRemote != LocalRemote && sourceRemote != LocalRemote {
|
||||||
|
go doCopyRemoteFileToRemote(context.Background(), cmd, sourceMsh, destMsh, sourceFullPath, destFullPath, outputPos)
|
||||||
|
}
|
||||||
|
return update, nil
|
||||||
|
}
|
||||||
|
|
||||||
func RemoteInstallCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
func RemoteInstallCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
||||||
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Remote)
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Remote)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -2463,6 +3016,7 @@ func addLineForCmd(ctx context.Context, metaCmd string, shouldFocus bool, ids re
|
|||||||
Cmd: cmd,
|
Cmd: cmd,
|
||||||
Screens: []*sstore.ScreenType{screen},
|
Screens: []*sstore.ScreenType{screen},
|
||||||
}
|
}
|
||||||
|
sstore.IncrementNumRunningCmds_Update(update, cmd.ScreenId, 1)
|
||||||
updateHistoryContext(ctx, rtnLine, cmd, cmd.FeState)
|
updateHistoryContext(ctx, rtnLine, cmd, cmd.FeState)
|
||||||
return update, nil
|
return update, nil
|
||||||
}
|
}
|
||||||
@ -3422,6 +3976,7 @@ func LineRestartCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (
|
|||||||
NoCreateCmdPtyFile: true,
|
NoCreateCmdPtyFile: true,
|
||||||
}
|
}
|
||||||
cmd, callback, err := remote.RunCommand(ctx, rcOpts, runPacket)
|
cmd, callback, err := remote.RunCommand(ctx, rcOpts, runPacket)
|
||||||
|
sstore.IncrementNumRunningCmds(cmd.ScreenId, 1)
|
||||||
if callback != nil {
|
if callback != nil {
|
||||||
defer callback()
|
defer callback()
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,11 @@ const (
|
|||||||
R_RemoteConnected = 16
|
R_RemoteConnected = 16
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ConnectedRemote = "connected"
|
||||||
|
LocalRemote = "local"
|
||||||
|
)
|
||||||
|
|
||||||
type resolvedIds struct {
|
type resolvedIds struct {
|
||||||
SessionId string
|
SessionId string
|
||||||
ScreenId string
|
ScreenId string
|
||||||
|
@ -50,6 +50,7 @@ const RemoteTermRows = 8
|
|||||||
const RemoteTermCols = 80
|
const RemoteTermCols = 80
|
||||||
const PtyReadBufSize = 100
|
const PtyReadBufSize = 100
|
||||||
const RemoteConnectTimeout = 15 * time.Second
|
const RemoteConnectTimeout = 15 * time.Second
|
||||||
|
const RpcIterChannelSize = 100
|
||||||
|
|
||||||
var envVarsToStrip map[string]bool = map[string]bool{
|
var envVarsToStrip map[string]bool = map[string]bool{
|
||||||
"PROMPT": true,
|
"PROMPT": true,
|
||||||
@ -665,7 +666,12 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState {
|
|||||||
if vars["remoteuser"] == "root" || vars["sudo"] == "1" {
|
if vars["remoteuser"] == "root" || vars["sudo"] == "1" {
|
||||||
vars["isroot"] = "1"
|
vars["isroot"] = "1"
|
||||||
}
|
}
|
||||||
state.RemoteVars = vars
|
varsCopy := make(map[string]string)
|
||||||
|
// deep copy so that concurrent calls don't collide on this data
|
||||||
|
for key, value := range vars {
|
||||||
|
varsCopy[key] = value
|
||||||
|
}
|
||||||
|
state.RemoteVars = varsCopy
|
||||||
state.ActiveShells = msh.StateMap.GetShells()
|
state.ActiveShells = msh.StateMap.GetShells()
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
@ -1203,6 +1209,10 @@ func (msh *MShellProc) ReInit(ctx context.Context, shellType string) (*packet.Sh
|
|||||||
return ssPk, nil
|
return ssPk, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (msh *MShellProc) WriteFile(ctx context.Context, writePk *packet.WriteFilePacketType) (*packet.RpcResponseIter, error) {
|
||||||
|
return msh.PacketRpcIter(ctx, writePk)
|
||||||
|
}
|
||||||
|
|
||||||
func (msh *MShellProc) StreamFile(ctx context.Context, streamPk *packet.StreamFilePacketType) (*packet.RpcResponseIter, error) {
|
func (msh *MShellProc) StreamFile(ctx context.Context, streamPk *packet.StreamFilePacketType) (*packet.RpcResponseIter, error) {
|
||||||
return msh.PacketRpcIter(ctx, streamPk)
|
return msh.PacketRpcIter(ctx, streamPk)
|
||||||
}
|
}
|
||||||
@ -1886,7 +1896,6 @@ func RunCommand(ctx context.Context, rcOpts RunCommandOpts, runPacket *packet.Ru
|
|||||||
RunPacket: runPacket,
|
RunPacket: runPacket,
|
||||||
})
|
})
|
||||||
|
|
||||||
go pushNumRunningCmdsUpdate(&runPacket.CK, 1)
|
|
||||||
return cmd, func() { removeCmdWait(runPacket.CK) }, nil
|
return cmd, func() { removeCmdWait(runPacket.CK) }, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1925,7 +1934,7 @@ func (msh *MShellProc) PacketRpcIter(ctx context.Context, pk packet.RpcPacketTyp
|
|||||||
return nil, fmt.Errorf("PacketRpc passed nil packet")
|
return nil, fmt.Errorf("PacketRpc passed nil packet")
|
||||||
}
|
}
|
||||||
reqId := pk.GetReqId()
|
reqId := pk.GetReqId()
|
||||||
msh.ServerProc.Output.RegisterRpc(reqId)
|
msh.ServerProc.Output.RegisterRpcSz(reqId, RpcIterChannelSize)
|
||||||
err := msh.ServerProc.Input.SendPacketCtx(ctx, pk)
|
err := msh.ServerProc.Input.SendPacketCtx(ctx, pk)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -2064,8 +2073,6 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) {
|
|||||||
// fall-through (nothing to do)
|
// fall-through (nothing to do)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
go pushNumRunningCmdsUpdate(&donePk.CK, -1)
|
|
||||||
sstore.MainBus.SendUpdate(update)
|
sstore.MainBus.SendUpdate(update)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -950,6 +950,7 @@ func UpdateCmdDoneInfo(ctx context.Context, ck base.CommandKey, donePk *packet.C
|
|||||||
// This is not a fatal error, so just log it
|
// This is not a fatal error, so just log it
|
||||||
log.Printf("error setting status indicator level after done packet: %v\n", err)
|
log.Printf("error setting status indicator level after done packet: %v\n", err)
|
||||||
}
|
}
|
||||||
|
IncrementNumRunningCmds_Update(update, screenId, -1)
|
||||||
|
|
||||||
return update, nil
|
return update, nil
|
||||||
}
|
}
|
||||||
|
@ -1040,6 +1040,10 @@ type RemoteType struct {
|
|||||||
OpenAIOpts *OpenAIOptsType `json:"openaiopts,omitempty"`
|
OpenAIOpts *OpenAIOptsType `json:"openaiopts,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RemoteType) IsLocal() bool {
|
||||||
|
return r.Local && !r.IsSudo()
|
||||||
|
}
|
||||||
|
|
||||||
func (r *RemoteType) IsSudo() bool {
|
func (r *RemoteType) IsSudo() bool {
|
||||||
return r.SSHOpts != nil && r.SSHOpts.IsSudo
|
return r.SSHOpts != nil && r.SSHOpts.IsSudo
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user