Merge branch 'main' of github.com:wavetermdev/waveterm into fix-connections-modal

This commit is contained in:
Red Adaya 2024-02-10 09:20:21 +08:00
commit 818bcd5094
128 changed files with 10957 additions and 7023 deletions

View File

@ -10,7 +10,7 @@
# 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.
@ -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
* Searchable contextual command history across all remote sessions (saved locally)
* 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
@ -35,6 +36,7 @@ brew install --cask wave
* Homepage — https://www.waveterm.dev
* Download Page — https://www.waveterm.dev/download
* Documentation — https://docs.waveterm.dev/
* Blog — https://blog.waveterm.dev/
* Quick Start Guide — https://docs.waveterm.dev/quickstart/
* Discord Community — https://discord.gg/XfvZ334gwU

View File

@ -664,3 +664,161 @@ a.a-block {
}
}
}
.settings-field {
display: flex;
flex-direction: row;
align-items: center;
&.settings-field.sub-field {
.settings-label {
font-weight: normal;
text-align: right;
padding-right: 20px;
}
}
&.settings-error {
color: @term-red;
margin-top: 20px;
padding: 10px;
border-radius: 5px;
background-color: #200707;
border: 1px solid @term-red;
font-weight: bold;
.error-dismiss {
padding: 2px;
cursor: pointer;
}
}
.settings-label {
font-weight: bold;
width: 12em;
display: flex;
flex-direction: row;
align-items: center;
.info-message {
margin-left: 5px;
}
}
.settings-input {
display: flex;
flex-direction: row;
align-items: center;
color: @term-white;
&.settings-clickable {
cursor: pointer;
}
&.inline-edit.edit-active {
input.input {
padding: 0;
height: 20px;
}
.button {
height: 20px;
}
}
input {
padding: 4px;
border-radius: 3px;
}
.control {
.icon {
width: 1.5em;
height: 1.5em;
margin: 0;
}
}
.tab-color-icon.color-default path {
fill: @tab-green;
}
.tab-color-icon.color-green path {
fill: @tab-green;
}
.tab-color-icon.color-orange path {
fill: @tab-orange;
}
.tab-color-icon.color-red path {
fill: @tab-red;
}
.tab-color-icon.color-yellow path {
fill: @tab-yellow;
}
.tab-color-icon.color-blue path {
fill: @tab-blue;
}
.tab-color-icon.color-mint path {
fill: @tab-mint;
}
.tab-color-icon.color-cyan path {
fill: @tab-cyan;
}
.tab-color-icon.color-white path {
fill: @tab-white;
}
.tab-color-icon.color-violet path {
fill: @tab-violet;
}
.tab-color-icon.color-pink path {
fill: @tab-pink;
}
.tab-colors,
.tab-icons {
display: flex;
flex-direction: row;
align-items: center;
.tab-color-sep,
.tab-icon-sep {
padding-left: 10px;
padding-right: 10px;
font-weight: bold;
}
.tab-color-icon,
.tab-icon-icon {
width: 1.1em;
vertical-align: middle;
}
.tab-color-name,
.tab-icon-name {
margin-left: 1em;
}
.tab-color-select,
.tab-icon-select {
cursor: pointer;
margin: 5px;
&:hover {
outline: 2px solid white;
}
}
}
.action-text {
margin-left: 20px;
color: @term-red;
}
.settings-share-link {
width: 160px;
}
}
&:not(:first-child) {
margin-top: 10px;
}
}

View File

@ -9,7 +9,7 @@ import { If } from "tsx-control-statements/components";
import dayjs from "dayjs";
import type { ContextMenuOpts } from "../types/types";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel } from "../model/model";
import { GlobalModel } from "../models";
import { isBlank } from "../util/util";
import { WorkspaceView } from "./workspace/workspaceview";
import { PluginsView } from "./pluginsview/pluginsview";
@ -32,7 +32,7 @@ class App extends React.Component<{}, {}> {
dcWait: OV<boolean> = mobx.observable.box(false, { name: "dcWait" });
mainContentRef: React.RefObject<HTMLDivElement> = React.createRef();
constructor(props: any) {
constructor(props: {}) {
super(props);
if (GlobalModel.isDev) document.body.className = "is-dev";
}

View File

@ -8,6 +8,7 @@ export const SESSION_SETTINGS = "sessionSettings";
export const LINE_SETTINGS = "lineSettings";
export const CLIENT_SETTINGS = "clientSettings";
export const TAB_SWITCHER = "tabSwitcher";
export const USER_INPUT = "userInput";
export const LineContainer_Main = "main";
export const LineContainer_History = "history";
@ -16,3 +17,33 @@ export const LineContainer_Sidebar = "sidebar";
export const ConfirmKey_HideShellPrompt = "hideshellprompt";
export const NoStrPos = -1;
export const RemotePtyRows = 8; // also in main.tsx
export const RemotePtyCols = 80;
export const ProdServerEndpoint = "http://127.0.0.1:1619";
export const ProdServerWsEndpoint = "ws://127.0.0.1:1623";
export const DevServerEndpoint = "http://127.0.0.1:8090";
export const DevServerWsEndpoint = "ws://127.0.0.1:8091";
export const DefaultTermFontSize = 12;
export const MinFontSize = 8;
export const MaxFontSize = 24;
export const InputChunkSize = 500;
export const RemoteColors = ["red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"];
export const TabColors = ["red", "orange", "yellow", "green", "mint", "cyan", "blue", "violet", "pink", "white"];
export const TabIcons = [
"sparkle",
"fire",
"ghost",
"cloud",
"compass",
"crown",
"droplet",
"graduation-cap",
"heart",
"file",
];
// @ts-ignore
export const VERSION = __WAVETERM_VERSION__;
// @ts-ignore
export const BUILD = __WAVETERM_BUILD__;

View File

@ -8,8 +8,8 @@ import { boundMethod } from "autobind-decorator";
import { If, For } from "tsx-control-statements/components";
import cn from "classnames";
import type { BookmarkType } from "../../types/types";
import { GlobalModel } from "../../model/model";
import { CmdStrCode, Markdown } from "../common/common";
import { GlobalModel } from "../../models";
import { CmdStrCode, Markdown } from "../common/elements";
import { ReactComponent as XmarkIcon } from "../assets/icons/line/xmark.svg";
import { ReactComponent as CopyIcon } from "../assets/icons/favourites/copy.svg";
@ -19,8 +19,16 @@ import { ReactComponent as FavoritesIcon } from "../assets/icons/favourites.svg"
import "./bookmarks.less";
type BookmarkProps = {
bookmark: BookmarkType;
};
@mobxReact.observer
class Bookmark extends React.Component<{ bookmark: BookmarkType }, {}> {
class Bookmark extends React.Component<BookmarkProps, {}> {
constructor(props: BookmarkProps) {
super(props);
}
@boundMethod
handleDeleteClick(): void {
let { bookmark } = this.props;
@ -179,6 +187,10 @@ class Bookmark extends React.Component<{ bookmark: BookmarkType }, {}> {
@mobxReact.observer
class BookmarksView extends React.Component<{}, {}> {
constructor(props: {}) {
super(props);
}
@boundMethod
closeView(): void {
GlobalModel.bookmarksModel.closeView();

View File

@ -6,24 +6,20 @@ import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { GlobalModel, GlobalCommandRunner, MinFontSize, MaxFontSize, RemotesModel } from "../../model/model";
import { Toggle, InlineSettingsTextEdit, SettingsError, Dropdown } from "../common/common";
import { CommandRtnType, ClientDataType } from "../../types/types";
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../models";
import { Toggle, InlineSettingsTextEdit, SettingsError, Dropdown } from "../common/elements";
import * as types from "../../types/types";
import { commandRtnHandler, isBlank } from "../../util/util";
import * as appconst from "../appconst";
import "./clientsettings.less";
type OV<V> = mobx.IObservableValue<V>;
// @ts-ignore
const VERSION = __WAVETERM_VERSION__;
// @ts-ignore
const BUILD = __WAVETERM_BUILD__;
@mobxReact.observer
class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hoveredItemId: string }> {
fontSizeDropdownActive: OV<boolean> = mobx.observable.box(false, { name: "clientSettings-fontSizeDropdownActive" });
errorMessage: OV<string> = mobx.observable.box(null, { name: "ClientSettings-errorMessage" });
fontSizeDropdownActive: types.OV<boolean> = mobx.observable.box(false, {
name: "clientSettings-fontSizeDropdownActive",
});
errorMessage: types.OV<string> = mobx.observable.box(null, { name: "ClientSettings-errorMessage" });
@boundMethod
dismissError(): void {
@ -52,7 +48,7 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
@boundMethod
handleChangeTelemetry(val: boolean): void {
let prtn: Promise<CommandRtnType> = null;
let prtn: Promise<types.CommandRtnType> = null;
if (val) {
prtn = GlobalCommandRunner.telemetryOn(false);
} else {
@ -63,7 +59,7 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
@boundMethod
handleChangeReleaseCheck(val: boolean): void {
let prtn: Promise<CommandRtnType> = null;
let prtn: Promise<types.CommandRtnType> = null;
if (val) {
prtn = GlobalCommandRunner.releaseCheckAutoOn(false);
} else {
@ -74,7 +70,7 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
getFontSizes(): any {
let availableFontSizes: { label: string; value: number }[] = [];
for (let s = MinFontSize; s <= MaxFontSize; s++) {
for (let s = appconst.MinFontSize; s <= appconst.MaxFontSize; s++) {
availableFontSizes.push({ label: s + "px", value: s });
}
return availableFontSizes;
@ -116,7 +112,7 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
return null;
}
let cdata: ClientDataType = GlobalModel.clientData.get();
let cdata: types.ClientDataType = GlobalModel.clientData.get();
let openAIOpts = cdata.openaiopts ?? {};
let apiTokenStr = isBlank(openAIOpts.apitoken) ? "(not set)" : "********";
let maxTokensStr = String(
@ -151,7 +147,7 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
<div className="settings-field">
<div className="settings-label">Client Version</div>
<div className="settings-input">
{VERSION} {BUILD}
{appconst.VERSION} {appconst.BUILD}
</div>
</div>
<div className="settings-field">

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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;
}
}

View 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 };

View 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;
}
}
}

View 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 };

View 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;
}

View 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 };

View 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>&#x2318;{text}</span>;
}
export { renderCmdText };

View 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;
}

View 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 };

View 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 };

View 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";

View 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;
}
}
}

View 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 };

View 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;
}

View 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 };

View 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 };

View 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;
}

View 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 "../../../models";
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 };

View 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;
}
}
}
}

View 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 };

View 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 };

View 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;
}
}

View 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 };

View File

@ -0,0 +1,9 @@
@import "../../../app/common/themes/themes.less";
.sidebar-handle {
position: absolute;
top: 0;
bottom: 0;
width: 5px;
cursor: col-resize;
}

View 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 "../../../models";
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 };

View 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 };

View File

@ -0,0 +1,28 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { GlobalModel } from "../../../models";
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 };

View 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;
}
}

View 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 };

View 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;
}
}
}

View 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 };

View 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);
}
}

View 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 };

View 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;
}
}

View 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 };

View File

@ -5,18 +5,14 @@ import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { GlobalModel } from "../../../model/model";
import { Modal, LinkButton } from "../common";
import { GlobalModel } from "../../../models";
import { Modal, LinkButton } from "../elements";
import * as util from "../../../util/util";
import * as appconst from "../../appconst";
import logo from "../../assets/waveterm-logo-with-bg.svg";
import "./about.less";
// @ts-ignore
const VERSION = __WAVETERM_VERSION__;
// @ts-ignore
let BUILD = __WAVETERM_BUILD__;
@mobxReact.observer
class AboutModal extends React.Component<{}, {}> {
@boundMethod
@ -42,7 +38,7 @@ class AboutModal extends React.Component<{}, {}> {
return (
<div className="status updated">
<div className="text-selectable">
Client Version {VERSION} ({BUILD})
Client Version {appconst.VERSION} ({appconst.BUILD})
</div>
</div>
);
@ -55,7 +51,7 @@ class AboutModal extends React.Component<{}, {}> {
<span>Up to Date</span>
</div>
<div className="selectable">
Client Version {VERSION} ({BUILD})
Client Version {appconst.VERSION} ({appconst.BUILD})
</div>
</div>
);
@ -67,7 +63,7 @@ class AboutModal extends React.Component<{}, {}> {
<span>Outdated Version</span>
</div>
<div className="selectable">
Client Version {VERSION} ({BUILD})
Client Version {appconst.VERSION} ({appconst.BUILD})
</div>
<div>
<button onClick={this.updateApp} className="button color-green text-secondary">

View File

@ -5,8 +5,8 @@ import * as React from "react";
import * as mobxReact from "mobx-react";
import { boundMethod } from "autobind-decorator";
import { If } from "tsx-control-statements/components";
import { Markdown, Modal, Button, Checkbox } from "../common";
import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
import { Markdown, Modal, Button, Checkbox } from "../elements";
import { GlobalModel, GlobalCommandRunner } from "../../../models";
import "./alert.less";

View File

@ -5,8 +5,8 @@ import * as React from "react";
import * as mobxReact from "mobx-react";
import { boundMethod } from "autobind-decorator";
import { If } from "tsx-control-statements/components";
import { GlobalModel } from "../../../model/model";
import { Modal, Button } from "../common";
import { GlobalModel } from "../../../models";
import { Modal, Button } from "../elements";
import "./clientstop.less";

View File

@ -6,11 +6,19 @@ import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { If } from "tsx-control-statements/components";
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../model/model";
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../models";
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 appconst from "../../appconst";
import "./createremoteconn.less";

View File

@ -5,8 +5,8 @@ import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { GlobalModel } from "../../../model/model";
import { Modal, Button } from "../common";
import { GlobalModel } from "../../../models";
import { Modal, Button } from "../elements";
import "./disconnected.less";

View File

@ -6,9 +6,9 @@ import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { If } from "tsx-control-statements/components";
import { boundMethod } from "autobind-decorator";
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../model/model";
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../models";
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 "./editremoteconn.less";

View File

@ -6,3 +6,7 @@ export { CreateRemoteConnModal } from "./createremoteconn";
export { ViewRemoteConnDetailModal } from "./viewremoteconndetail";
export { EditRemoteConnModal } from "./editremoteconn";
export { TabSwitcherModal } from "./tabswitcher";
export { SessionSettingsModal } from "./sessionsettings";
export { ScreenSettingsModal } from "./screensettings";
export { LineSettingsModal } from "./linesettings";
export { UserInputModal } from "./userinput";

View File

@ -0,0 +1,23 @@
@import "../../../app/common/themes/themes.less";
.line-settings-modal {
width: 640px;
.wave-modal-content {
gap: 24px;
.wave-modal-body {
display: flex;
padding: 0px 20px;
flex-direction: column;
align-items: flex-start;
gap: 4px;
align-self: stretch;
width: 100%;
.settings-input .hotkey {
color: @text-secondary;
}
}
}
}

View File

@ -0,0 +1,151 @@
// 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 { GlobalModel, GlobalCommandRunner } from "../../../models";
import { SettingsError, Modal, Dropdown } from "../elements";
import { LineType, RendererPluginType } from "../../../types/types";
import { PluginModel } from "../../../plugins/plugins";
import { commandRtnHandler } from "../../../util/util";
import "./linesettings.less";
type OV<V> = mobx.IObservableValue<V>;
@mobxReact.observer
class LineSettingsModal extends React.Component<{}, {}> {
rendererDropdownActive: OV<boolean> = mobx.observable.box(false, { name: "lineSettings-rendererDropdownActive" });
errorMessage: OV<string> = mobx.observable.box(null, { name: "ScreenSettings-errorMessage" });
linenum: number;
constructor(props: any) {
super(props);
this.linenum = GlobalModel.lineSettingsModal.get();
if (this.linenum == null) {
return;
}
}
@boundMethod
closeModal(): void {
mobx.action(() => {
GlobalModel.lineSettingsModal.set(null);
})();
GlobalModel.modalsModel.popModal();
}
@boundMethod
handleChangeArchived(val: boolean): void {
let line = this.getLine();
if (line == null) {
return;
}
let prtn = GlobalCommandRunner.lineArchive(line.lineid, val);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
toggleRendererDropdown(): void {
mobx.action(() => {
this.rendererDropdownActive.set(!this.rendererDropdownActive.get());
})();
}
getLine(): LineType {
let screen = GlobalModel.getActiveScreen();
if (screen == null) {
return;
}
return screen.getLineByNum(this.linenum);
}
@boundMethod
clickSetRenderer(renderer: string): void {
let line = this.getLine();
if (line == null) {
return;
}
let prtn = GlobalCommandRunner.lineSet(line.lineid, { renderer: renderer });
commandRtnHandler(prtn, this.errorMessage);
mobx.action(() => {
this.rendererDropdownActive.set(false);
})();
}
getOptions(plugins: RendererPluginType[]) {
// Add label and value to each object in the array
const options = plugins.map((item) => ({
...item,
label: item.name,
value: item.name,
}));
// Create an additional object with label "terminal" and value null
const terminalItem = {
label: "terminal",
value: null,
name: null,
rendererType: null,
heightType: null,
dataType: null,
collapseType: null,
globalCss: null,
mimeTypes: null,
};
// Create an additional object with label "none" and value none
const noneItem = {
label: "none",
value: "none",
name: null,
rendererType: null,
heightType: null,
dataType: null,
collapseType: null,
globalCss: null,
mimeTypes: null,
};
// Combine the options with the terminal item
return [terminalItem, ...options, noneItem];
}
render() {
let line = this.getLine();
if (line == null) {
setTimeout(() => {
this.closeModal();
}, 0);
return null;
}
let plugins = PluginModel.rendererPlugins;
let renderer = line.renderer ?? "terminal";
return (
<Modal className="line-settings-modal">
<Modal.Header onClose={this.closeModal} title={`line settings (${line.linenum})`} />
<div className="wave-modal-body">
<div className="settings-field">
<div className="settings-label">Renderer</div>
<div className="settings-input">
<Dropdown
className="renderer-dropdown"
options={this.getOptions(plugins)}
defaultValue={renderer}
onChange={this.clickSetRenderer}
/>
</div>
</div>
<SettingsError errorMessage={this.errorMessage} />
<div style={{ height: 50 }} />
</div>
<Modal.Footer cancelLabel="Close" onCancel={this.closeModal} />
</Modal>
);
}
}
export { LineSettingsModal };

View File

@ -3,7 +3,7 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import { GlobalModel } from "../../../model/model";
import { GlobalModel } from "../../../models";
import { TosModal } from "./tos";
@mobxReact.observer
@ -17,7 +17,7 @@ class ModalsProvider extends React.Component {
for (let i = 0; i < store.length; i++) {
let entry = store[i];
let Comp = entry.component;
rtn.push(<Comp key={entry.uniqueKey} />);
rtn.push(<Comp key={entry.uniqueKey} {...entry.props} />);
}
return <>{rtn}</>;
}

View File

@ -9,20 +9,24 @@ import {
ViewRemoteConnDetailModal,
EditRemoteConnModal,
TabSwitcherModal,
SessionSettingsModal,
ScreenSettingsModal,
LineSettingsModal,
UserInputModal,
} from "../modals";
import { ScreenSettingsModal, SessionSettingsModal, LineSettingsModal } from "./settings";
import * as constants from "../../appconst";
const modalsRegistry: { [key: string]: () => React.ReactElement } = {
[constants.ABOUT]: () => <AboutModal />,
[constants.CREATE_REMOTE]: () => <CreateRemoteConnModal />,
[constants.VIEW_REMOTE]: () => <ViewRemoteConnDetailModal />,
[constants.EDIT_REMOTE]: () => <EditRemoteConnModal />,
[constants.ALERT]: () => <AlertModal />,
[constants.SCREEN_SETTINGS]: () => <ScreenSettingsModal />,
[constants.SESSION_SETTINGS]: () => <SessionSettingsModal />,
[constants.LINE_SETTINGS]: () => <LineSettingsModal />,
[constants.TAB_SWITCHER]: () => <TabSwitcherModal />,
const modalsRegistry: { [key: string]: React.ComponentType } = {
[constants.ABOUT]: AboutModal,
[constants.CREATE_REMOTE]: CreateRemoteConnModal,
[constants.VIEW_REMOTE]: ViewRemoteConnDetailModal,
[constants.EDIT_REMOTE]: EditRemoteConnModal,
[constants.ALERT]: AlertModal,
[constants.SCREEN_SETTINGS]: ScreenSettingsModal,
[constants.SESSION_SETTINGS]: SessionSettingsModal,
[constants.LINE_SETTINGS]: LineSettingsModal,
[constants.TAB_SWITCHER]: TabSwitcherModal,
[constants.USER_INPUT]: UserInputModal,
};
export { modalsRegistry };

View File

@ -0,0 +1,52 @@
@import "../../../app/common/themes/themes.less";
.screen-settings-modal {
width: 640px;
.wave-modal-content {
gap: 24px;
.wave-modal-body {
display: flex;
padding: 0px 20px;
flex-direction: column;
align-items: flex-start;
gap: 4px;
align-self: stretch;
width: 100%;
.screen-settings-dropdown {
width: 412px;
.lefticon {
position: absolute;
top: 50%;
left: 16px;
transform: translateY(-50%);
.globe-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
}
.status-icon {
position: absolute;
left: 7px;
top: 8px;
}
}
}
.archived-label,
.actions-label {
div:first-child {
margin-right: 5px;
}
div:last-child i {
font-size: 13px;
}
}
}
}
}

View File

@ -0,0 +1,366 @@
// 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 { If, For } from "tsx-control-statements/components";
import cn from "classnames";
import { GlobalModel, GlobalCommandRunner, Screen } from "../../../models";
import { Toggle, InlineSettingsTextEdit, SettingsError, Modal, Dropdown, Tooltip } from "../elements";
import { RemoteType } from "../../../types/types";
import * as util from "../../../util/util";
import { commandRtnHandler } from "../../../util/util";
import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg";
import { ReactComponent as GlobeIcon } from "../../assets/icons/globe.svg";
import { ReactComponent as StatusCircleIcon } from "../../assets/icons/statuscircle.svg";
import * as appconst from "../../appconst";
import "./screensettings.less";
type OV<V> = mobx.IObservableValue<V>;
const ScreenDeleteMessage = `
Are you sure you want to delete this tab?
All commands and output will be deleted. To hide the tab, and retain the commands and output, use 'archive'.
`.trim();
const WebShareConfirmMarkdown = `
You are about to share a terminal tab on the web. Please make sure that you do
NOT share any private information, keys, passwords, or other sensitive information.
You are responsible for what you are sharing, be smart.
`.trim();
const WebStopShareConfirmMarkdown = `
Are you sure you want to stop web-sharing this tab?
`.trim();
@mobxReact.observer
class ScreenSettingsModal extends React.Component<{}, {}> {
shareCopied: OV<boolean> = mobx.observable.box(false, { name: "ScreenSettings-shareCopied" });
errorMessage: OV<string> = mobx.observable.box(null, { name: "ScreenSettings-errorMessage" });
screen: Screen;
sessionId: string;
screenId: string;
remotes: RemoteType[];
constructor(props) {
super(props);
let screenSettingsModal = GlobalModel.screenSettingsModal.get();
let { sessionId, screenId } = screenSettingsModal;
this.sessionId = sessionId;
this.screenId = screenId;
this.screen = GlobalModel.getScreenById(sessionId, screenId);
if (this.screen == null || sessionId == null || screenId == null) {
return;
}
this.remotes = GlobalModel.remotes;
}
@boundMethod
getOptions(): { label: string; value: string }[] {
return this.remotes
.filter((r) => !r.archived)
.map((remote) => ({
...remote,
label:
remote.remotealias && !util.isBlank(remote.remotealias)
? `${remote.remotecanonicalname}`
: remote.remotecanonicalname,
value: remote.remotecanonicalname,
}))
.sort((a, b) => {
let connValA = util.getRemoteConnVal(a);
let connValB = util.getRemoteConnVal(b);
if (connValA !== connValB) {
return connValA - connValB;
}
return a.remoteidx - b.remoteidx;
});
}
@boundMethod
closeModal(): void {
mobx.action(() => {
GlobalModel.screenSettingsModal.set(null);
})();
GlobalModel.modalsModel.popModal();
}
@boundMethod
selectTabColor(color: string): void {
if (this.screen == null) {
return;
}
if (this.screen.getTabColor() == color) {
return;
}
let prtn = GlobalCommandRunner.screenSetSettings(this.screenId, { tabcolor: color }, false);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
selectTabIcon(icon: string): void {
if (this.screen.getTabIcon() == icon) {
return;
}
let prtn = GlobalCommandRunner.screenSetSettings(this.screen.screenId, { tabicon: icon }, false);
util.commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
handleChangeArchived(val: boolean): void {
if (this.screen == null) {
return;
}
if (this.screen.archived.get() == val) {
return;
}
let prtn = GlobalCommandRunner.screenArchive(this.screenId, val);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
handleChangeWebShare(val: boolean): void {
if (this.screen == null) {
return;
}
if (this.screen.isWebShared() == val) {
return;
}
let message = val ? WebShareConfirmMarkdown : WebStopShareConfirmMarkdown;
let alertRtn = GlobalModel.showAlert({ message: message, confirm: true, markdown: true });
alertRtn.then((result) => {
if (!result) {
return;
}
let prtn = GlobalCommandRunner.screenWebShare(this.screen.screenId, val);
commandRtnHandler(prtn, this.errorMessage);
});
}
@boundMethod
copyShareLink(): void {
if (this.screen == null) {
return null;
}
let shareLink = this.screen.getWebShareUrl();
if (shareLink == null) {
return;
}
navigator.clipboard.writeText(shareLink);
mobx.action(() => {
this.shareCopied.set(true);
})();
setTimeout(() => {
mobx.action(() => {
this.shareCopied.set(false);
})();
}, 600);
}
@boundMethod
inlineUpdateName(val: string): void {
if (this.screen == null) {
return;
}
if (util.isStrEq(val, this.screen.name.get())) {
return;
}
let prtn = GlobalCommandRunner.screenSetSettings(this.screenId, { name: val }, false);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
inlineUpdateShareName(val: string): void {
if (this.screen == null) {
return;
}
if (util.isStrEq(val, this.screen.getShareName())) {
return;
}
let prtn = GlobalCommandRunner.screenSetSettings(this.screenId, { sharename: val }, false);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
dismissError(): void {
mobx.action(() => {
this.errorMessage.set(null);
})();
}
@boundMethod
handleDeleteScreen(): void {
if (this.screen == null) {
return;
}
if (this.screen.getScreenLines().lines.length == 0) {
GlobalCommandRunner.screenDelete(this.screenId, false);
GlobalModel.modalsModel.popModal();
return;
}
let message = ScreenDeleteMessage;
let alertRtn = GlobalModel.showAlert({ message: message, confirm: true, markdown: true });
alertRtn.then((result) => {
if (!result) {
return;
}
let prtn = GlobalCommandRunner.screenDelete(this.screenId, false);
commandRtnHandler(prtn, this.errorMessage);
GlobalModel.modalsModel.popModal();
});
}
@boundMethod
selectRemote(cname: string): void {
let prtn = GlobalCommandRunner.screenSetRemote(cname, true, false);
util.commandRtnHandler(prtn, this.errorMessage);
}
render() {
let screen = this.screen;
if (screen == null) {
return null;
}
let color: string = null;
let icon: string = null;
let index: number = 0;
let curRemote = GlobalModel.getRemote(GlobalModel.getActiveScreen().getCurRemoteInstance().remoteid);
return (
<Modal className="screen-settings-modal">
<Modal.Header onClose={this.closeModal} title={`tab settings (${screen.name.get()})`} />
<div className="wave-modal-body">
<div className="settings-field">
<div className="settings-label">Tab Id</div>
<div className="settings-input">{screen.screenId}</div>
</div>
<div className="settings-field">
<div className="settings-label">Name</div>
<div className="settings-input">
<InlineSettingsTextEdit
placeholder="name"
text={screen.name.get() ?? "(none)"}
value={screen.name.get() ?? ""}
onChange={this.inlineUpdateName}
maxLength={50}
showIcon={true}
/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">Connection</div>
<div className="settings-input">
<Dropdown
className="screen-settings-dropdown"
label={curRemote.remotealias}
options={this.getOptions()}
defaultValue={curRemote.remotecanonicalname}
onChange={this.selectRemote}
decoration={{
startDecoration: (
<div className="lefticon">
<GlobeIcon className="globe-icon" />
<StatusCircleIcon
className={cn("status-icon", "status-" + curRemote.status)}
/>
</div>
),
}}
/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">Tab Color</div>
<div className="settings-input">
<div className="tab-colors">
<div className="tab-color-cur">
<SquareIcon className={cn("tab-color-icon", "color-" + screen.getTabColor())} />
<span className="tab-color-name">{screen.getTabColor()}</span>
</div>
<div className="tab-color-sep">|</div>
<For each="color" of={appconst.TabColors}>
<div
key={color}
className="tab-color-select"
onClick={() => this.selectTabColor(color)}
>
<SquareIcon className={cn("tab-color-icon", "color-" + color)} />
</div>
</For>
</div>
</div>
</div>
<div className="settings-field">
<div className="settings-label">Tab Icon</div>
<div className="settings-input">
<div className="tab-icons">
<div className="tab-icon-cur">
<If condition={screen.getTabIcon() == "default"}>
<SquareIcon className={cn("tab-color-icon", "color-white")} />
</If>
<If condition={screen.getTabIcon() != "default"}>
<i className={`fa-sharp fa-solid fa-${screen.getTabIcon()}`}></i>
</If>
<span className="tab-icon-name">{screen.getTabIcon()}</span>
</div>
<div className="tab-icon-sep">|</div>
<For each="icon" index="index" of={appconst.TabIcons}>
<div
key={`${color}-${index}`}
className="tab-icon-select"
onClick={() => this.selectTabIcon(icon)}
>
<i className={`fa-sharp fa-solid fa-${icon}`}></i>
</div>
</For>
</div>
</div>
</div>
<div className="settings-field">
<div className="settings-label archived-label">
<div className="">Archived</div>
<Tooltip
message={`Archive will hide the tab. Commands and output will be retained in history.`}
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
className="screen-settings-tooltip"
>
<i className="fa-sharp fa-regular fa-circle-question" />
</Tooltip>
</div>
<div className="settings-input">
<Toggle checked={screen.archived.get()} onChange={this.handleChangeArchived} />
</div>
</div>
<div className="settings-field">
<div className="settings-label actions-label">
<div>Actions</div>
<Tooltip
message={`Delete will remove the tab, removing all commands and output from history.`}
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
className="screen-settings-tooltip"
>
<i className="fa-sharp fa-regular fa-circle-question" />
</Tooltip>
</div>
<div className="settings-input">
<div
onClick={this.handleDeleteScreen}
className="button is-prompt-danger is-outlined is-small"
>
Delete Tab
</div>
</div>
</div>
<SettingsError errorMessage={this.errorMessage} />
</div>
<Modal.Footer cancelLabel="Close" onCancel={this.closeModal} />
</Modal>
);
}
}
export { ScreenSettingsModal };

View File

@ -0,0 +1,28 @@
@import "../../../app/common/themes/themes.less";
.session-settings-modal {
width: 640px;
.wave-modal-content {
gap: 24px;
.wave-modal-body {
display: flex;
padding: 0px 20px;
flex-direction: column;
align-items: flex-start;
gap: 4px;
align-self: stretch;
width: 100%;
.settings-label > div:first-child {
margin-right: 5px;
}
}
}
}
.session-settings-tooltip i {
font-size: 12px;
margin-left: 0.5px;
}

View File

@ -0,0 +1,155 @@
// 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 { GlobalModel, GlobalCommandRunner, Session } from "../../../models";
import { Toggle, InlineSettingsTextEdit, SettingsError, Modal, Tooltip } from "../elements";
import * as util from "../../../util/util";
import { commandRtnHandler } from "../../../util/util";
import "./sessionsettings.less";
type OV<V> = mobx.IObservableValue<V>;
const SessionDeleteMessage = `
Are you sure you want to delete this workspace?
All commands and output will be deleted. To hide the workspace, and retain the commands and output, use 'archive'.
`.trim();
@mobxReact.observer
class SessionSettingsModal extends React.Component<{}, {}> {
errorMessage: OV<string> = mobx.observable.box(null, { name: "ScreenSettings-errorMessage" });
session: Session;
sessionId: string;
constructor(props: any) {
super(props);
this.sessionId = GlobalModel.sessionSettingsModal.get();
this.session = GlobalModel.getSessionById(this.sessionId);
if (this.session == null) {
return;
}
}
@boundMethod
closeModal(): void {
mobx.action(() => {
GlobalModel.sessionSettingsModal.set(null);
})();
GlobalModel.modalsModel.popModal();
}
@boundMethod
handleInlineChangeName(newVal: string): void {
if (this.session == null) {
return;
}
if (util.isStrEq(newVal, this.session.name.get())) {
return;
}
let prtn = GlobalCommandRunner.sessionSetSettings(this.sessionId, { name: newVal }, false);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
handleChangeArchived(val: boolean): void {
if (this.session == null) {
return;
}
if (this.session.archived.get() == val) {
return;
}
let prtn = GlobalCommandRunner.sessionArchive(this.sessionId, val);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
handleDeleteSession(): void {
let message = SessionDeleteMessage;
let alertRtn = GlobalModel.showAlert({ message: message, confirm: true, markdown: true });
alertRtn.then((result) => {
if (!result) {
return;
}
let prtn = GlobalCommandRunner.sessionDelete(this.sessionId);
commandRtnHandler(prtn, this.errorMessage, () => GlobalModel.modalsModel.popModal());
});
}
@boundMethod
dismissError(): void {
mobx.action(() => {
this.errorMessage.set(null);
})();
}
render() {
if (this.session == null) {
return null;
}
return (
<Modal className="session-settings-modal">
<Modal.Header onClose={this.closeModal} title={`workspace settings (${this.session.name.get()})`} />
<div className="wave-modal-body">
<div className="settings-field">
<div className="settings-label">Name</div>
<div className="settings-input">
<InlineSettingsTextEdit
placeholder="name"
text={this.session.name.get() ?? "(none)"}
value={this.session.name.get() ?? ""}
onChange={this.handleInlineChangeName}
maxLength={50}
showIcon={true}
/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">
<div>Archived</div>
<Tooltip
className="session-settings-tooltip"
message="Archive will hide the workspace from the active menu. Commands and output will be
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 className="settings-input">
<Toggle checked={this.session.archived.get()} onChange={this.handleChangeArchived} />
</div>
</div>
<div className="settings-field">
<div className="settings-label">
<div>Actions</div>
<Tooltip
className="session-settings-tooltip"
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 className="settings-input">
<div
onClick={this.handleDeleteSession}
className="button is-prompt-danger is-outlined is-small"
>
Delete Workspace
</div>
</div>
</div>
<SettingsError errorMessage={this.errorMessage} />
</div>
<Modal.Footer cancelLabel="Close" onCancel={this.closeModal} />
</Modal>
);
}
}
export { SessionSettingsModal };

File diff suppressed because it is too large Load Diff

View File

@ -1,838 +0,0 @@
// 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 { If, For } from "tsx-control-statements/components";
import cn from "classnames";
import {
GlobalModel,
GlobalCommandRunner,
TabColors,
MinFontSize,
MaxFontSize,
TabIcons,
Screen,
Session,
} from "../../../model/model";
import { Toggle, InlineSettingsTextEdit, SettingsError, InfoMessage, Modal, Dropdown, Tooltip } from "../common";
import { LineType, RendererPluginType, ClientDataType, CommandRtnType, RemoteType } from "../../../types/types";
import { PluginModel } from "../../../plugins/plugins";
import * as util from "../../../util/util";
import { commandRtnHandler } from "../../../util/util";
import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg";
import { ReactComponent as GlobeIcon } from "../../assets/icons/globe.svg";
import { ReactComponent as StatusCircleIcon } from "../../assets/icons/statuscircle.svg";
import "./settings.less";
type OV<V> = mobx.IObservableValue<V>;
// @ts-ignore
const VERSION = __WAVETERM_VERSION__;
// @ts-ignore
const BUILD = __WAVETERM_BUILD__;
const ScreenDeleteMessage = `
Are you sure you want to delete this tab?
All commands and output will be deleted. To hide the tab, and retain the commands and output, use 'archive'.
`.trim();
const SessionDeleteMessage = `
Are you sure you want to delete this workspace?
All commands and output will be deleted. To hide the workspace, and retain the commands and output, use 'archive'.
`.trim();
const WebShareConfirmMarkdown = `
You are about to share a terminal tab on the web. Please make sure that you do
NOT share any private information, keys, passwords, or other sensitive information.
You are responsible for what you are sharing, be smart.
`.trim();
const WebStopShareConfirmMarkdown = `
Are you sure you want to stop web-sharing this tab?
`.trim();
@mobxReact.observer
class ScreenSettingsModal extends React.Component<{}, {}> {
shareCopied: OV<boolean> = mobx.observable.box(false, { name: "ScreenSettings-shareCopied" });
errorMessage: OV<string> = mobx.observable.box(null, { name: "ScreenSettings-errorMessage" });
screen: Screen;
sessionId: string;
screenId: string;
remotes: RemoteType[];
constructor(props) {
super(props);
let screenSettingsModal = GlobalModel.screenSettingsModal.get();
let { sessionId, screenId } = screenSettingsModal;
this.sessionId = sessionId;
this.screenId = screenId;
this.screen = GlobalModel.getScreenById(sessionId, screenId);
if (this.screen == null || sessionId == null || screenId == null) {
return;
}
this.remotes = GlobalModel.remotes;
}
@boundMethod
getOptions(): { label: string; value: string }[] {
return this.remotes
.filter((r) => !r.archived)
.map((remote) => ({
...remote,
label:
remote.remotealias && !util.isBlank(remote.remotealias)
? `${remote.remotecanonicalname}`
: remote.remotecanonicalname,
value: remote.remotecanonicalname,
}))
.sort((a, b) => {
let connValA = util.getRemoteConnVal(a);
let connValB = util.getRemoteConnVal(b);
if (connValA !== connValB) {
return connValA - connValB;
}
return a.remoteidx - b.remoteidx;
});
}
@boundMethod
closeModal(): void {
mobx.action(() => {
GlobalModel.screenSettingsModal.set(null);
})();
GlobalModel.modalsModel.popModal();
}
@boundMethod
selectTabColor(color: string): void {
if (this.screen == null) {
return;
}
if (this.screen.getTabColor() == color) {
return;
}
let prtn = GlobalCommandRunner.screenSetSettings(this.screenId, { tabcolor: color }, false);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
selectTabIcon(icon: string): void {
if (this.screen.getTabIcon() == icon) {
return;
}
let prtn = GlobalCommandRunner.screenSetSettings(this.screen.screenId, { tabicon: icon }, false);
util.commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
handleChangeArchived(val: boolean): void {
if (this.screen == null) {
return;
}
if (this.screen.archived.get() == val) {
return;
}
let prtn = GlobalCommandRunner.screenArchive(this.screenId, val);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
handleChangeWebShare(val: boolean): void {
if (this.screen == null) {
return;
}
if (this.screen.isWebShared() == val) {
return;
}
let message = val ? WebShareConfirmMarkdown : WebStopShareConfirmMarkdown;
let alertRtn = GlobalModel.showAlert({ message: message, confirm: true, markdown: true });
alertRtn.then((result) => {
if (!result) {
return;
}
let prtn = GlobalCommandRunner.screenWebShare(this.screen.screenId, val);
commandRtnHandler(prtn, this.errorMessage);
});
}
@boundMethod
copyShareLink(): void {
if (this.screen == null) {
return null;
}
let shareLink = this.screen.getWebShareUrl();
if (shareLink == null) {
return;
}
navigator.clipboard.writeText(shareLink);
mobx.action(() => {
this.shareCopied.set(true);
})();
setTimeout(() => {
mobx.action(() => {
this.shareCopied.set(false);
})();
}, 600);
}
@boundMethod
inlineUpdateName(val: string): void {
if (this.screen == null) {
return;
}
if (util.isStrEq(val, this.screen.name.get())) {
return;
}
let prtn = GlobalCommandRunner.screenSetSettings(this.screenId, { name: val }, false);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
inlineUpdateShareName(val: string): void {
if (this.screen == null) {
return;
}
if (util.isStrEq(val, this.screen.getShareName())) {
return;
}
let prtn = GlobalCommandRunner.screenSetSettings(this.screenId, { sharename: val }, false);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
dismissError(): void {
mobx.action(() => {
this.errorMessage.set(null);
})();
}
@boundMethod
handleDeleteScreen(): void {
if (this.screen == null) {
return;
}
if (this.screen.getScreenLines().lines.length == 0) {
GlobalCommandRunner.screenDelete(this.screenId, false);
GlobalModel.modalsModel.popModal();
return;
}
let message = ScreenDeleteMessage;
let alertRtn = GlobalModel.showAlert({ message: message, confirm: true, markdown: true });
alertRtn.then((result) => {
if (!result) {
return;
}
let prtn = GlobalCommandRunner.screenDelete(this.screenId, false);
commandRtnHandler(prtn, this.errorMessage);
GlobalModel.modalsModel.popModal();
});
}
@boundMethod
selectRemote(cname: string): void {
let prtn = GlobalCommandRunner.screenSetRemote(cname, true, false);
util.commandRtnHandler(prtn, this.errorMessage);
}
render() {
let screen = this.screen;
if (screen == null) {
return null;
}
let color: string = null;
let icon: string = null;
let index: number = 0;
let curRemote = GlobalModel.getRemote(GlobalModel.getActiveScreen().getCurRemoteInstance().remoteid);
return (
<Modal className="screen-settings-modal">
<Modal.Header onClose={this.closeModal} title={`tab settings (${screen.name.get()})`} />
<div className="wave-modal-body">
<div className="settings-field">
<div className="settings-label">Tab Id</div>
<div className="settings-input">{screen.screenId}</div>
</div>
<div className="settings-field">
<div className="settings-label">Name</div>
<div className="settings-input">
<InlineSettingsTextEdit
placeholder="name"
text={screen.name.get() ?? "(none)"}
value={screen.name.get() ?? ""}
onChange={this.inlineUpdateName}
maxLength={50}
showIcon={true}
/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">Connection</div>
<div className="settings-input">
<Dropdown
className="screen-settings-dropdown"
label={curRemote.remotealias}
options={this.getOptions()}
defaultValue={curRemote.remotecanonicalname}
onChange={this.selectRemote}
decoration={{
startDecoration: (
<div className="lefticon">
<GlobeIcon className="globe-icon" />
<StatusCircleIcon
className={cn("status-icon", "status-" + curRemote.status)}
/>
</div>
),
}}
/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">Tab Color</div>
<div className="settings-input">
<div className="tab-colors">
<div className="tab-color-cur">
<SquareIcon className={cn("tab-color-icon", "color-" + screen.getTabColor())} />
<span className="tab-color-name">{screen.getTabColor()}</span>
</div>
<div className="tab-color-sep">|</div>
<For each="color" of={TabColors}>
<div
key={color}
className="tab-color-select"
onClick={() => this.selectTabColor(color)}
>
<SquareIcon className={cn("tab-color-icon", "color-" + color)} />
</div>
</For>
</div>
</div>
</div>
<div className="settings-field">
<div className="settings-label">Tab Icon</div>
<div className="settings-input">
<div className="tab-icons">
<div className="tab-icon-cur">
<If condition={screen.getTabIcon() == "default"}>
<SquareIcon className={cn("tab-color-icon", "color-white")} />
</If>
<If condition={screen.getTabIcon() != "default"}>
<i className={`fa-sharp fa-solid fa-${screen.getTabIcon()}`}></i>
</If>
<span className="tab-icon-name">{screen.getTabIcon()}</span>
</div>
<div className="tab-icon-sep">|</div>
<For each="icon" index="index" of={TabIcons}>
<div
key={`${color}-${index}`}
className="tab-icon-select"
onClick={() => this.selectTabIcon(icon)}
>
<i className={`fa-sharp fa-solid fa-${icon}`}></i>
</div>
</For>
</div>
</div>
</div>
<div className="settings-field">
<div className="settings-label archived-label">
<div className="">Archived</div>
<Tooltip
message={`Archive will hide the tab. Commands and output will be retained in history.`}
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
className="screen-settings-tooltip"
>
<i className="fa-sharp fa-regular fa-circle-question" />
</Tooltip>
</div>
<div className="settings-input">
<Toggle checked={screen.archived.get()} onChange={this.handleChangeArchived} />
</div>
</div>
<div className="settings-field">
<div className="settings-label actions-label">
<div>Actions</div>
<Tooltip
message={`Delete will remove the tab, removing all commands and output from history.`}
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
className="screen-settings-tooltip"
>
<i className="fa-sharp fa-regular fa-circle-question" />
</Tooltip>
</div>
<div className="settings-input">
<div
onClick={this.handleDeleteScreen}
className="button is-prompt-danger is-outlined is-small"
>
Delete Tab
</div>
</div>
</div>
<SettingsError errorMessage={this.errorMessage} />
</div>
<Modal.Footer cancelLabel="Close" onCancel={this.closeModal} />
</Modal>
);
}
}
@mobxReact.observer
class SessionSettingsModal extends React.Component<{}, {}> {
errorMessage: OV<string> = mobx.observable.box(null, { name: "ScreenSettings-errorMessage" });
session: Session;
sessionId: string;
constructor(props: any) {
super(props);
this.sessionId = GlobalModel.sessionSettingsModal.get();
this.session = GlobalModel.getSessionById(this.sessionId);
if (this.session == null) {
return;
}
}
@boundMethod
closeModal(): void {
mobx.action(() => {
GlobalModel.sessionSettingsModal.set(null);
})();
GlobalModel.modalsModel.popModal();
}
@boundMethod
handleInlineChangeName(newVal: string): void {
if (this.session == null) {
return;
}
if (util.isStrEq(newVal, this.session.name.get())) {
return;
}
let prtn = GlobalCommandRunner.sessionSetSettings(this.sessionId, { name: newVal }, false);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
handleChangeArchived(val: boolean): void {
if (this.session == null) {
return;
}
if (this.session.archived.get() == val) {
return;
}
let prtn = GlobalCommandRunner.sessionArchive(this.sessionId, val);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
handleDeleteSession(): void {
let message = SessionDeleteMessage;
let alertRtn = GlobalModel.showAlert({ message: message, confirm: true, markdown: true });
alertRtn.then((result) => {
if (!result) {
return;
}
let prtn = GlobalCommandRunner.sessionDelete(this.sessionId);
commandRtnHandler(prtn, this.errorMessage, () => GlobalModel.modalsModel.popModal());
});
}
@boundMethod
dismissError(): void {
mobx.action(() => {
this.errorMessage.set(null);
})();
}
render() {
if (this.session == null) {
return null;
}
return (
<Modal className="session-settings-modal">
<Modal.Header onClose={this.closeModal} title={`workspace settings (${this.session.name.get()})`} />
<div className="wave-modal-body">
<div className="settings-field">
<div className="settings-label">Name</div>
<div className="settings-input">
<InlineSettingsTextEdit
placeholder="name"
text={this.session.name.get() ?? "(none)"}
value={this.session.name.get() ?? ""}
onChange={this.handleInlineChangeName}
maxLength={50}
showIcon={true}
/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">
<div>Archived</div>
<InfoMessage width={400}>
Archive will hide the workspace from the active menu. Commands and output will be
retained in history.
</InfoMessage>
</div>
<div className="settings-input">
<Toggle checked={this.session.archived.get()} onChange={this.handleChangeArchived} />
</div>
</div>
<div className="settings-field">
<div className="settings-label">
<div>Actions</div>
<InfoMessage width={400}>
Delete will remove the workspace, removing all commands and output from history.
</InfoMessage>
</div>
<div className="settings-input">
<div
onClick={this.handleDeleteSession}
className="button is-prompt-danger is-outlined is-small"
>
Delete Workspace
</div>
</div>
</div>
<SettingsError errorMessage={this.errorMessage} />
</div>
<Modal.Footer cancelLabel="Close" onCancel={this.closeModal} />
</Modal>
);
}
}
@mobxReact.observer
class LineSettingsModal extends React.Component<{}, {}> {
rendererDropdownActive: OV<boolean> = mobx.observable.box(false, { name: "lineSettings-rendererDropdownActive" });
errorMessage: OV<string> = mobx.observable.box(null, { name: "ScreenSettings-errorMessage" });
linenum: number;
constructor(props: any) {
super(props);
this.linenum = GlobalModel.lineSettingsModal.get();
if (this.linenum == null) {
return;
}
}
@boundMethod
closeModal(): void {
mobx.action(() => {
GlobalModel.lineSettingsModal.set(null);
})();
GlobalModel.modalsModel.popModal();
}
@boundMethod
handleChangeArchived(val: boolean): void {
let line = this.getLine();
if (line == null) {
return;
}
let prtn = GlobalCommandRunner.lineArchive(line.lineid, val);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
toggleRendererDropdown(): void {
mobx.action(() => {
this.rendererDropdownActive.set(!this.rendererDropdownActive.get());
})();
}
getLine(): LineType {
let screen = GlobalModel.getActiveScreen();
if (screen == null) {
return;
}
return screen.getLineByNum(this.linenum);
}
@boundMethod
clickSetRenderer(renderer: string): void {
let line = this.getLine();
if (line == null) {
return;
}
let prtn = GlobalCommandRunner.lineSet(line.lineid, { renderer: renderer });
commandRtnHandler(prtn, this.errorMessage);
mobx.action(() => {
this.rendererDropdownActive.set(false);
})();
}
getOptions(plugins: RendererPluginType[]) {
// Add label and value to each object in the array
const options = plugins.map((item) => ({
...item,
label: item.name,
value: item.name,
}));
// Create an additional object with label "terminal" and value null
const terminalItem = {
label: "terminal",
value: null,
name: null,
rendererType: null,
heightType: null,
dataType: null,
collapseType: null,
globalCss: null,
mimeTypes: null,
};
// Create an additional object with label "none" and value none
const noneItem = {
label: "none",
value: "none",
name: null,
rendererType: null,
heightType: null,
dataType: null,
collapseType: null,
globalCss: null,
mimeTypes: null,
};
// Combine the options with the terminal item
return [terminalItem, ...options, noneItem];
}
render() {
let line = this.getLine();
if (line == null) {
setTimeout(() => {
this.closeModal();
}, 0);
return null;
}
let plugins = PluginModel.rendererPlugins;
let renderer = line.renderer ?? "terminal";
return (
<Modal className="line-settings-modal">
<Modal.Header onClose={this.closeModal} title={`line settings (${line.linenum})`} />
<div className="wave-modal-body">
<div className="settings-field">
<div className="settings-label">Renderer</div>
<div className="settings-input">
<Dropdown
className="renderer-dropdown"
options={this.getOptions(plugins)}
defaultValue={renderer}
onChange={this.clickSetRenderer}
/>
</div>
</div>
<SettingsError errorMessage={this.errorMessage} />
<div style={{ height: 50 }} />
</div>
<Modal.Footer cancelLabel="Close" onCancel={this.closeModal} />
</Modal>
);
}
}
@mobxReact.observer
class ClientSettingsModal extends React.Component<{}, {}> {
fontSizeDropdownActive: OV<boolean> = mobx.observable.box(false, { name: "clientSettings-fontSizeDropdownActive" });
errorMessage: OV<string> = mobx.observable.box(null, { name: "ClientSettings-errorMessage" });
@boundMethod
closeModal(): void {
GlobalModel.modalsModel.popModal();
}
@boundMethod
dismissError(): void {
mobx.action(() => {
this.errorMessage.set(null);
})();
}
@boundMethod
handleChangeFontSize(fontSize: string): void {
let newFontSize = Number(fontSize);
this.fontSizeDropdownActive.set(false);
if (GlobalModel.termFontSize.get() == newFontSize) {
return;
}
let prtn = GlobalCommandRunner.setTermFontSize(newFontSize, false);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
togglefontSizeDropdown(): void {
mobx.action(() => {
this.fontSizeDropdownActive.set(!this.fontSizeDropdownActive.get());
})();
}
@boundMethod
handleChangeTelemetry(val: boolean): void {
let prtn: Promise<CommandRtnType> = null;
if (val) {
prtn = GlobalCommandRunner.telemetryOn(false);
} else {
prtn = GlobalCommandRunner.telemetryOff(false);
}
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
handleChangeReleaseCheck(val: boolean): void {
let prtn: Promise<CommandRtnType> = null;
if (val) {
prtn = GlobalCommandRunner.releaseCheckAutoOn(false);
} else {
prtn = GlobalCommandRunner.releaseCheckAutoOff(false);
}
commandRtnHandler(prtn, this.errorMessage);
}
getFontSizes(): any {
let availableFontSizes: { label: string; value: number }[] = [];
for (let s = MinFontSize; s <= MaxFontSize; s++) {
availableFontSizes.push({ label: s + "px", value: s });
}
return availableFontSizes;
}
@boundMethod
inlineUpdateOpenAIModel(newModel: string): void {
let prtn = GlobalCommandRunner.setClientOpenAISettings({ model: newModel });
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
inlineUpdateOpenAIToken(newToken: string): void {
let prtn = GlobalCommandRunner.setClientOpenAISettings({ apitoken: newToken });
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
inlineUpdateOpenAIMaxTokens(newMaxTokensStr: string): void {
let prtn = GlobalCommandRunner.setClientOpenAISettings({ maxtokens: newMaxTokensStr });
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
setErrorMessage(msg: string): void {
mobx.action(() => {
this.errorMessage.set(msg);
})();
}
render() {
let cdata: ClientDataType = GlobalModel.clientData.get();
let openAIOpts = cdata.openaiopts ?? {};
let apiTokenStr = util.isBlank(openAIOpts.apitoken) ? "(not set)" : "********";
let maxTokensStr = String(
openAIOpts.maxtokens == null || openAIOpts.maxtokens == 0 ? 1000 : openAIOpts.maxtokens
);
let curFontSize = GlobalModel.termFontSize.get();
return (
<Modal className="client-settings-modal">
<Modal.Header onClose={this.closeModal} title="Client settings" />
<div className="wave-modal-body">
<div className="settings-field">
<div className="settings-label">Term Font Size</div>
<div className="settings-input">
<Dropdown
className="font-size-dropdown"
options={this.getFontSizes()}
defaultValue={`${curFontSize}px`}
onChange={this.handleChangeFontSize}
/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">Client ID</div>
<div className="settings-input">{cdata.clientid}</div>
</div>
<div className="settings-field">
<div className="settings-label">Client Version</div>
<div className="settings-input">
{VERSION} {BUILD}
</div>
</div>
<div className="settings-field">
<div className="settings-label">DB Version</div>
<div className="settings-input">{cdata.dbversion}</div>
</div>
<div className="settings-field">
<div className="settings-label">Basic Telemetry</div>
<div className="settings-input">
<Toggle checked={!cdata.clientopts.notelemetry} onChange={this.handleChangeTelemetry} />
</div>
</div>
<div className="settings-field">
<div className="settings-label">Check for Updates</div>
<div className="settings-input">
<Toggle
checked={!cdata.clientopts.noreleasecheck}
onChange={this.handleChangeReleaseCheck}
/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">OpenAI Token</div>
<div className="settings-input">
<InlineSettingsTextEdit
placeholder=""
text={apiTokenStr}
value={""}
onChange={this.inlineUpdateOpenAIToken}
maxLength={100}
showIcon={true}
/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">OpenAI Model</div>
<div className="settings-input">
<InlineSettingsTextEdit
placeholder="gpt-3.5-turbo"
text={util.isBlank(openAIOpts.model) ? "gpt-3.5-turbo" : openAIOpts.model}
value={openAIOpts.model ?? ""}
onChange={this.inlineUpdateOpenAIModel}
maxLength={100}
showIcon={true}
/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">OpenAI MaxTokens</div>
<div className="settings-input">
<InlineSettingsTextEdit
placeholder=""
text={maxTokensStr}
value={maxTokensStr}
onChange={this.inlineUpdateOpenAIMaxTokens}
maxLength={10}
showIcon={true}
/>
</div>
</div>
<SettingsError errorMessage={this.errorMessage} />
</div>
<Modal.Footer cancelLabel="Close" onCancel={this.closeModal} />
</Modal>
);
}
}
export {
ScreenSettingsModal,
SessionSettingsModal,
LineSettingsModal,
ClientSettingsModal,
WebStopShareConfirmMarkdown,
};

View File

@ -7,10 +7,10 @@ import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { For } from "tsx-control-statements/components";
import cn from "classnames";
import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
import { Modal, TextField, InputDecoration, Tooltip } from "../common";
import { GlobalModel, GlobalCommandRunner } from "../../../models";
import { Modal, TextField, InputDecoration, Tooltip } from "../elements";
import * as util from "../../../util/util";
import { Screen } from "../../../model/model";
import { Screen } from "../../../models";
import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg";
import "./tabswitcher.less";

View File

@ -4,8 +4,8 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import { boundMethod } from "autobind-decorator";
import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
import { Toggle, Modal, Button } from "../common";
import { GlobalModel, GlobalCommandRunner } from "../../../models";
import { Toggle, Modal, Button } from "../elements";
import * as util from "../../../util/util";
import { ClientDataType } from "../../../types/types";

View File

@ -0,0 +1,15 @@
@import "../../../app/common/themes/themes.less";
.userinput-modal {
width: 500px;
.wave-modal-content {
.wave-modal-body {
padding: 20px 20px;
.userinput-query {
margin-bottom: 10px;
}
}
}
}

View File

@ -0,0 +1,88 @@
import * as React from "react";
import { GlobalModel } from "../../../models";
import { Choose, When, If } from "tsx-control-statements/components";
import { Modal, PasswordField, Markdown } from "../elements";
import { UserInputRequest } from "../../../types/types";
import "./userinput.less";
export const UserInputModal = (userInputRequest: UserInputRequest) => {
const [responseText, setResponseText] = React.useState("");
const [countdown, setCountdown] = React.useState(Math.floor(userInputRequest.timeoutms / 1000));
const closeModal = React.useCallback(() => {
GlobalModel.sendUserInput({
type: "userinputresp",
requestid: userInputRequest.requestid,
errormsg: "Canceled by the user",
});
GlobalModel.remotesModel.closeModal();
}, [responseText, userInputRequest]);
const handleSendText = React.useCallback(() => {
GlobalModel.sendUserInput({
type: "userinputresp",
requestid: userInputRequest.requestid,
text: responseText,
});
GlobalModel.remotesModel.closeModal();
}, [responseText, userInputRequest]);
const handleSendConfirm = React.useCallback(
(response: boolean) => {
GlobalModel.sendUserInput({
type: "userinputresp",
requestid: userInputRequest.requestid,
confirm: response,
});
GlobalModel.remotesModel.closeModal();
},
[userInputRequest]
);
React.useEffect(() => {
let timeout: ReturnType<typeof setTimeout>;
if (countdown == 0) {
timeout = setTimeout(() => {
GlobalModel.remotesModel.closeModal();
}, 300);
} else {
timeout = setTimeout(() => {
setCountdown(countdown - 1);
}, 1000);
}
return () => clearTimeout(timeout);
}, [countdown]);
return (
<Modal className="userinput-modal">
<Modal.Header onClose={closeModal} title={userInputRequest.title + ` (${countdown})`} />
<div className="wave-modal-body">
<div className="userinput-query">
<If condition={userInputRequest.markdown}>
<Markdown text={userInputRequest.querytext} />
</If>
<If condition={!userInputRequest.markdown}>{userInputRequest.querytext}</If>
</div>
<Choose>
<When condition={userInputRequest.responsetype == "text"}>
<PasswordField onChange={setResponseText} value={responseText} maxLength={400} />
</When>
</Choose>
</div>
<Choose>
<When condition={userInputRequest.responsetype == "text"}>
<Modal.Footer onCancel={closeModal} onOk={handleSendText} okLabel="Continue" />
</When>
<When condition={userInputRequest.responsetype == "confirm"}>
<Modal.Footer
onCancel={() => handleSendConfirm(false)}
onOk={() => handleSendConfirm(true)}
okLabel="Yes"
cancelLabel="No"
/>
</When>
</Choose>
</Modal>
);
};

View File

@ -7,9 +7,9 @@ import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { If, For } from "tsx-control-statements/components";
import cn from "classnames";
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../model/model";
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../models";
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 textmeasure from "../../../util/textmeasure";

View File

@ -6,8 +6,14 @@ import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel, LineContainerModel } from "../../../model/model";
import type { LineType, RemoteType, RemotePtrType, LineHeightChangeCallbackType } from "../../../types/types";
import { GlobalModel } from "../../../models";
import type {
LineType,
RemoteType,
RemotePtrType,
LineHeightChangeCallbackType,
LineContainerType,
} from "../../../types/types";
import cn from "classnames";
import { isBlank } from "../../../util/util";
import { ReactComponent as FolderIcon } from "../../assets/icons/folder.svg";
@ -21,7 +27,7 @@ type OArr<V> = mobx.IObservableArray<V>;
type OMap<K, V> = mobx.ObservableMap<K, V>;
type RendererComponentProps = {
screen: LineContainerModel;
screen: LineContainerType;
line: LineType;
width: number;
staticRender: boolean;

View File

@ -7,8 +7,8 @@ import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { If, For } from "tsx-control-statements/components";
import cn from "classnames";
import { GlobalModel, RemotesModel, GlobalCommandRunner } from "../../model/model";
import { Button, IconButton, Status, ShowWaveShellInstallPrompt } from "../common/common";
import { GlobalModel, RemotesModel, GlobalCommandRunner } from "../../models";
import { Button, Status, ShowWaveShellInstallPrompt } from "../common/elements";
import * as T from "../../types/types";
import * as util from "../../util/util";
import * as appconst from "../appconst";

View File

@ -1,408 +0,0 @@
@import "../../app/common/themes/themes.less";
.modal.prompt-modal.remotes-modal {
.modal-content {
min-width: 850px;
}
.icon {
width: 1em;
height: 1em;
fill: @base-color;
margin: 0;
}
.button {
svg {
float: right;
margin-top: 0.3em;
margin-right: 0;
}
}
.dropdown,
.button {
display: inline-flex;
}
.dropdown .button {
border: none !important;
}
.inner-content {
display: flex;
flex-direction: row;
align-items: stretch;
padding: 0;
min-height: 45em;
max-height: 45em;
.remotes-menu {
flex: 0 0 200px;
border-right: 1px solid @disabled-color;
overflow-y: auto;
.remote-menu-item {
border-top: 1px solid @disabled-color;
padding: 0.5em;
display: flex;
flex-direction: row;
cursor: pointer;
&.add-remote {
padding: 10px 5px 10px 5px;
}
&:hover {
background-color: #333;
}
&.is-selected {
background-color: @active-menu-color;
.remote-name .remote-name-secondary {
color: @term-white;
}
}
&:first-child {
border-top: 0;
}
.remote-status-light {
width: 2em;
margin-top: 0.7em;
margin-right: 0.7em;
font-size: 0.8em;
}
.remote-name {
flex-grow: 1;
.remote-name-primary {
font-weight: bold;
max-width: 10em;
margin-right: 1em;
}
.remote-name-secondary {
color: @disabled-color;
max-width: 14em;
}
}
}
}
.remote-detail {
padding: 10px;
flex-grow: 1;
display: flex;
flex-direction: column;
.settings-field {
margin-top: 0.75em;
}
* {
flex-shrink: 0;
}
.detail-subtitle {
margin-bottom: 10px;
margin-top: 10px;
}
.title {
color: @term-white;
padding: 0.75em 0;
margin-bottom: 0;
border-bottom: 1px solid #777;
}
.terminal-wrapper {
margin-left: 0;
margin-bottom: 0;
&.has-message {
margin-top: 0;
}
box-shadow: none;
border: 1px solid #777;
border-radius: 0 0 5px 5px;
.xterm-rows {
padding-top: 0.5em;
}
}
.action-buttons {
display: flex;
flex-direction: row;
gap: 10px;
margin-top: 2px;
}
.remote-message {
margin-top: 5px;
padding: 8px;
border-radius: 5px 5px 0 0;
background-color: #333;
border: 1px solid #777;
border-bottom: none;
.message-row {
display: flex;
flex-direction: row;
align-items: center;
svg {
vertical-align: text-bottom;
}
}
.remote-status {
position: relative;
top: -1px;
}
.button {
height: 22px;
}
}
.settings-field {
.update-auth-button {
visibility: hidden;
}
&:hover {
.update-auth-button {
visibility: visible;
}
.hide-hover {
display: none;
}
}
}
&.auth-editing,
&.create-remote {
.settings-field.align-top {
align-items: flex-start;
.settings-label {
margin-top: 8px;
}
.settings-input {
align-items: flex-start;
}
}
.settings-label {
display: flex;
flex-direction: row;
align-items: center;
width: 12em !important;
}
.settings-field .settings-input .undo-icon {
cursor: pointer;
margin-left: 5px;
}
.editremote-dropdown .dropdown-trigger button {
width: 120px;
justify-content: flex-start;
color: @base-color;
border: none;
&:hover {
box-shadow: none;
}
}
.settings-field .raw-input {
width: 120px;
}
.settings-input input {
background: rgba(255, 255, 255, 0.8);
width: 250px;
outline: none;
}
.dropdown .dropdown-item {
padding: 5px 5px 5px 12px;
}
.dropdown .dropdown-content {
max-width: 10.6em;
}
.settings-input {
.info-message {
margin-left: 22px;
}
}
.settings-label {
.info-message {
margin-right: 15px;
}
}
}
}
}
.terminal-wrapper {
position: relative;
padding: 2px 10px 5px 4px;
margin: 5px 5px 10px 5px;
box-shadow: 0 0 1px 1px rgba(255, 255, 255, 0.3);
&.focus {
box-shadow: 0 0 3px 3px rgba(255, 255, 255, 0.3);
}
.term-tag {
position: absolute;
top: 0;
right: 0;
background-color: @term-red;
color: @term-white;
z-index: 110;
padding: 4px;
}
}
}
.dropdown.conn-dropdown {
padding-left: 0;
border-radius: 8px;
background-color: rgba(241, 246, 243, 0.08);
.conn-dd-trigger {
display: flex;
flex-direction: row;
width: 413px;
padding: 6px 8px 6px 12px;
align-items: center;
height: 42px;
.lefticon {
margin-right: 8px;
margin-top: 4px;
position: relative;
.status-icon {
width: 10px;
height: 10px;
stroke-width: 2px;
stroke: @status-outline;
position: absolute;
bottom: 3px;
right: -2px;
}
}
.dd-control {
display: flex;
padding: 4px;
align-items: center;
.icon {
height: 16px;
width: 16px;
}
}
.globe-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
}
.conntext {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
flex: 1 0 0;
.conntext-solo {
color: @text-primary;
text-overflow: ellipsis;
}
.conntext-1 {
color: @text-primary;
text-overflow: ellipsis;
}
.conntext-2 {
color: @text-secondary;
text-overflow: ellipsis;
}
}
}
.conn-dd-menu {
display: flex;
width: 413px;
padding: 6px;
flex-direction: column;
align-items: flex-start;
border-radius: 8px;
background-color: @dropdown-menu;
.dropdown-item {
display: flex;
padding: 5px 12px 5px 8px;
align-items: center;
gap: 8px;
align-self: stretch;
border-radius: 6px;
.status-div {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
padding: 3px;
svg.status-icon {
width: 10px;
height: 10px;
}
}
.add-div {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
svg.add-icon {
width: 16px;
height: 16px;
path {
fill: @text-primary;
}
}
}
.text-standard {
color: @text-secondary;
}
.text-caption {
color: @text-caption;
}
.ellipsis {
text-overflow: ellipsis;
}
&:hover {
background-color: rgba(241, 246, 243, 0.08);
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -8,13 +8,13 @@ import { If, For } from "tsx-control-statements/components";
import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { GlobalModel, GlobalCommandRunner, Cmd } from "../../model/model";
import { GlobalModel, GlobalCommandRunner, Cmd } from "../../models";
import { HistoryItem, RemotePtrType, LineType, CmdDataType } from "../../types/types";
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import customParseFormat from "dayjs/plugin/customParseFormat";
import { Line } from "../line/linecomps";
import { CmdStrCode } from "../common/common";
import { CmdStrCode } from "../common/elements";
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../util/keyutil";
import { ReactComponent as FavoritesIcon } from "../assets/icons/favourites.svg";

View File

@ -9,7 +9,7 @@ import { boundMethod } from "autobind-decorator";
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { If } from "tsx-control-statements/components";
import { GlobalModel, GlobalCommandRunner, Cmd, getTermPtyData } from "../../model/model";
import { GlobalModel, GlobalCommandRunner, Cmd } from "../../models";
import { termHeightFromRows } from "../../util/textmeasure";
import type {
LineType,
@ -19,11 +19,12 @@ import type {
LineHeightChangeCallbackType,
RendererModelInitializeParams,
RendererModel,
LineContainerType,
} from "../../types/types";
import cn from "classnames";
import { getTermPtyData } from "../../util/modelutil";
import type { LineContainerModel } from "../../model/model";
import { renderCmdText } from "../common/common";
import { renderCmdText } from "../common/elements";
import { SimpleBlobRenderer } from "../../plugins/core/basicrenderer";
import { IncrementalRenderer } from "../../plugins/core/incrementalrenderer";
import { TerminalRenderer } from "../../plugins/terminal/terminal";
@ -101,7 +102,7 @@ class SmallLineAvatar extends React.Component<{ line: LineType; cmd: Cmd; onRigh
@mobxReact.observer
class LineCmd extends React.Component<
{
screen: LineContainerModel;
screen: LineContainerType;
line: LineType;
width: number;
staticRender: boolean;
@ -799,7 +800,7 @@ class LineCmd extends React.Component<
@mobxReact.observer
class Line extends React.Component<
{
screen: LineContainerModel;
screen: LineContainerType;
line: LineType;
width: number;
staticRender: boolean;
@ -830,7 +831,7 @@ class Line extends React.Component<
@mobxReact.observer
class LineText extends React.Component<
{
screen: LineContainerModel;
screen: LineContainerType;
line: LineType;
renderMode: RenderModeType;
noSelect?: boolean;

View File

@ -22,10 +22,11 @@ import type {
LineType,
TermContextUnion,
RendererContainerType,
ExtBlob,
} from "../../../types/types";
import { debounce } from "throttle-debounce";
import * as util from "../../../util/util";
import { GlobalModel } from "../../../model/model";
import { GlobalModel } from "../../../models";
type OV<V> = mobx.IObservableValue<V>;
type CV<V> = mobx.IComputedValue<V>;
@ -277,7 +278,7 @@ class SimpleBlobRenderer extends React.Component<
cwd={festate.cwd}
cmdstr={cmdstr}
exitcode={exitcode}
data={model.dataBlob}
data={model.dataBlob as ExtBlob}
readOnly={model.readOnly}
notFound={model.notFound}
lineState={model.lineState}

View File

@ -5,9 +5,9 @@ import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { GlobalModel } from "../../model/model";
import { GlobalModel } from "../../models";
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";
@ -22,7 +22,7 @@ class PluginsView extends React.Component<{}, {}> {
renderPluginIcon(plugin): any {
let Comp = plugin.iconComp;
return <Comp/>;
return <Comp />;
}
render() {

View File

@ -17,10 +17,10 @@ import { ReactComponent as WorkspacesIcon } from "../assets/icons/workspaces.svg
import { ReactComponent as SettingsIcon } from "../assets/icons/settings.svg";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel, GlobalCommandRunner, Session, VERSION } from "../../model/model";
import { GlobalModel, GlobalCommandRunner, Session } from "../../models";
import { isBlank, openLink } from "../../util/util";
import { ResizableSidebar } from "../common/common";
import * as constants from "../appconst";
import { ResizableSidebar } from "../common/elements";
import * as appconst from "../appconst";
import "./sidebar.less";
import { ActionsIcon, CenteredIcon, FrontIcon, StatusIndicator } from "../common/icons/icons";
@ -167,7 +167,7 @@ class MainSideBar extends React.Component<MainSideBarProps, {}> {
mobx.action(() => {
GlobalModel.sessionSettingsModal.set(session.sessionId);
})();
GlobalModel.modalsModel.pushModal(constants.SESSION_SETTINGS);
GlobalModel.modalsModel.pushModal(appconst.SESSION_SETTINGS);
}
getSessions() {
@ -208,7 +208,7 @@ class MainSideBar extends React.Component<MainSideBarProps, {}> {
let clientData = this.props.clientData;
let needsUpdate = false;
if (!clientData?.clientopts.noreleasecheck && !isBlank(clientData?.releaseinfo?.latestversion)) {
needsUpdate = compareLoose(VERSION, clientData.releaseinfo.latestversion) < 0;
needsUpdate = compareLoose(appconst.VERSION, clientData.releaseinfo.latestversion) < 0;
}
let mainSidebar = GlobalModel.mainSidebarModel;
let isCollapsed = mainSidebar.getCollapsed();

View File

@ -4,7 +4,7 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { GlobalModel } from "../../../model/model";
import { GlobalModel } from "../../../models";
import { isBlank } from "../../../util/util";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
@ -12,7 +12,7 @@ import { Prompt } from "../../common/prompt/prompt";
import { TextAreaInput } from "./textareainput";
import { If, For } from "tsx-control-statements/components";
import type { OpenAICmdInfoChatMessageType } from "../../../types/types";
import { Markdown } from "../../common/common";
import { Markdown } from "../../common/elements";
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../../util/keyutil";
@mobxReact.observer

View File

@ -10,8 +10,8 @@ import cn from "classnames";
import dayjs from "dayjs";
import type { RemoteType, RemoteInstanceType, RemotePtrType } from "../../../types/types";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel, GlobalCommandRunner, Screen } from "../../../model/model";
import { renderCmdText } from "../../common/common";
import { GlobalModel, GlobalCommandRunner, Screen } from "../../../models";
import { renderCmdText } from "../../common/elements";
import { TextAreaInput } from "./textareainput";
import { InfoMsg } from "./infomsg";
import { HistoryInfo } from "./historyinfo";

View File

@ -11,7 +11,7 @@ import cn from "classnames";
import dayjs from "dayjs";
import type { HistoryItem, HistoryQueryOpts } from "../../../types/types";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel } from "../../../model/model";
import { GlobalModel } from "../../../models";
import { isBlank } from "../../../util/util";
dayjs.extend(localizedFormat);

View File

@ -7,7 +7,7 @@ import { If, For } from "tsx-control-statements/components";
import cn from "classnames";
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel } from "../../../model/model";
import { GlobalModel } from "../../../models";
import { makeExternLink } from "../../../util/util";
dayjs.extend(localizedFormat);

View File

@ -9,7 +9,7 @@ import * as util from "../../../util/util";
import { If } from "tsx-control-statements/components";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { GlobalModel, GlobalCommandRunner, Screen } from "../../../model/model";
import { GlobalModel, GlobalCommandRunner, Screen } from "../../../models";
import { getMonoFontSize } from "../../../util/textmeasure";
import { isModKeyPress, hasNoModifiers } from "../../../util/util";
import * as appconst from "../../appconst";

View File

@ -10,16 +10,15 @@ import { If, For } from "tsx-control-statements/components";
import cn from "classnames";
import { debounce } from "throttle-debounce";
import dayjs from "dayjs";
import { GlobalCommandRunner, TabColors, TabIcons, ForwardLineContainer, GlobalModel, ScreenLines, Screen, Session } from "../../../model/model";
import { GlobalCommandRunner, ForwardLineContainer, GlobalModel, ScreenLines, Screen, Session } from "../../../models";
import type { LineType, RenderModeType, LineFactoryProps } from "../../../types/types";
import * as T from "../../../types/types";
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 { Line } from "../../line/linecomps";
import { LinesView } from "../../line/linesview";
import * as util from "../../../util/util";
import { TextField, Dropdown } from "../../common/common";
import { ReactComponent as EllipseIcon } from "../../assets/icons/ellipse.svg";
import { ReactComponent as Check12Icon } from "../../assets/icons/check12.svg";
import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg";
@ -434,7 +433,7 @@ class NewTabSettings extends React.Component<{ screen: Screen }, {}> {
<div key="square" className="icondiv" title="square" onClick={() => this.selectTabIcon("square")}>
<SquareIcon className="icon square-icon" />
</div>
<For each="icon" of={TabIcons}>
<For each="icon" of={appconst.TabIcons}>
<div
className="icondiv tabicon"
key={icon}
@ -461,7 +460,7 @@ class NewTabSettings extends React.Component<{ screen: Screen }, {}> {
<>
<div className="text-s1 unselectable">Select the color</div>
<div className="control-iconlist">
<For each="color" of={TabColors}>
<For each="color" of={appconst.TabColors}>
<div
className="icondiv"
key={color}

View File

@ -6,9 +6,9 @@ import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { GlobalModel, GlobalCommandRunner, Screen } from "../../../model/model";
import { GlobalModel, GlobalCommandRunner, Screen } from "../../../models";
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 * as constants from "../../appconst";
import { Reorder } from "framer-motion";

View File

@ -7,7 +7,7 @@ import * as mobx from "mobx";
import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import { For } from "tsx-control-statements/components";
import { GlobalModel, GlobalCommandRunner, Session, Screen } from "../../../model/model";
import { GlobalModel, GlobalCommandRunner, Session, Screen } from "../../../models";
import { ReactComponent as AddIcon } from "../../assets/icons/add.svg";
import { Reorder } from "framer-motion";
import { ScreenTab } from "./tab";

View File

@ -7,7 +7,7 @@ import * as mobx from "mobx";
import cn from "classnames";
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel } from "../../model/model";
import { GlobalModel } from "../../models";
import { CmdInput } from "./cmdinput/cmdinput";
import { ScreenView } from "./screen/screenview";
import { ScreenTabs } from "./screen/tabs";

287
src/models/bookmarks.ts Normal file
View File

@ -0,0 +1,287 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as mobx from "mobx";
import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import { genMergeSimpleData } from "../util/util";
import { BookmarkType } from "../types/types";
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../util/keyutil";
import { OV, OArr } from "../types/types";
import { GlobalCommandRunner } from "./global";
import { Model } from "./model";
class BookmarksModel {
globalModel: Model;
bookmarks: OArr<BookmarkType> = mobx.observable.array([], {
name: "Bookmarks",
});
activeBookmark: OV<string> = mobx.observable.box(null, {
name: "activeBookmark",
});
editingBookmark: OV<string> = mobx.observable.box(null, {
name: "editingBookmark",
});
pendingDelete: OV<string> = mobx.observable.box(null, {
name: "pendingDelete",
});
copiedIndicator: OV<string> = mobx.observable.box(null, {
name: "copiedIndicator",
});
tempDesc: OV<string> = mobx.observable.box("", {
name: "bookmarkEdit-tempDesc",
});
tempCmd: OV<string> = mobx.observable.box("", {
name: "bookmarkEdit-tempCmd",
});
constructor(globalModel: Model) {
this.globalModel = globalModel;
}
showBookmarksView(bmArr: BookmarkType[], selectedBookmarkId: string): void {
bmArr = bmArr ?? [];
mobx.action(() => {
this.reset();
this.globalModel.activeMainView.set("bookmarks");
this.bookmarks.replace(bmArr);
if (selectedBookmarkId != null) {
this.selectBookmark(selectedBookmarkId);
}
if (this.activeBookmark.get() == null && bmArr.length > 0) {
this.activeBookmark.set(bmArr[0].bookmarkid);
}
})();
}
reset(): void {
mobx.action(() => {
this.activeBookmark.set(null);
this.editingBookmark.set(null);
this.pendingDelete.set(null);
this.tempDesc.set("");
this.tempCmd.set("");
})();
}
closeView(): void {
this.globalModel.showSessionView();
setTimeout(() => this.globalModel.inputModel.giveFocus(), 50);
}
@boundMethod
clearPendingDelete(): void {
mobx.action(() => this.pendingDelete.set(null))();
}
useBookmark(bookmarkId: string): void {
let bm = this.getBookmark(bookmarkId);
if (bm == null) {
return;
}
mobx.action(() => {
this.reset();
this.globalModel.showSessionView();
this.globalModel.inputModel.setCurLine(bm.cmdstr);
setTimeout(() => this.globalModel.inputModel.giveFocus(), 50);
})();
}
selectBookmark(bookmarkId: string): void {
let bm = this.getBookmark(bookmarkId);
if (bm == null) {
return;
}
if (this.activeBookmark.get() == bookmarkId) {
return;
}
mobx.action(() => {
this.cancelEdit();
this.activeBookmark.set(bookmarkId);
})();
}
cancelEdit(): void {
mobx.action(() => {
this.pendingDelete.set(null);
this.editingBookmark.set(null);
this.tempDesc.set("");
this.tempCmd.set("");
})();
}
confirmEdit(): void {
if (this.editingBookmark.get() == null) {
return;
}
let bm = this.getBookmark(this.editingBookmark.get());
mobx.action(() => {
this.editingBookmark.set(null);
bm.description = this.tempDesc.get();
bm.cmdstr = this.tempCmd.get();
this.tempDesc.set("");
this.tempCmd.set("");
})();
GlobalCommandRunner.editBookmark(bm.bookmarkid, bm.description, bm.cmdstr);
}
handleDeleteBookmark(bookmarkId: string): void {
if (this.pendingDelete.get() == null || this.pendingDelete.get() != this.activeBookmark.get()) {
mobx.action(() => this.pendingDelete.set(this.activeBookmark.get()))();
setTimeout(this.clearPendingDelete, 2000);
return;
}
GlobalCommandRunner.deleteBookmark(bookmarkId);
this.clearPendingDelete();
}
getBookmark(bookmarkId: string): BookmarkType {
if (bookmarkId == null) {
return null;
}
for (const bm of this.bookmarks) {
if (bm.bookmarkid == bookmarkId) {
return bm;
}
}
return null;
}
getBookmarkPos(bookmarkId: string): number {
if (bookmarkId == null) {
return -1;
}
for (let i = 0; i < this.bookmarks.length; i++) {
let bm = this.bookmarks[i];
if (bm.bookmarkid == bookmarkId) {
return i;
}
}
return -1;
}
getActiveBookmark(): BookmarkType {
let activeBookmarkId = this.activeBookmark.get();
return this.getBookmark(activeBookmarkId);
}
handleEditBookmark(bookmarkId: string): void {
let bm = this.getBookmark(bookmarkId);
if (bm == null) {
return;
}
mobx.action(() => {
this.pendingDelete.set(null);
this.activeBookmark.set(bookmarkId);
this.editingBookmark.set(bookmarkId);
this.tempDesc.set(bm.description ?? "");
this.tempCmd.set(bm.cmdstr ?? "");
})();
}
handleCopyBookmark(bookmarkId: string): void {
let bm = this.getBookmark(bookmarkId);
if (bm == null) {
return;
}
navigator.clipboard.writeText(bm.cmdstr);
mobx.action(() => {
this.copiedIndicator.set(bm.bookmarkid);
})();
setTimeout(() => {
mobx.action(() => {
this.copiedIndicator.set(null);
})();
}, 600);
}
mergeBookmarks(bmArr: BookmarkType[]): void {
mobx.action(() => {
genMergeSimpleData(
this.bookmarks,
bmArr,
(bm: BookmarkType) => bm.bookmarkid,
(bm: BookmarkType) => sprintf("%05d", bm.orderidx)
);
})();
}
handleDocKeyDown(e: any): void {
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
if (checkKeyPressed(waveEvent, "Escape")) {
e.preventDefault();
if (this.editingBookmark.get() != null) {
this.cancelEdit();
return;
}
this.closeView();
return;
}
if (this.editingBookmark.get() != null) {
return;
}
if (checkKeyPressed(waveEvent, "Backspace") || checkKeyPressed(waveEvent, "Delete")) {
if (this.activeBookmark.get() == null) {
return;
}
e.preventDefault();
this.handleDeleteBookmark(this.activeBookmark.get());
return;
}
if (
checkKeyPressed(waveEvent, "ArrowUp") ||
checkKeyPressed(waveEvent, "ArrowDown") ||
checkKeyPressed(waveEvent, "PageUp") ||
checkKeyPressed(waveEvent, "PageDown")
) {
e.preventDefault();
if (this.bookmarks.length == 0) {
return;
}
let newPos = 0; // if active is null, then newPos will be 0 (select the first)
if (this.activeBookmark.get() != null) {
let amtMap = { ArrowUp: -1, ArrowDown: 1, PageUp: -10, PageDown: 10 };
let amt = amtMap[e.code];
let curIdx = this.getBookmarkPos(this.activeBookmark.get());
newPos = curIdx + amt;
if (newPos < 0) {
newPos = 0;
}
if (newPos >= this.bookmarks.length) {
newPos = this.bookmarks.length - 1;
}
}
let bm = this.bookmarks[newPos];
mobx.action(() => {
this.activeBookmark.set(bm.bookmarkid);
})();
return;
}
if (checkKeyPressed(waveEvent, "Enter")) {
if (this.activeBookmark.get() == null) {
return;
}
this.useBookmark(this.activeBookmark.get());
return;
}
if (checkKeyPressed(waveEvent, "e")) {
if (this.activeBookmark.get() == null) {
return;
}
e.preventDefault();
this.handleEditBookmark(this.activeBookmark.get());
return;
}
if (checkKeyPressed(waveEvent, "c")) {
if (this.activeBookmark.get() == null) {
return;
}
e.preventDefault();
this.handleCopyBookmark(this.activeBookmark.get());
}
}
}
export { BookmarksModel };

View File

@ -0,0 +1,26 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as mobx from "mobx";
import { Model } from "./model";
class ClientSettingsViewModel {
globalModel: Model;
constructor(globalModel: Model) {
this.globalModel = globalModel;
}
closeView(): void {
this.globalModel.showSessionView();
setTimeout(() => this.globalModel.inputModel.giveFocus(), 50);
}
showClientSettingsView(): void {
mobx.action(() => {
this.globalModel.activeMainView.set("clientsettings");
})();
}
}
export { ClientSettingsViewModel };

145
src/models/cmd.ts Normal file
View File

@ -0,0 +1,145 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as mobx from "mobx";
import { stringToBase64 } from "../util/util";
import { TermWrap } from "../plugins/terminal/term";
import {
RemotePtrType,
CmdDataType,
TermOptsType,
FeInputPacketType,
RendererModel,
WebCmd,
WebRemote,
} from "../types/types";
import { cmdStatusIsRunning } from "../app/line/lineutil";
import { Model } from "./model";
import { OV } from "../types/types";
const InputChunkSize = 500;
class Cmd {
model: Model;
screenId: string;
remote: RemotePtrType;
lineId: string;
data: OV<CmdDataType>;
constructor(cmd: CmdDataType) {
this.model = Model.getInstance();
this.screenId = cmd.screenid;
this.lineId = cmd.lineid;
this.remote = cmd.remote;
this.data = mobx.observable.box(cmd, { deep: false, name: "cmd-data" });
}
setCmd(cmd: CmdDataType) {
mobx.action(() => {
let origData = this.data.get();
this.data.set(cmd);
if (origData != null && cmd != null && origData.status != cmd.status) {
this.model.cmdStatusUpdate(this.screenId, this.lineId, origData.status, cmd.status);
}
})();
}
getRestartTs(): number {
return this.data.get().restartts;
}
getAsWebCmd(lineid: string): WebCmd {
let cmd = this.data.get();
let remote = this.model.getRemote(this.remote.remoteid);
let webRemote: WebRemote = null;
if (remote != null) {
webRemote = {
remoteid: cmd.remote.remoteid,
alias: remote.remotealias,
canonicalname: remote.remotecanonicalname,
name: this.remote.name,
homedir: remote.remotevars["home"],
isroot: !!remote.remotevars["isroot"],
};
}
let webCmd: WebCmd = {
screenid: cmd.screenid,
lineid: lineid,
remote: webRemote,
status: cmd.status,
cmdstr: cmd.cmdstr,
rawcmdstr: cmd.rawcmdstr,
festate: cmd.festate,
termopts: cmd.termopts,
cmdpid: cmd.cmdpid,
remotepid: cmd.remotepid,
donets: cmd.donets,
exitcode: cmd.exitcode,
durationms: cmd.durationms,
rtnstate: cmd.rtnstate,
vts: 0,
rtnstatestr: null,
};
return webCmd;
}
getExitCode(): number {
return this.data.get().exitcode;
}
getRtnState(): boolean {
return this.data.get().rtnstate;
}
getStatus(): string {
return this.data.get().status;
}
getTermOpts(): TermOptsType {
return this.data.get().termopts;
}
getCmdStr(): string {
return this.data.get().cmdstr;
}
getRemoteFeState(): Record<string, string> {
return this.data.get().festate;
}
isRunning(): boolean {
let data = this.data.get();
return cmdStatusIsRunning(data.status);
}
handleData(data: string, termWrap: TermWrap): void {
if (!this.isRunning()) {
return;
}
for (let pos = 0; pos < data.length; pos += InputChunkSize) {
let dataChunk = data.slice(pos, pos + InputChunkSize);
this.handleInputChunk(dataChunk);
}
}
handleDataFromRenderer(data: string, renderer: RendererModel): void {
if (!this.isRunning()) {
return;
}
for (let pos = 0; pos < data.length; pos += InputChunkSize) {
let dataChunk = data.slice(pos, pos + InputChunkSize);
this.handleInputChunk(dataChunk);
}
}
handleInputChunk(data: string): void {
let inputPacket: FeInputPacketType = {
type: "feinput",
ck: this.screenId + "/" + this.lineId,
remote: this.remote,
inputdata64: stringToBase64(data),
};
this.model.sendInputPacket(inputPacket);
}
}
export { Cmd };

431
src/models/commandrunner.ts Normal file
View File

@ -0,0 +1,431 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as mobx from "mobx";
import { RendererContext, CommandRtnType, HistorySearchParams, LineStateType } from "../types/types";
import { GlobalModel } from "./global";
class CommandRunner {
private constructor() {}
static getInstance() {
if (!(window as any).GlobalCommandRunner) {
(window as any).GlobalCommandRunner = new CommandRunner();
}
return (window as any).GlobalCommandRunner;
}
loadHistory(show: boolean, htype: string) {
let kwargs = { nohist: "1" };
if (!show) {
kwargs["noshow"] = "1";
}
if (htype != null && htype != "screen") {
kwargs["type"] = htype;
}
GlobalModel.submitCommand("history", null, null, kwargs, true);
}
resetShellState() {
GlobalModel.submitCommand("reset", null, null, null, true);
}
historyPurgeLines(lines: string[]): Promise<CommandRtnType> {
let prtn = GlobalModel.submitCommand("history", "purge", lines, { nohist: "1" }, false);
return prtn;
}
switchSession(session: string) {
mobx.action(() => {
GlobalModel.activeMainView.set("session");
})();
GlobalModel.submitCommand("session", null, [session], { nohist: "1" }, false);
}
switchScreen(screen: string, session?: string) {
mobx.action(() => {
GlobalModel.activeMainView.set("session");
})();
let kwargs = { nohist: "1" };
if (session != null) {
kwargs["session"] = session;
}
GlobalModel.submitCommand("screen", null, [screen], kwargs, false);
}
lineView(sessionId: string, screenId: string, lineNum?: number) {
let screen = GlobalModel.getScreenById(sessionId, screenId);
if (screen != null && lineNum != null) {
screen.setAnchorFields(lineNum, 0, "line:view");
}
let lineNumStr = lineNum == null || lineNum == 0 ? "E" : String(lineNum);
GlobalModel.submitCommand("line", "view", [sessionId, screenId, lineNumStr], { nohist: "1" }, false);
}
lineArchive(lineArg: string, archive: boolean): Promise<CommandRtnType> {
let kwargs = { nohist: "1" };
let archiveStr = archive ? "1" : "0";
return GlobalModel.submitCommand("line", "archive", [lineArg, archiveStr], kwargs, false);
}
lineDelete(lineArg: string, interactive: boolean): Promise<CommandRtnType> {
return GlobalModel.submitCommand("line", "delete", [lineArg], { nohist: "1" }, interactive);
}
lineRestart(lineArg: string, interactive: boolean): Promise<CommandRtnType> {
return GlobalModel.submitCommand("line", "restart", [lineArg], { nohist: "1" }, interactive);
}
lineSet(lineArg: string, opts: { renderer?: string }): Promise<CommandRtnType> {
let kwargs = { nohist: "1" };
if ("renderer" in opts) {
kwargs["renderer"] = opts.renderer ?? "";
}
return GlobalModel.submitCommand("line", "set", [lineArg], kwargs, false);
}
createNewSession() {
GlobalModel.submitCommand("session", "open", null, { nohist: "1" }, false);
}
createNewScreen() {
GlobalModel.submitCommand("screen", "open", null, { nohist: "1" }, false);
}
closeScreen(screen: string) {
GlobalModel.submitCommand("screen", "close", [screen], { nohist: "1" }, false);
}
// include is lineIds to include, exclude is lineIds to exclude
// if include is given then it *only* does those ids. if exclude is given (or not),
// it does all running commands in the screen except for excluded.
resizeScreen(screenId: string, rows: number, cols: number, opts?: { include?: string[]; exclude?: string[] }) {
let kwargs: Record<string, string> = {
nohist: "1",
screen: screenId,
cols: String(cols),
rows: String(rows),
};
if (opts?.include != null && opts?.include.length > 0) {
kwargs.include = opts.include.join(",");
}
if (opts?.exclude != null && opts?.exclude.length > 0) {
kwargs.exclude = opts.exclude.join(",");
}
GlobalModel.submitCommand("screen", "resize", null, kwargs, false);
}
screenArchive(screenId: string, shouldArchive: boolean): Promise<CommandRtnType> {
return GlobalModel.submitCommand(
"screen",
"archive",
[screenId, shouldArchive ? "1" : "0"],
{ nohist: "1" },
false
);
}
screenDelete(screenId: string, interactive: boolean): Promise<CommandRtnType> {
return GlobalModel.submitCommand("screen", "delete", [screenId], { nohist: "1" }, interactive);
}
screenWebShare(screenId: string, shouldShare: boolean): Promise<CommandRtnType> {
let kwargs: Record<string, string> = { nohist: "1" };
kwargs["screen"] = screenId;
return GlobalModel.submitCommand("screen", "webshare", [shouldShare ? "1" : "0"], kwargs, false);
}
showRemote(remoteid: string) {
GlobalModel.submitCommand("remote", "show", null, { nohist: "1", remote: remoteid }, true);
}
showAllRemotes() {
GlobalModel.submitCommand("remote", "showall", null, { nohist: "1" }, true);
}
connectRemote(remoteid: string) {
GlobalModel.submitCommand("remote", "connect", null, { nohist: "1", remote: remoteid }, true);
}
disconnectRemote(remoteid: string) {
GlobalModel.submitCommand("remote", "disconnect", null, { nohist: "1", remote: remoteid }, true);
}
installRemote(remoteid: string) {
GlobalModel.submitCommand("remote", "install", null, { nohist: "1", remote: remoteid }, true);
}
installCancelRemote(remoteid: string) {
GlobalModel.submitCommand("remote", "installcancel", null, { nohist: "1", remote: remoteid }, true);
}
createRemote(cname: string, kwargsArg: Record<string, string>, interactive: boolean): Promise<CommandRtnType> {
let kwargs = Object.assign({}, kwargsArg);
kwargs["nohist"] = "1";
return GlobalModel.submitCommand("remote", "new", [cname], kwargs, interactive);
}
openCreateRemote(): void {
GlobalModel.submitCommand("remote", "new", null, { nohist: "1", visual: "1" }, true);
}
screenSetRemote(remoteArg: string, nohist: boolean, interactive: boolean): Promise<CommandRtnType> {
let kwargs = {};
if (nohist) {
kwargs["nohist"] = "1";
}
return GlobalModel.submitCommand("connect", null, [remoteArg], kwargs, interactive);
}
editRemote(remoteid: string, kwargsArg: Record<string, string>): void {
let kwargs = Object.assign({}, kwargsArg);
kwargs["nohist"] = "1";
kwargs["remote"] = remoteid;
GlobalModel.submitCommand("remote", "set", null, kwargs, true);
}
openEditRemote(remoteid: string): void {
GlobalModel.submitCommand("remote", "set", null, { remote: remoteid, nohist: "1", visual: "1" }, true);
}
archiveRemote(remoteid: string) {
GlobalModel.submitCommand("remote", "archive", null, { remote: remoteid, nohist: "1" }, true);
}
importSshConfig() {
GlobalModel.submitCommand("remote", "parse", null, { nohist: "1", visual: "1" }, true);
}
screenSelectLine(lineArg: string, focusVal?: string) {
let kwargs: Record<string, string> = {
nohist: "1",
line: lineArg,
};
if (focusVal != null) {
kwargs["focus"] = focusVal;
}
GlobalModel.submitCommand("screen", "set", null, kwargs, false);
}
screenReorder(screenId: string, index: string) {
let kwargs: Record<string, string> = {
nohist: "1",
screenId: screenId,
index: index,
};
GlobalModel.submitCommand("screen", "reorder", null, kwargs, false);
}
setTermUsedRows(termContext: RendererContext, height: number) {
let kwargs: Record<string, string> = {};
kwargs["screen"] = termContext.screenId;
kwargs["hohist"] = "1";
let posargs = [String(termContext.lineNum), String(height)];
GlobalModel.submitCommand("line", "setheight", posargs, kwargs, false);
}
screenSetAnchor(sessionId: string, screenId: string, anchorVal: string): void {
let kwargs = {
nohist: "1",
anchor: anchorVal,
session: sessionId,
screen: screenId,
};
GlobalModel.submitCommand("screen", "set", null, kwargs, false);
}
screenSetFocus(focusVal: string): void {
GlobalModel.submitCommand("screen", "set", null, { focus: focusVal, nohist: "1" }, false);
}
screenSetSettings(
screenId: string,
settings: { tabcolor?: string; tabicon?: string; name?: string; sharename?: string },
interactive: boolean
): Promise<CommandRtnType> {
let kwargs: { [key: string]: any } = Object.assign({}, settings);
kwargs["nohist"] = "1";
kwargs["screen"] = screenId;
return GlobalModel.submitCommand("screen", "set", null, kwargs, interactive);
}
sessionArchive(sessionId: string, shouldArchive: boolean): Promise<CommandRtnType> {
return GlobalModel.submitCommand(
"session",
"archive",
[sessionId, shouldArchive ? "1" : "0"],
{ nohist: "1" },
false
);
}
sessionDelete(sessionId: string): Promise<CommandRtnType> {
return GlobalModel.submitCommand("session", "delete", [sessionId], { nohist: "1" }, false);
}
sessionSetSettings(sessionId: string, settings: { name?: string }, interactive: boolean): Promise<CommandRtnType> {
let kwargs = Object.assign({}, settings);
kwargs["nohist"] = "1";
kwargs["session"] = sessionId;
return GlobalModel.submitCommand("session", "set", null, kwargs, interactive);
}
lineStar(lineId: string, starVal: number) {
GlobalModel.submitCommand("line", "star", [lineId, String(starVal)], { nohist: "1" }, true);
}
lineBookmark(lineId: string) {
GlobalModel.submitCommand("line", "bookmark", [lineId], { nohist: "1" }, true);
}
linePin(lineId: string, val: boolean) {
GlobalModel.submitCommand("line", "pin", [lineId, val ? "1" : "0"], { nohist: "1" }, true);
}
bookmarksView() {
GlobalModel.submitCommand("bookmarks", "show", null, { nohist: "1" }, true);
}
connectionsView() {
GlobalModel.connectionViewModel.showConnectionsView();
}
clientSettingsView() {
GlobalModel.clientSettingsViewModel.showClientSettingsView();
}
historyView(params: HistorySearchParams) {
let kwargs = { nohist: "1" };
kwargs["offset"] = String(params.offset);
kwargs["rawoffset"] = String(params.rawOffset);
if (params.searchText != null) {
kwargs["text"] = params.searchText;
}
if (params.searchSessionId != null) {
kwargs["searchsession"] = params.searchSessionId;
}
if (params.searchRemoteId != null) {
kwargs["searchremote"] = params.searchRemoteId;
}
if (params.fromTs != null) {
kwargs["fromts"] = String(params.fromTs);
}
if (params.noMeta) {
kwargs["meta"] = "0";
}
if (params.filterCmds) {
kwargs["filter"] = "1";
}
GlobalModel.submitCommand("history", "viewall", null, kwargs, true);
}
telemetryOff(interactive: boolean): Promise<CommandRtnType> {
return GlobalModel.submitCommand("telemetry", "off", null, { nohist: "1" }, interactive);
}
telemetryOn(interactive: boolean): Promise<CommandRtnType> {
return GlobalModel.submitCommand("telemetry", "on", null, { nohist: "1" }, interactive);
}
releaseCheckAutoOff(interactive: boolean): Promise<CommandRtnType> {
return GlobalModel.submitCommand("releasecheck", "autooff", null, { nohist: "1" }, interactive);
}
releaseCheckAutoOn(interactive: boolean): Promise<CommandRtnType> {
return GlobalModel.submitCommand("releasecheck", "autoon", null, { nohist: "1" }, interactive);
}
setTermFontSize(fsize: number, interactive: boolean): Promise<CommandRtnType> {
let kwargs = {
nohist: "1",
termfontsize: String(fsize),
};
return GlobalModel.submitCommand("client", "set", null, kwargs, interactive);
}
setClientOpenAISettings(opts: { model?: string; apitoken?: string; maxtokens?: string }): Promise<CommandRtnType> {
let kwargs = {
nohist: "1",
};
if (opts.model != null) {
kwargs["openaimodel"] = opts.model;
}
if (opts.apitoken != null) {
kwargs["openaiapitoken"] = opts.apitoken;
}
if (opts.maxtokens != null) {
kwargs["openaimaxtokens"] = opts.maxtokens;
}
return GlobalModel.submitCommand("client", "set", null, kwargs, false);
}
clientAcceptTos(): void {
GlobalModel.submitCommand("client", "accepttos", null, { nohist: "1" }, true);
}
clientSetConfirmFlag(flag: string, value: boolean): Promise<CommandRtnType> {
let kwargs = { nohist: "1" };
let valueStr = value ? "1" : "0";
return GlobalModel.submitCommand("client", "setconfirmflag", [flag, valueStr], kwargs, false);
}
clientSetSidebar(width: number, collapsed: boolean): Promise<CommandRtnType> {
let kwargs = { nohist: "1", width: `${width}`, collapsed: collapsed ? "1" : "0" };
return GlobalModel.submitCommand("client", "setsidebar", null, kwargs, false);
}
editBookmark(bookmarkId: string, desc: string, cmdstr: string) {
let kwargs = {
nohist: "1",
desc: desc,
cmdstr: cmdstr,
};
GlobalModel.submitCommand("bookmark", "set", [bookmarkId], kwargs, true);
}
deleteBookmark(bookmarkId: string): void {
GlobalModel.submitCommand("bookmark", "delete", [bookmarkId], { nohist: "1" }, true);
}
openSharedSession(): void {
GlobalModel.submitCommand("session", "openshared", null, { nohist: "1" }, true);
}
setLineState(
screenId: string,
lineId: string,
state: LineStateType,
interactive: boolean
): Promise<CommandRtnType> {
let stateStr = JSON.stringify(state);
return GlobalModel.submitCommand(
"line",
"set",
[lineId],
{ screen: screenId, nohist: "1", state: stateStr },
interactive
);
}
screenSidebarAddLine(lineId: string) {
GlobalModel.submitCommand("sidebar", "add", null, { nohist: "1", line: lineId }, false);
}
screenSidebarRemove() {
GlobalModel.submitCommand("sidebar", "remove", null, { nohist: "1" }, false);
}
screenSidebarClose(): void {
GlobalModel.submitCommand("sidebar", "close", null, { nohist: "1" }, false);
}
screenSidebarOpen(width?: string): void {
let kwargs: Record<string, string> = { nohist: "1" };
if (width != null) {
kwargs.width = width;
}
GlobalModel.submitCommand("sidebar", "open", null, kwargs, false);
}
}
export { CommandRunner };

View File

@ -0,0 +1,26 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as mobx from "mobx";
import { Model } from "./model";
class ConnectionsViewModel {
globalModel: Model;
constructor(globalModel: Model) {
this.globalModel = globalModel;
}
closeView(): void {
this.globalModel.showSessionView();
setTimeout(() => this.globalModel.inputModel.giveFocus(), 50);
}
showConnectionsView(): void {
mobx.action(() => {
this.globalModel.activeMainView.set("connections");
})();
}
}
export { ConnectionsViewModel };

View File

@ -0,0 +1,115 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { TermWrap } from "../plugins/terminal/term";
import * as types from "../types/types";
import { windowWidthToCols, windowHeightToRows } from "../util/textmeasure";
import { MagicLayout } from "../app/magiclayout";
import { Model } from "./model";
import { GlobalCommandRunner } from "./global";
import { Cmd } from "./cmd";
import { Screen } from "./screen";
class ForwardLineContainer {
globalModel: Model;
winSize: types.WindowSize;
screen: Screen;
containerType: types.LineContainerStrs;
lineId: string;
constructor(screen: Screen, winSize: types.WindowSize, containerType: types.LineContainerStrs, lineId: string) {
this.globalModel = Model.getInstance();
this.screen = screen;
this.winSize = winSize;
this.containerType = containerType;
this.lineId = lineId;
}
screenSizeCallback(winSize: types.WindowSize): void {
this.winSize = winSize;
let termWrap = this.getTermWrap(this.lineId);
if (termWrap != null) {
let fontSize = this.globalModel.termFontSize.get();
let cols = windowWidthToCols(winSize.width, fontSize);
let rows = windowHeightToRows(winSize.height, fontSize);
termWrap.resizeCols(cols);
GlobalCommandRunner.resizeScreen(this.screen.screenId, rows, cols, { include: [this.lineId] });
}
}
getContainerType(): types.LineContainerStrs {
return this.containerType;
}
getCmd(line: types.LineType): Cmd {
return this.screen.getCmd(line);
}
isSidebarOpen(): boolean {
return false;
}
isLineIdInSidebar(lineId: string): boolean {
return false;
}
setLineFocus(lineNum: number, focus: boolean): void {
this.screen.setLineFocus(lineNum, focus);
}
setContentHeight(context: types.RendererContext, height: number): void {
return;
}
getMaxContentSize(): types.WindowSize {
let rtn = { width: this.winSize.width, height: this.winSize.height };
rtn.width = rtn.width - MagicLayout.ScreenMaxContentWidthBuffer;
return rtn;
}
getIdealContentSize(): types.WindowSize {
return this.winSize;
}
loadTerminalRenderer(elem: Element, line: types.LineType, cmd: Cmd, width: number): void {
this.screen.loadTerminalRenderer(elem, line, cmd, width);
}
registerRenderer(lineId: string, renderer: types.RendererModel): void {
this.screen.registerRenderer(lineId, renderer);
}
unloadRenderer(lineId: string): void {
this.screen.unloadRenderer(lineId);
}
getContentHeight(context: types.RendererContext): number {
return this.screen.getContentHeight(context);
}
getUsedRows(context: types.RendererContext, line: types.LineType, cmd: Cmd, width: number): number {
return this.screen.getUsedRows(context, line, cmd, width);
}
getIsFocused(lineNum: number): boolean {
return this.screen.getIsFocused(lineNum);
}
getRenderer(lineId: string): types.RendererModel {
return this.screen.getRenderer(lineId);
}
getTermWrap(lineId: string): TermWrap {
return this.screen.getTermWrap(lineId);
}
getFocusType(): types.FocusTypeStrs {
return this.screen.getFocusType();
}
getSelectedLine(): number {
return this.screen.getSelectedLine();
}
}
export { ForwardLineContainer };

6
src/models/global.ts Normal file
View File

@ -0,0 +1,6 @@
import { Model } from "./model";
import { CommandRunner } from "./commandrunner";
const GlobalModel = Model.getInstance();
const GlobalCommandRunner = CommandRunner.getInstance();
export { GlobalModel, GlobalCommandRunner };

326
src/models/historyview.ts Normal file
View File

@ -0,0 +1,326 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { isBlank } from "../util/util";
import {
LineType,
HistoryItem,
CmdDataType,
HistoryViewDataType,
HistorySearchParams,
CommandRtnType,
} from "../types/types";
import { termWidthFromCols, termHeightFromRows } from "../util/textmeasure";
import dayjs from "dayjs";
import * as appconst from "../app/appconst";
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../util/keyutil";
import { OV, OArr, OMap } from "../types/types";
import { GlobalCommandRunner } from "./global";
import { Model } from "./model";
import { Cmd } from "./cmd";
import { SpecialLineContainer } from "./speciallinecontainer";
const HistoryPageSize = 50;
class HistoryViewModel {
globalModel: Model;
items: OArr<HistoryItem> = mobx.observable.array([], {
name: "HistoryItems",
});
hasMore: OV<boolean> = mobx.observable.box(false, {
name: "historyview-hasmore",
});
offset: OV<number> = mobx.observable.box(0, { name: "historyview-offset" });
searchText: OV<string> = mobx.observable.box("", {
name: "historyview-searchtext",
});
activeSearchText: string = null;
selectedItems: OMap<string, boolean> = mobx.observable.map({}, { name: "historyview-selectedItems" });
deleteActive: OV<boolean> = mobx.observable.box(false, {
name: "historyview-deleteActive",
});
activeItem: OV<string> = mobx.observable.box(null, {
name: "historyview-activeItem",
});
searchSessionId: OV<string> = mobx.observable.box(null, {
name: "historyview-searchSessionId",
});
searchRemoteId: OV<string> = mobx.observable.box(null, {
name: "historyview-searchRemoteId",
});
searchShowMeta: OV<boolean> = mobx.observable.box(true, {
name: "historyview-searchShowMeta",
});
searchFromDate: OV<string> = mobx.observable.box(null, {
name: "historyview-searchfromts",
});
searchFilterCmds: OV<boolean> = mobx.observable.box(true, {
name: "historyview-filtercmds",
});
nextRawOffset: number = 0;
curRawOffset: number = 0;
historyItemLines: LineType[] = [];
historyItemCmds: CmdDataType[] = [];
specialLineContainer: SpecialLineContainer;
constructor(globalModel: Model) {
this.globalModel = globalModel;
}
closeView(): void {
this.globalModel.showSessionView();
setTimeout(() => this.globalModel.inputModel.giveFocus(), 50);
}
getLineById(lineId: string): LineType {
if (isBlank(lineId)) {
return null;
}
for (const line of this.historyItemLines) {
if (line.lineid == lineId) {
return line;
}
}
return null;
}
getCmdById(lineId: string): Cmd {
if (isBlank(lineId)) {
return null;
}
for (const cmd of this.historyItemCmds) {
if (cmd.lineid == lineId) {
return new Cmd(cmd);
}
}
return null;
}
getHistoryItemById(historyId: string): HistoryItem {
if (isBlank(historyId)) {
return null;
}
for (const hitem of this.items) {
if (hitem.historyid == historyId) {
return hitem;
}
}
return null;
}
setActiveItem(historyId: string) {
if (this.activeItem.get() == historyId) {
return;
}
let hitem = this.getHistoryItemById(historyId);
mobx.action(() => {
if (hitem == null) {
this.activeItem.set(null);
this.specialLineContainer = null;
} else {
this.activeItem.set(hitem.historyid);
let width = termWidthFromCols(80, this.globalModel.termFontSize.get());
let height = termHeightFromRows(25, this.globalModel.termFontSize.get());
this.specialLineContainer = new SpecialLineContainer(
this,
{ width, height },
false,
appconst.LineContainer_History
);
}
})();
}
doSelectedDelete(): void {
if (!this.deleteActive.get()) {
mobx.action(() => {
this.deleteActive.set(true);
})();
setTimeout(this.clearActiveDelete, 2000);
return;
}
let prtn = this.globalModel.showAlert({
message: "Deleting lines from history also deletes their content from your workspaces.",
confirm: true,
});
prtn.then((result) => {
if (!result) {
return;
}
if (result) {
this._deleteSelected();
}
});
}
_deleteSelected(): void {
let lineIds = Array.from(this.selectedItems.keys());
let prtn = GlobalCommandRunner.historyPurgeLines(lineIds);
prtn.then((result: CommandRtnType) => {
if (!result.success) {
this.globalModel.showAlert({ message: "Error removing history lines." });
}
});
let params = this._getSearchParams();
GlobalCommandRunner.historyView(params);
}
@boundMethod
clearActiveDelete(): void {
mobx.action(() => {
this.deleteActive.set(false);
})();
}
_getSearchParams(newOffset?: number, newRawOffset?: number): HistorySearchParams {
let offset = newOffset ?? this.offset.get();
let rawOffset = newRawOffset ?? this.curRawOffset;
let opts: HistorySearchParams = {
offset: offset,
rawOffset: rawOffset,
searchText: this.activeSearchText,
searchSessionId: this.searchSessionId.get(),
searchRemoteId: this.searchRemoteId.get(),
};
if (!this.searchShowMeta.get()) {
opts.noMeta = true;
}
if (this.searchFromDate.get() != null) {
let fromDate = this.searchFromDate.get();
let fromTs = dayjs(fromDate, "YYYY-MM-DD").valueOf();
let d = new Date(fromTs);
d.setDate(d.getDate() + 1);
let ts = d.getTime() - 1;
opts.fromTs = ts;
}
if (this.searchFilterCmds.get()) {
opts.filterCmds = true;
}
return opts;
}
reSearch(): void {
this.setActiveItem(null);
GlobalCommandRunner.historyView(this._getSearchParams());
}
resetAllFilters(): void {
mobx.action(() => {
this.activeSearchText = "";
this.searchText.set("");
this.searchSessionId.set(null);
this.searchRemoteId.set(null);
this.searchFromDate.set(null);
this.searchShowMeta.set(true);
this.searchFilterCmds.set(true);
})();
GlobalCommandRunner.historyView(this._getSearchParams(0, 0));
}
setFromDate(fromDate: string): void {
if (this.searchFromDate.get() == fromDate) {
return;
}
mobx.action(() => {
this.searchFromDate.set(fromDate);
})();
GlobalCommandRunner.historyView(this._getSearchParams(0, 0));
}
setSearchFilterCmds(filter: boolean): void {
if (this.searchFilterCmds.get() == filter) {
return;
}
mobx.action(() => {
this.searchFilterCmds.set(filter);
})();
GlobalCommandRunner.historyView(this._getSearchParams(0, 0));
}
setSearchShowMeta(show: boolean): void {
if (this.searchShowMeta.get() == show) {
return;
}
mobx.action(() => {
this.searchShowMeta.set(show);
})();
GlobalCommandRunner.historyView(this._getSearchParams(0, 0));
}
setSearchSessionId(sessionId: string): void {
if (this.searchSessionId.get() == sessionId) {
return;
}
mobx.action(() => {
this.searchSessionId.set(sessionId);
})();
GlobalCommandRunner.historyView(this._getSearchParams(0, 0));
}
setSearchRemoteId(remoteId: string): void {
if (this.searchRemoteId.get() == remoteId) {
return;
}
mobx.action(() => {
this.searchRemoteId.set(remoteId);
})();
GlobalCommandRunner.historyView(this._getSearchParams(0, 0));
}
goPrev(): void {
let offset = this.offset.get();
offset = offset - HistoryPageSize;
if (offset < 0) {
offset = 0;
}
let params = this._getSearchParams(offset, 0);
GlobalCommandRunner.historyView(params);
}
goNext(): void {
let offset = this.offset.get();
offset += HistoryPageSize;
let params = this._getSearchParams(offset, this.nextRawOffset ?? 0);
GlobalCommandRunner.historyView(params);
}
submitSearch(): void {
mobx.action(() => {
this.hasMore.set(false);
this.items.replace([]);
this.activeSearchText = this.searchText.get();
this.historyItemLines = [];
this.historyItemCmds = [];
})();
GlobalCommandRunner.historyView(this._getSearchParams(0, 0));
}
handleDocKeyDown(e: any): void {
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
if (checkKeyPressed(waveEvent, "Escape")) {
e.preventDefault();
this.closeView();
return;
}
}
showHistoryView(data: HistoryViewDataType): void {
mobx.action(() => {
this.globalModel.activeMainView.set("history");
this.hasMore.set(data.hasmore);
this.items.replace(data.items || []);
this.offset.set(data.offset);
this.nextRawOffset = data.nextrawoffset;
this.curRawOffset = data.rawoffset;
this.historyItemLines = data.lines ?? [];
this.historyItemCmds = data.cmds ?? [];
this.selectedItems.clear();
})();
}
}
export { HistoryViewModel };

16
src/models/index.ts Normal file
View File

@ -0,0 +1,16 @@
export * from "./global";
export * from "./model";
export { BookmarksModel } from "./bookmarks";
export { ClientSettingsViewModel } from "./clientsettingsview";
export { Cmd } from "./cmd";
export { ConnectionsViewModel } from "./connectionsview";
export { InputModel } from "./input";
export { MainSidebarModel } from "./mainsidebar";
export { ModalsModel } from "./modals";
export { PluginsModel } from "./plugins";
export { RemotesModel } from "./remotes";
export { Screen } from "./screen";
export { ScreenLines } from "./screenlines";
export { Session } from "./session";
export { SpecialLineContainer } from "./speciallinecontainer";
export { ForwardLineContainer } from "./forwardlinecontainer";

798
src/models/input.ts Normal file
View File

@ -0,0 +1,798 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import type React from "react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { isBlank } from "../util/util";
import {
HistoryItem,
RemotePtrType,
InfoType,
HistoryInfoType,
HistoryQueryOpts,
HistoryTypeStrs,
OpenAICmdInfoChatMessageType,
OV,
StrWithPos,
} from "../types/types";
import * as appconst from "../app/appconst";
import { Model } from "./model";
import { GlobalCommandRunner } from "./global";
function getDefaultHistoryQueryOpts(): HistoryQueryOpts {
return {
queryType: "screen",
limitRemote: true,
limitRemoteInstance: true,
limitUser: true,
queryStr: "",
maxItems: 10000,
includeMeta: true,
fromTs: 0,
};
}
class InputModel {
globalModel: Model;
historyShow: OV<boolean> = mobx.observable.box(false);
infoShow: OV<boolean> = mobx.observable.box(false);
aIChatShow: OV<boolean> = mobx.observable.box(false);
cmdInputHeight: OV<number> = mobx.observable.box(0);
aiChatTextAreaRef: React.RefObject<HTMLTextAreaElement>;
aiChatWindowRef: React.RefObject<HTMLDivElement>;
codeSelectBlockRefArray: Array<React.RefObject<HTMLElement>>;
codeSelectSelectedIndex: OV<number> = mobx.observable.box(-1);
AICmdInfoChatItems: mobx.IObservableArray<OpenAICmdInfoChatMessageType> = mobx.observable.array([], {
name: "aicmdinfo-chat",
});
readonly codeSelectTop: number = -2;
readonly codeSelectBottom: number = -1;
historyType: mobx.IObservableValue<HistoryTypeStrs> = mobx.observable.box("screen");
historyLoading: mobx.IObservableValue<boolean> = mobx.observable.box(false);
historyAfterLoadIndex: number = 0;
historyItems: mobx.IObservableValue<HistoryItem[]> = mobx.observable.box(null, {
name: "history-items",
deep: false,
}); // sorted in reverse (most recent is index 0)
filteredHistoryItems: mobx.IComputedValue<HistoryItem[]> = null;
historyIndex: mobx.IObservableValue<number> = mobx.observable.box(0, {
name: "history-index",
}); // 1-indexed (because 0 is current)
modHistory: mobx.IObservableArray<string> = mobx.observable.array([""], {
name: "mod-history",
});
historyQueryOpts: OV<HistoryQueryOpts> = mobx.observable.box(getDefaultHistoryQueryOpts());
infoMsg: OV<InfoType> = mobx.observable.box(null);
infoTimeoutId: any = null;
inputMode: OV<null | "comment" | "global"> = mobx.observable.box(null);
inputExpanded: OV<boolean> = mobx.observable.box(false, {
name: "inputExpanded",
});
// cursor
forceCursorPos: OV<number> = mobx.observable.box(null);
// focus
inputFocused: OV<boolean> = mobx.observable.box(false);
lineFocused: OV<boolean> = mobx.observable.box(false);
physicalInputFocused: OV<boolean> = mobx.observable.box(false);
forceInputFocus: boolean = false;
constructor(globalModel: Model) {
this.globalModel = globalModel;
this.filteredHistoryItems = mobx.computed(() => {
return this._getFilteredHistoryItems();
});
mobx.action(() => {
this.codeSelectSelectedIndex.set(-1);
this.codeSelectBlockRefArray = [];
})();
}
setInputMode(inputMode: null | "comment" | "global"): void {
mobx.action(() => {
this.inputMode.set(inputMode);
})();
}
onInputFocus(isFocused: boolean): void {
mobx.action(() => {
if (isFocused) {
this.inputFocused.set(true);
this.lineFocused.set(false);
} else if (this.inputFocused.get()) {
this.inputFocused.set(false);
}
})();
}
onLineFocus(isFocused: boolean): void {
mobx.action(() => {
if (isFocused) {
this.inputFocused.set(false);
this.lineFocused.set(true);
} else if (this.lineFocused.get()) {
this.lineFocused.set(false);
}
})();
}
_focusCmdInput(): void {
let elem = document.getElementById("main-cmd-input");
if (elem != null) {
elem.focus();
}
}
_focusHistoryInput(): void {
let elem: HTMLElement = document.querySelector(".cmd-input input.history-input");
if (elem != null) {
elem.focus();
}
}
giveFocus(): void {
if (this.historyShow.get()) {
this._focusHistoryInput();
} else {
this._focusCmdInput();
}
}
setPhysicalInputFocused(isFocused: boolean): void {
mobx.action(() => {
this.physicalInputFocused.set(isFocused);
})();
if (isFocused) {
let screen = this.globalModel.getActiveScreen();
if (screen != null) {
if (screen.focusType.get() != "input") {
GlobalCommandRunner.screenSetFocus("input");
}
}
}
}
hasFocus(): boolean {
let mainInputElem = document.getElementById("main-cmd-input");
if (document.activeElement == mainInputElem) {
return true;
}
let historyInputElem = document.querySelector(".cmd-input input.history-input");
if (document.activeElement == historyInputElem) {
return true;
}
return false;
}
setHistoryType(htype: HistoryTypeStrs): void {
if (this.historyQueryOpts.get().queryType == htype) {
return;
}
this.loadHistory(true, -1, htype);
}
findBestNewIndex(oldItem: HistoryItem): number {
if (oldItem == null) {
return 0;
}
let newItems = this.getFilteredHistoryItems();
if (newItems.length == 0) {
return 0;
}
let bestIdx = 0;
for (let i = 0; i < newItems.length; i++) {
// still start at i=0 to catch the historynum equality case
let item = newItems[i];
if (item.historynum == oldItem.historynum) {
bestIdx = i;
break;
}
let bestTsDiff = Math.abs(item.ts - newItems[bestIdx].ts);
let curTsDiff = Math.abs(item.ts - oldItem.ts);
if (curTsDiff < bestTsDiff) {
bestIdx = i;
}
}
return bestIdx + 1;
}
setHistoryQueryOpts(opts: HistoryQueryOpts): void {
mobx.action(() => {
let oldItem = this.getHistorySelectedItem();
this.historyQueryOpts.set(opts);
let bestIndex = this.findBestNewIndex(oldItem);
setTimeout(() => this.setHistoryIndex(bestIndex, true), 10);
})();
}
setOpenAICmdInfoChat(chat: OpenAICmdInfoChatMessageType[]): void {
this.AICmdInfoChatItems.replace(chat);
this.codeSelectBlockRefArray = [];
}
setHistoryShow(show: boolean): void {
if (this.historyShow.get() == show) {
return;
}
mobx.action(() => {
this.historyShow.set(show);
if (this.hasFocus()) {
this.giveFocus();
}
})();
}
isHistoryLoaded(): boolean {
if (this.historyLoading.get()) {
return false;
}
let hitems = this.historyItems.get();
return hitems != null;
}
loadHistory(show: boolean, afterLoadIndex: number, htype: HistoryTypeStrs) {
if (this.historyLoading.get()) {
return;
}
if (this.isHistoryLoaded()) {
if (this.historyQueryOpts.get().queryType == htype) {
return;
}
}
this.historyAfterLoadIndex = afterLoadIndex;
mobx.action(() => {
this.historyLoading.set(true);
})();
GlobalCommandRunner.loadHistory(show, htype);
}
openHistory(): void {
if (this.historyLoading.get()) {
return;
}
if (!this.isHistoryLoaded()) {
this.loadHistory(true, 0, "screen");
return;
}
if (!this.historyShow.get()) {
mobx.action(() => {
this.setHistoryShow(true);
this.infoShow.set(false);
this.dropModHistory(true);
this.giveFocus();
})();
}
}
updateCmdLine(cmdLine: StrWithPos): void {
mobx.action(() => {
this.setCurLine(cmdLine.str);
if (cmdLine.pos != appconst.NoStrPos) {
this.forceCursorPos.set(cmdLine.pos);
}
})();
}
getHistorySelectedItem(): HistoryItem {
let hidx = this.historyIndex.get();
if (hidx == 0) {
return null;
}
let hitems = this.getFilteredHistoryItems();
if (hidx > hitems.length) {
return null;
}
return hitems[hidx - 1];
}
getFirstHistoryItem(): HistoryItem {
let hitems = this.getFilteredHistoryItems();
if (hitems.length == 0) {
return null;
}
return hitems[0];
}
setHistorySelectionNum(hnum: string): void {
let hitems = this.getFilteredHistoryItems();
for (let i = 0; i < hitems.length; i++) {
if (hitems[i].historynum == hnum) {
this.setHistoryIndex(i + 1);
return;
}
}
}
setHistoryInfo(hinfo: HistoryInfoType): void {
mobx.action(() => {
let oldItem = this.getHistorySelectedItem();
let hitems: HistoryItem[] = hinfo.items ?? [];
this.historyItems.set(hitems);
this.historyLoading.set(false);
this.historyQueryOpts.get().queryType = hinfo.historytype;
if (hinfo.historytype == "session" || hinfo.historytype == "global") {
this.historyQueryOpts.get().limitRemote = false;
this.historyQueryOpts.get().limitRemoteInstance = false;
}
if (this.historyAfterLoadIndex == -1) {
let bestIndex = this.findBestNewIndex(oldItem);
setTimeout(() => this.setHistoryIndex(bestIndex, true), 100);
} else if (this.historyAfterLoadIndex) {
if (hitems.length >= this.historyAfterLoadIndex) {
this.setHistoryIndex(this.historyAfterLoadIndex);
}
}
this.historyAfterLoadIndex = 0;
if (hinfo.show) {
this.openHistory();
}
})();
}
getFilteredHistoryItems(): HistoryItem[] {
return this.filteredHistoryItems.get();
}
_getFilteredHistoryItems(): HistoryItem[] {
let hitems: HistoryItem[] = this.historyItems.get() ?? [];
let rtn: HistoryItem[] = [];
let opts = mobx.toJS(this.historyQueryOpts.get());
let ctx = this.globalModel.getUIContext();
let curRemote: RemotePtrType = ctx.remote;
if (curRemote == null) {
curRemote = { ownerid: "", name: "", remoteid: "" };
}
curRemote = mobx.toJS(curRemote);
for (const hitem of hitems) {
if (hitem.ismetacmd) {
if (!opts.includeMeta) {
continue;
}
} else if (opts.limitRemoteInstance) {
if (hitem.remote == null || isBlank(hitem.remote.remoteid)) {
continue;
}
if (
(curRemote.ownerid ?? "") != (hitem.remote.ownerid ?? "") ||
(curRemote.remoteid ?? "") != (hitem.remote.remoteid ?? "") ||
(curRemote.name ?? "") != (hitem.remote.name ?? "")
) {
continue;
}
} else if (opts.limitRemote) {
if (hitem.remote == null || isBlank(hitem.remote.remoteid)) {
continue;
}
if (
(curRemote.ownerid ?? "") != (hitem.remote.ownerid ?? "") ||
(curRemote.remoteid ?? "") != (hitem.remote.remoteid ?? "")
) {
continue;
}
}
if (!isBlank(opts.queryStr)) {
if (isBlank(hitem.cmdstr)) {
continue;
}
let idx = hitem.cmdstr.indexOf(opts.queryStr);
if (idx == -1) {
continue;
}
}
rtn.push(hitem);
}
return rtn;
}
scrollHistoryItemIntoView(hnum: string): void {
let elem: HTMLElement = document.querySelector(".cmd-history .hnum-" + hnum);
if (elem == null) {
return;
}
let historyDiv = elem.closest(".cmd-history");
if (historyDiv == null) {
return;
}
let buffer = 15;
let titleHeight = 24;
let titleDiv: HTMLElement = document.querySelector(".cmd-history .history-title");
if (titleDiv != null) {
titleHeight = titleDiv.offsetHeight + 2;
}
let elemOffset = elem.offsetTop;
let elemHeight = elem.clientHeight;
let topPos = historyDiv.scrollTop;
let endPos = topPos + historyDiv.clientHeight;
if (elemOffset + elemHeight + buffer > endPos) {
if (elemHeight + buffer > historyDiv.clientHeight - titleHeight) {
historyDiv.scrollTop = elemOffset - titleHeight;
return;
}
historyDiv.scrollTop = elemOffset - historyDiv.clientHeight + elemHeight + buffer;
return;
}
if (elemOffset < topPos + titleHeight) {
if (elemHeight + buffer > historyDiv.clientHeight - titleHeight) {
historyDiv.scrollTop = elemOffset - titleHeight;
return;
}
historyDiv.scrollTop = elemOffset - titleHeight - buffer;
}
}
grabSelectedHistoryItem(): void {
let hitem = this.getHistorySelectedItem();
if (hitem == null) {
this.resetHistory();
return;
}
mobx.action(() => {
this.resetInput();
this.setCurLine(hitem.cmdstr);
})();
}
setHistoryIndex(hidx: number, force?: boolean): void {
if (hidx < 0) {
return;
}
if (!force && this.historyIndex.get() == hidx) {
return;
}
mobx.action(() => {
this.historyIndex.set(hidx);
if (this.historyShow.get()) {
let hitem = this.getHistorySelectedItem();
if (hitem == null) {
hitem = this.getFirstHistoryItem();
}
if (hitem != null) {
this.scrollHistoryItemIntoView(hitem.historynum);
}
}
})();
}
moveHistorySelection(amt: number): void {
if (amt == 0) {
return;
}
if (!this.isHistoryLoaded()) {
return;
}
let hitems = this.getFilteredHistoryItems();
let idx = this.historyIndex.get();
idx += amt;
if (idx < 0) {
idx = 0;
}
if (idx > hitems.length) {
idx = hitems.length;
}
this.setHistoryIndex(idx);
}
flashInfoMsg(info: InfoType, timeoutMs: number): void {
this._clearInfoTimeout();
mobx.action(() => {
this.infoMsg.set(info);
if (info == null) {
this.infoShow.set(false);
} else {
this.infoShow.set(true);
this.setHistoryShow(false);
}
})();
if (info != null && timeoutMs) {
this.infoTimeoutId = setTimeout(() => {
if (this.historyShow.get()) {
return;
}
this.clearInfoMsg(false);
}, timeoutMs);
}
}
setCmdInfoChatRefs(
textAreaRef: React.RefObject<HTMLTextAreaElement>,
chatWindowRef: React.RefObject<HTMLDivElement>
) {
this.aiChatTextAreaRef = textAreaRef;
this.aiChatWindowRef = chatWindowRef;
}
setAIChatFocus() {
if (this.aiChatTextAreaRef?.current != null) {
this.aiChatTextAreaRef.current.focus();
}
}
grabCodeSelectSelection() {
if (
this.codeSelectSelectedIndex.get() >= 0 &&
this.codeSelectSelectedIndex.get() < this.codeSelectBlockRefArray.length
) {
let curBlockRef = this.codeSelectBlockRefArray[this.codeSelectSelectedIndex.get()];
let codeText = curBlockRef.current.innerText;
codeText = codeText.replace(/\n$/, ""); // remove trailing newline
this.setCurLine(codeText);
this.giveFocus();
}
}
addCodeBlockToCodeSelect(blockRef: React.RefObject<HTMLElement>): number {
let rtn = -1;
rtn = this.codeSelectBlockRefArray.length;
this.codeSelectBlockRefArray.push(blockRef);
return rtn;
}
setCodeSelectSelectedCodeBlock(blockIndex: number) {
mobx.action(() => {
if (blockIndex >= 0 && blockIndex < this.codeSelectBlockRefArray.length) {
this.codeSelectSelectedIndex.set(blockIndex);
let currentRef = this.codeSelectBlockRefArray[blockIndex].current;
if (currentRef != null) {
if (this.aiChatWindowRef?.current != null) {
let chatWindowTop = this.aiChatWindowRef.current.scrollTop;
let chatWindowBottom = chatWindowTop + this.aiChatWindowRef.current.clientHeight - 100;
let elemTop = currentRef.offsetTop;
let elemBottom = elemTop - currentRef.offsetHeight;
let elementIsInView = elemBottom < chatWindowBottom && elemTop > chatWindowTop;
if (!elementIsInView) {
this.aiChatWindowRef.current.scrollTop =
elemBottom - this.aiChatWindowRef.current.clientHeight / 3;
}
}
}
this.codeSelectBlockRefArray = [];
this.setAIChatFocus();
}
})();
}
codeSelectSelectNextNewestCodeBlock() {
// oldest code block = index 0 in array
// this decrements codeSelectSelected index
mobx.action(() => {
if (this.codeSelectSelectedIndex.get() == this.codeSelectTop) {
this.codeSelectSelectedIndex.set(this.codeSelectBottom);
} else if (this.codeSelectSelectedIndex.get() == this.codeSelectBottom) {
return;
}
let incBlockIndex = this.codeSelectSelectedIndex.get() + 1;
if (this.codeSelectSelectedIndex.get() == this.codeSelectBlockRefArray.length - 1) {
this.codeSelectDeselectAll();
if (this.aiChatWindowRef?.current != null) {
this.aiChatWindowRef.current.scrollTop = this.aiChatWindowRef.current.scrollHeight;
}
}
if (incBlockIndex >= 0 && incBlockIndex < this.codeSelectBlockRefArray.length) {
this.setCodeSelectSelectedCodeBlock(incBlockIndex);
}
})();
}
codeSelectSelectNextOldestCodeBlock() {
mobx.action(() => {
if (this.codeSelectSelectedIndex.get() == this.codeSelectBottom) {
if (this.codeSelectBlockRefArray.length > 0) {
this.codeSelectSelectedIndex.set(this.codeSelectBlockRefArray.length);
} else {
return;
}
} else if (this.codeSelectSelectedIndex.get() == this.codeSelectTop) {
return;
}
let decBlockIndex = this.codeSelectSelectedIndex.get() - 1;
if (decBlockIndex < 0) {
this.codeSelectDeselectAll(this.codeSelectTop);
if (this.aiChatWindowRef?.current != null) {
this.aiChatWindowRef.current.scrollTop = 0;
}
}
if (decBlockIndex >= 0 && decBlockIndex < this.codeSelectBlockRefArray.length) {
this.setCodeSelectSelectedCodeBlock(decBlockIndex);
}
})();
}
getCodeSelectSelectedIndex() {
return this.codeSelectSelectedIndex.get();
}
getCodeSelectRefArrayLength() {
return this.codeSelectBlockRefArray.length;
}
codeBlockIsSelected(blockIndex: number): boolean {
return blockIndex == this.codeSelectSelectedIndex.get();
}
codeSelectDeselectAll(direction: number = this.codeSelectBottom) {
mobx.action(() => {
this.codeSelectSelectedIndex.set(direction);
this.codeSelectBlockRefArray = [];
})();
}
openAIAssistantChat(): void {
mobx.action(() => {
this.aIChatShow.set(true);
this.setAIChatFocus();
})();
}
closeAIAssistantChat(): void {
mobx.action(() => {
this.aIChatShow.set(false);
this.giveFocus();
})();
}
clearAIAssistantChat(): void {
let prtn = this.globalModel.submitChatInfoCommand("", "", true);
prtn.then((rtn) => {
if (!rtn.success) {
console.log("submit chat command error: " + rtn.error);
}
}).catch((error) => {
console.log("submit chat command error: ", error);
});
}
hasScrollingInfoMsg(): boolean {
if (!this.infoShow.get()) {
return false;
}
let info = this.infoMsg.get();
if (info == null) {
return false;
}
let div = document.querySelector(".cmd-input-info");
if (div == null) {
return false;
}
return div.scrollHeight > div.clientHeight;
}
_clearInfoTimeout(): void {
if (this.infoTimeoutId != null) {
clearTimeout(this.infoTimeoutId);
this.infoTimeoutId = null;
}
}
clearInfoMsg(setNull: boolean): void {
this._clearInfoTimeout();
mobx.action(() => {
this.setHistoryShow(false);
this.infoShow.set(false);
if (setNull) {
this.infoMsg.set(null);
}
})();
}
toggleInfoMsg(): void {
this._clearInfoTimeout();
mobx.action(() => {
if (this.historyShow.get()) {
this.setHistoryShow(false);
return;
}
let isShowing = this.infoShow.get();
if (isShowing) {
this.infoShow.set(false);
} else {
if (this.infoMsg.get() != null) {
this.infoShow.set(true);
}
}
})();
}
@boundMethod
uiSubmitCommand(): void {
mobx.action(() => {
let commandStr = this.getCurLine();
if (commandStr.trim() == "") {
return;
}
this.resetInput();
this.globalModel.submitRawCommand(commandStr, true, true);
})();
}
isEmpty(): boolean {
return this.getCurLine().trim() == "";
}
resetInputMode(): void {
mobx.action(() => {
this.setInputMode(null);
this.setCurLine("");
})();
}
setCurLine(val: string): void {
let hidx = this.historyIndex.get();
mobx.action(() => {
if (this.modHistory.length <= hidx) {
this.modHistory.length = hidx + 1;
}
this.modHistory[hidx] = val;
})();
}
resetInput(): void {
mobx.action(() => {
this.setHistoryShow(false);
this.closeAIAssistantChat();
this.infoShow.set(false);
this.inputMode.set(null);
this.resetHistory();
this.dropModHistory(false);
this.infoMsg.set(null);
this.inputExpanded.set(false);
this._clearInfoTimeout();
})();
}
@boundMethod
toggleExpandInput(): void {
mobx.action(() => {
this.inputExpanded.set(!this.inputExpanded.get());
this.forceInputFocus = true;
})();
}
getCurLine(): string {
let hidx = this.historyIndex.get();
if (hidx < this.modHistory.length && this.modHistory[hidx] != null) {
return this.modHistory[hidx];
}
let hitems = this.getFilteredHistoryItems();
if (hidx == 0 || hitems == null || hidx > hitems.length) {
return "";
}
let hitem = hitems[hidx - 1];
if (hitem == null) {
return "";
}
return hitem.cmdstr;
}
dropModHistory(keepLine0: boolean): void {
mobx.action(() => {
if (keepLine0) {
if (this.modHistory.length > 1) {
this.modHistory.splice(1, this.modHistory.length - 1);
}
} else {
this.modHistory.replace([""]);
}
})();
}
resetHistory(): void {
mobx.action(() => {
this.setHistoryShow(false);
this.historyLoading.set(false);
this.historyType.set("screen");
this.historyItems.set(null);
this.historyIndex.set(0);
this.historyQueryOpts.set(getDefaultHistoryQueryOpts());
this.historyAfterLoadIndex = 0;
this.dropModHistory(true);
})();
}
}
export { InputModel };

85
src/models/mainsidebar.ts Normal file
View File

@ -0,0 +1,85 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as mobx from "mobx";
import { MagicLayout } from "../app/magiclayout";
import { OV } from "../types/types";
import { Model } from "./model";
class MainSidebarModel {
globalModel: Model = null;
tempWidth: OV<number> = mobx.observable.box(null, {
name: "MainSidebarModel-tempWidth",
});
tempCollapsed: OV<boolean> = mobx.observable.box(null, {
name: "MainSidebarModel-tempCollapsed",
});
isDragging: OV<boolean> = mobx.observable.box(false, {
name: "MainSidebarModel-isDragging",
});
constructor(globalModel: Model) {
this.globalModel = globalModel;
}
setTempWidthAndTempCollapsed(newWidth: number, newCollapsed: boolean): void {
const width = Math.max(MagicLayout.MainSidebarMinWidth, Math.min(newWidth, MagicLayout.MainSidebarMaxWidth));
mobx.action(() => {
this.tempWidth.set(width);
this.tempCollapsed.set(newCollapsed);
})();
}
/**
* Gets the intended width for the sidebar. If the sidebar is being dragged, returns the tempWidth. If the sidebar is collapsed, returns the default width.
* @param ignoreCollapse If true, returns the persisted width even if the sidebar is collapsed.
* @returns The intended width for the sidebar or the default width if the sidebar is collapsed. Can be overridden using ignoreCollapse.
*/
getWidth(ignoreCollapse: boolean = false): number {
const clientData = this.globalModel.clientData.get();
let width = clientData?.clientopts?.mainsidebar?.width ?? MagicLayout.MainSidebarDefaultWidth;
if (this.isDragging.get()) {
if (this.tempWidth.get() == null && width == null) {
return MagicLayout.MainSidebarDefaultWidth;
}
if (this.tempWidth.get() == null) {
return width;
}
return this.tempWidth.get();
}
// Set by CLI and collapsed
if (this.getCollapsed()) {
if (ignoreCollapse) {
return width;
} else {
return MagicLayout.MainSidebarMinWidth;
}
} else {
if (width <= MagicLayout.MainSidebarMinWidth) {
width = MagicLayout.MainSidebarDefaultWidth;
}
const snapPoint = MagicLayout.MainSidebarMinWidth + MagicLayout.MainSidebarSnapThreshold;
if (width < snapPoint || width > MagicLayout.MainSidebarMaxWidth) {
width = MagicLayout.MainSidebarDefaultWidth;
}
}
return width;
}
getCollapsed(): boolean {
const clientData = this.globalModel.clientData.get();
const collapsed = clientData?.clientopts?.mainsidebar?.collapsed;
if (this.isDragging.get()) {
if (this.tempCollapsed.get() == null && collapsed == null) {
return false;
}
if (this.tempCollapsed.get() == null) {
return collapsed;
}
return this.tempCollapsed.get();
}
return collapsed;
}
}
export { MainSidebarModel };

30
src/models/modals.ts Normal file
View File

@ -0,0 +1,30 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as mobx from "mobx";
import { v4 as uuidv4 } from "uuid";
import { ModalStoreEntry } from "../types/types";
import { modalsRegistry } from "../app/common/modals/registry";
import { OArr } from "../types/types";
class ModalsModel {
store: OArr<ModalStoreEntry> = mobx.observable.array([], { name: "ModalsModel-store", deep: false });
pushModal(modalId: string, props?: any) {
const modalFactory = modalsRegistry[modalId];
if (modalFactory && !this.store.some((modal) => modal.id === modalId)) {
mobx.action(() => {
this.store.push({ id: modalId, component: modalFactory, uniqueKey: uuidv4(), props });
})();
}
}
popModal() {
mobx.action(() => {
this.store.pop();
})();
}
}
export { ModalsModel };

1477
src/models/model.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -51,6 +51,8 @@ import {
HistoryViewDataType,
AlertMessageType,
HistorySearchParams,
UserInputRequest,
UserInputResponsePacket,
FocusTypeStrs,
ScreenLinesType,
HistoryTypeStrs,
@ -3376,14 +3378,14 @@ class RemotesModel {
}
class ModalsModel {
store: OArr<T.ModalStoreEntry> = mobx.observable.array([], { name: "ModalsModel-store" });
store: OArr<T.ModalStoreEntry> = mobx.observable.array([], { name: "ModalsModel-store", deep: false });
pushModal(modalId: string) {
pushModal(modalId: string, props?: any) {
const modalFactory = modalsRegistry[modalId];
if (modalFactory && !this.store.some((modal) => modal.id === modalId)) {
mobx.action(() => {
this.store.push({ id: modalId, component: modalFactory, uniqueKey: uuidv4() });
this.store.push({ id: modalId, component: modalFactory, uniqueKey: uuidv4(), props: props });
})();
}
}
@ -4184,6 +4186,10 @@ class Model {
this.getScreenById_single(snc.screenid)?.setNumRunningCmds(snc.num);
}
}
if ("userinputrequest" in update) {
let userInputRequest: UserInputRequest = update.userinputrequest;
this.modalsModel.pushModal(appconst.USER_INPUT, userInputRequest);
}
}
updateRemotes(remotes: RemoteType[]): void {
@ -4591,6 +4597,10 @@ class Model {
this.ws.pushMessage(inputPacket);
}
sendUserInput(userInputResponsePacket: UserInputResponsePacket) {
this.ws.pushMessage(userInputResponsePacket);
}
sendCmdInputText(screenId: string, sp: T.StrWithPos) {
let pk: T.CmdInputTextPacketType = {
type: "cmdinputtext",

47
src/models/plugins.ts Normal file
View File

@ -0,0 +1,47 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as mobx from "mobx";
import { PluginModel } from "../plugins/plugins";
import { RendererPluginType } from "../types/types";
import { OV } from "../types/types";
import { GlobalCommandRunner } from "./global";
import { Model } from "./model";
class PluginsModel {
globalModel: Model = null;
selectedPlugin: OV<RendererPluginType> = mobx.observable.box(null, { name: "selectedPlugin" });
constructor(globalModel: Model) {
this.globalModel = globalModel;
}
showPluginsView(): void {
PluginModel.loadAllPluginResources();
mobx.action(() => {
this.reset();
this.globalModel.activeMainView.set("plugins");
const allPlugins = PluginModel.allPlugins();
this.selectedPlugin.set(allPlugins.length > 0 ? allPlugins[0] : null);
})();
}
setSelectedPlugin(plugin: RendererPluginType): void {
mobx.action(() => {
this.selectedPlugin.set(plugin);
})();
}
reset(): void {
mobx.action(() => {
this.selectedPlugin.set(null);
})();
}
closeView(): void {
this.globalModel.showSessionView();
setTimeout(() => this.globalModel.inputModel.giveFocus(), 50);
}
}
export { PluginsModel };

206
src/models/remotes.ts Normal file
View File

@ -0,0 +1,206 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { stringToBase64 } from "../util/util";
import { TermWrap } from "../plugins/terminal/term";
import { RemoteInputPacketType, RemoteEditType } from "../types/types";
import * as appconst from "../app/appconst";
import { OV } from "../types/types";
import { GlobalCommandRunner } from "./global";
import { Model } from "./model";
import { getTermPtyData } from "../util/modelutil";
const RemotePtyRows = 8; // also in main.tsx
const RemotePtyCols = 80;
class RemotesModel {
globalModel: Model;
selectedRemoteId: OV<string> = mobx.observable.box(null, {
name: "RemotesModel-selectedRemoteId",
});
remoteTermWrap: TermWrap = null;
remoteTermWrapFocus: OV<boolean> = mobx.observable.box(false, {
name: "RemotesModel-remoteTermWrapFocus",
});
showNoInputMsg: OV<boolean> = mobx.observable.box(false, {
name: "RemotesModel-showNoInputMg",
});
showNoInputTimeoutId: any = null;
remoteEdit: OV<RemoteEditType> = mobx.observable.box(null, {
name: "RemotesModel-remoteEdit",
});
recentConnAddedState: OV<boolean> = mobx.observable.box(false, {
name: "RemotesModel-recentlyAdded",
});
constructor(globalModel: Model) {
this.globalModel = globalModel;
}
get recentConnAdded(): boolean {
return this.recentConnAddedState.get();
}
@boundMethod
setRecentConnAdded(value: boolean) {
mobx.action(() => {
this.recentConnAddedState.set(value);
})();
}
deSelectRemote(): void {
mobx.action(() => {
this.selectedRemoteId.set(null);
this.remoteEdit.set(null);
})();
}
openReadModal(remoteId: string): void {
mobx.action(() => {
this.setRecentConnAdded(false);
this.selectedRemoteId.set(remoteId);
this.remoteEdit.set(null);
this.globalModel.modalsModel.pushModal(appconst.VIEW_REMOTE);
})();
}
openAddModal(redit: RemoteEditType): void {
mobx.action(() => {
this.remoteEdit.set(redit);
this.globalModel.modalsModel.pushModal(appconst.CREATE_REMOTE);
})();
}
openEditModal(redit?: RemoteEditType): void {
mobx.action(() => {
this.selectedRemoteId.set(redit?.remoteid);
this.remoteEdit.set(redit);
this.globalModel.modalsModel.pushModal(appconst.EDIT_REMOTE);
})();
}
selectRemote(remoteId: string): void {
if (this.selectedRemoteId.get() == remoteId) {
return;
}
mobx.action(() => {
this.selectedRemoteId.set(remoteId);
this.remoteEdit.set(null);
})();
}
@boundMethod
startEditAuth(): void {
let remoteId = this.selectedRemoteId.get();
if (remoteId != null) {
GlobalCommandRunner.openEditRemote(remoteId);
}
}
isAuthEditMode(): boolean {
return this.remoteEdit.get() != null;
}
@boundMethod
closeModal(): void {
mobx.action(() => {
this.globalModel.modalsModel.popModal();
})();
setTimeout(() => this.globalModel.refocus(), 10);
}
disposeTerm(): void {
if (this.remoteTermWrap == null) {
return;
}
this.remoteTermWrap.dispose();
this.remoteTermWrap = null;
mobx.action(() => {
this.remoteTermWrapFocus.set(false);
})();
}
receiveData(remoteId: string, ptyPos: number, ptyData: Uint8Array, reason?: string) {
if (this.remoteTermWrap == null) {
return;
}
if (this.remoteTermWrap.getContextRemoteId() != remoteId) {
return;
}
this.remoteTermWrap.receiveData(ptyPos, ptyData);
}
@boundMethod
setRemoteTermWrapFocus(focus: boolean): void {
mobx.action(() => {
this.remoteTermWrapFocus.set(focus);
})();
}
@boundMethod
setShowNoInputMsg(val: boolean) {
mobx.action(() => {
if (this.showNoInputTimeoutId != null) {
clearTimeout(this.showNoInputTimeoutId);
this.showNoInputTimeoutId = null;
}
if (val) {
this.showNoInputMsg.set(true);
this.showNoInputTimeoutId = setTimeout(() => this.setShowNoInputMsg(false), 2000);
} else {
this.showNoInputMsg.set(false);
}
})();
}
@boundMethod
termKeyHandler(remoteId: string, event: any, termWrap: TermWrap): void {
let remote = this.globalModel.getRemote(remoteId);
if (remote == null) {
return;
}
if (remote.status != "connecting" && remote.installstatus != "connecting") {
this.setShowNoInputMsg(true);
return;
}
let inputPacket: RemoteInputPacketType = {
type: "remoteinput",
remoteid: remoteId,
inputdata64: stringToBase64(event.key),
};
this.globalModel.sendInputPacket(inputPacket);
}
createTermWrap(elem: HTMLElement): void {
this.disposeTerm();
let remoteId = this.selectedRemoteId.get();
if (remoteId == null) {
return;
}
let termOpts = {
rows: RemotePtyRows,
cols: RemotePtyCols,
flexrows: false,
maxptysize: 64 * 1024,
};
let termWrap = new TermWrap(elem, {
termContext: { remoteId: remoteId },
usedRows: RemotePtyRows,
termOpts: termOpts,
winSize: null,
keyHandler: (e, termWrap) => {
this.termKeyHandler(remoteId, e, termWrap);
},
focusHandler: this.setRemoteTermWrapFocus.bind(this),
isRunning: true,
fontSize: this.globalModel.termFontSize.get(),
ptyDataSource: getTermPtyData,
onUpdateContentHeight: null,
});
this.remoteTermWrap = termWrap;
}
}
export { RemotesModel };

697
src/models/screen.ts Normal file
View File

@ -0,0 +1,697 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as mobx from "mobx";
import { sprintf } from "sprintf-js";
import { debounce } from "throttle-debounce";
import { base64ToArray, boundInt, isModKeyPress, isBlank } from "../util/util";
import { TermWrap } from "../plugins/terminal/term";
import {
LineType,
RemoteInstanceType,
RemotePtrType,
ScreenDataType,
ScreenOptsType,
PtyDataUpdateType,
RendererContext,
RendererModel,
FocusTypeStrs,
WindowSize,
WebShareOpts,
StatusIndicatorLevel,
LineContainerStrs,
ScreenViewOptsType,
} from "../types/types";
import { windowWidthToCols, windowHeightToRows, termWidthFromCols, termHeightFromRows } from "../util/textmeasure";
import { getRendererContext } from "../app/line/lineutil";
import { MagicLayout } from "../app/magiclayout";
import * as appconst from "../app/appconst";
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../util/keyutil";
import { OV } from "../types/types";
import { Model } from "./model";
import { GlobalCommandRunner } from "./global";
import { Cmd } from "./cmd";
import { ScreenLines } from "./screenlines";
import { getTermPtyData } from "../util/modelutil";
class Screen {
globalModel: Model;
sessionId: string;
screenId: string;
screenIdx: OV<number>;
opts: OV<ScreenOptsType>;
viewOpts: OV<ScreenViewOptsType>;
name: OV<string>;
archived: OV<boolean>;
curRemote: OV<RemotePtrType>;
nextLineNum: OV<number>;
lastScreenSize: WindowSize;
lastCols: number;
lastRows: number;
selectedLine: OV<number>;
focusType: OV<FocusTypeStrs>;
anchor: OV<{ anchorLine: number; anchorOffset: number }>;
termLineNumFocus: OV<number>;
setAnchor_debounced: (anchorLine: number, anchorOffset: number) => void;
terminals: Record<string, TermWrap> = {}; // lineid => TermWrap
renderers: Record<string, RendererModel> = {}; // lineid => RendererModel
shareMode: OV<string>;
webShareOpts: OV<WebShareOpts>;
filterRunning: OV<boolean>;
statusIndicator: OV<StatusIndicatorLevel>;
numRunningCmds: OV<number>;
constructor(sdata: ScreenDataType, globalModel: Model) {
this.globalModel = globalModel;
this.sessionId = sdata.sessionid;
this.screenId = sdata.screenid;
this.name = mobx.observable.box(sdata.name, { name: "screen-name" });
this.nextLineNum = mobx.observable.box(sdata.nextlinenum, { name: "screen-nextlinenum" });
this.screenIdx = mobx.observable.box(sdata.screenidx, {
name: "screen-screenidx",
});
this.opts = mobx.observable.box(sdata.screenopts, { name: "screen-opts" });
this.viewOpts = mobx.observable.box(sdata.screenviewopts, { name: "viewOpts" });
this.archived = mobx.observable.box(!!sdata.archived, {
name: "screen-archived",
});
this.focusType = mobx.observable.box(sdata.focustype, {
name: "focusType",
});
this.selectedLine = mobx.observable.box(sdata.selectedline == 0 ? null : sdata.selectedline, {
name: "selectedLine",
});
this.setAnchor_debounced = debounce(1000, this.setAnchor.bind(this));
this.anchor = mobx.observable.box(
{ anchorLine: sdata.selectedline, anchorOffset: 0 },
{ name: "screen-anchor" }
);
this.termLineNumFocus = mobx.observable.box(0, {
name: "termLineNumFocus",
});
this.curRemote = mobx.observable.box(sdata.curremote, {
name: "screen-curRemote",
});
this.shareMode = mobx.observable.box(sdata.sharemode, {
name: "screen-shareMode",
});
this.webShareOpts = mobx.observable.box(sdata.webshareopts, {
name: "screen-webShareOpts",
});
this.filterRunning = mobx.observable.box(false, {
name: "screen-filter-running",
});
this.statusIndicator = mobx.observable.box(StatusIndicatorLevel.None, {
name: "screen-status-indicator",
});
this.numRunningCmds = mobx.observable.box(0, {
name: "screen-num-running-cmds",
});
}
dispose() {}
isWebShared(): boolean {
return this.shareMode.get() == "web" && this.webShareOpts.get() != null;
}
isSidebarOpen(): boolean {
let viewOpts = this.viewOpts.get();
if (viewOpts == null) {
return false;
}
return viewOpts.sidebar?.open;
}
isLineIdInSidebar(lineId: string): boolean {
let viewOpts = this.viewOpts.get();
if (viewOpts == null) {
return false;
}
if (!viewOpts.sidebar?.open) {
return false;
}
return viewOpts?.sidebar?.sidebarlineid == lineId;
}
getContainerType(): LineContainerStrs {
return appconst.LineContainer_Main;
}
getShareName(): string {
if (!this.isWebShared()) {
return null;
}
let opts = this.webShareOpts.get();
if (opts == null) {
return null;
}
return opts.sharename;
}
getWebShareUrl(): string {
let viewKey: string = null;
if (this.webShareOpts.get() != null) {
viewKey = this.webShareOpts.get().viewkey;
}
if (viewKey == null) {
return null;
}
if (this.globalModel.isDev) {
return sprintf(
"http://devtest.getprompt.com:9001/static/index-dev.html?screenid=%s&viewkey=%s",
this.screenId,
viewKey
);
}
return sprintf("https://share.getprompt.dev/share/%s?viewkey=%s", this.screenId, viewKey);
}
mergeData(data: ScreenDataType) {
if (data.sessionid != this.sessionId || data.screenid != this.screenId) {
throw new Error("invalid screen update, ids don't match");
}
mobx.action(() => {
this.screenIdx.set(data.screenidx);
this.opts.set(data.screenopts);
this.viewOpts.set(data.screenviewopts);
this.name.set(data.name);
this.nextLineNum.set(data.nextlinenum);
this.archived.set(!!data.archived);
let oldSelectedLine = this.selectedLine.get();
let oldFocusType = this.focusType.get();
this.selectedLine.set(data.selectedline);
this.curRemote.set(data.curremote);
this.focusType.set(data.focustype);
this.refocusLine(data, oldFocusType, oldSelectedLine);
this.shareMode.set(data.sharemode);
this.webShareOpts.set(data.webshareopts);
// do not update anchorLine/anchorOffset (only stored)
})();
}
getContentHeight(context: RendererContext): number {
return this.globalModel.getContentHeight(context);
}
setContentHeight(context: RendererContext, height: number): void {
this.globalModel.setContentHeight(context, height);
}
getCmd(line: LineType): Cmd {
return this.globalModel.getCmd(line);
}
getCmdById(lineId: string): Cmd {
return this.globalModel.getCmdByScreenLine(this.screenId, lineId);
}
getAnchorStr(): string {
let anchor = this.anchor.get();
if (anchor.anchorLine == null || anchor.anchorLine == 0) {
return "0";
}
return sprintf("%d:%d", anchor.anchorLine, anchor.anchorOffset);
}
getTabColor(): string {
let tabColor = "default";
let screenOpts = this.opts.get();
if (screenOpts != null && !isBlank(screenOpts.tabcolor)) {
tabColor = screenOpts.tabcolor;
}
return tabColor;
}
getTabIcon(): string {
let tabIcon = "default";
let screenOpts = this.opts.get();
if (screenOpts != null && !isBlank(screenOpts.tabicon)) {
tabIcon = screenOpts.tabicon;
}
return tabIcon;
}
getCurRemoteInstance(): RemoteInstanceType {
let session = this.globalModel.getSessionById(this.sessionId);
let rptr = this.curRemote.get();
if (rptr == null) {
return null;
}
return session.getRemoteInstance(this.screenId, rptr);
}
setAnchorFields(anchorLine: number, anchorOffset: number, reason: string): void {
mobx.action(() => {
this.anchor.set({ anchorLine: anchorLine, anchorOffset: anchorOffset });
})();
}
refocusLine(sdata: ScreenDataType, oldFocusType: string, oldSelectedLine: number): void {
let isCmdFocus = sdata.focustype == "cmd";
if (!isCmdFocus) {
return;
}
let curLineFocus = this.globalModel.getFocusedLine();
let sline: LineType = null;
if (sdata.selectedline != 0) {
sline = this.getLineByNum(sdata.selectedline);
}
if (
curLineFocus.cmdInputFocus ||
(curLineFocus.linenum != null && curLineFocus.linenum != sdata.selectedline)
) {
(document.activeElement as HTMLElement).blur();
}
if (sline != null) {
let renderer = this.getRenderer(sline.lineid);
if (renderer != null) {
renderer.giveFocus();
}
let termWrap = this.getTermWrap(sline.lineid);
if (termWrap != null) {
termWrap.giveFocus();
}
}
}
setFocusType(ftype: FocusTypeStrs): void {
mobx.action(() => {
this.focusType.set(ftype);
})();
}
setAnchor(anchorLine: number, anchorOffset: number): void {
let setVal = anchorLine == null || anchorLine == 0 ? "0" : sprintf("%d:%d", anchorLine, anchorOffset);
GlobalCommandRunner.screenSetAnchor(this.sessionId, this.screenId, setVal);
}
getAnchor(): { anchorLine: number; anchorOffset: number } {
let anchor = this.anchor.get();
if (anchor.anchorLine == null || anchor.anchorLine == 0) {
return { anchorLine: this.selectedLine.get(), anchorOffset: 0 };
}
return anchor;
}
getMaxLineNum(): number {
let win = this.getScreenLines();
if (win == null) {
return null;
}
let lines = win.lines;
if (lines == null || lines.length == 0) {
return null;
}
return lines[lines.length - 1].linenum;
}
getLineByNum(lineNum: number): LineType {
if (lineNum == null) {
return null;
}
let win = this.getScreenLines();
if (win == null) {
return null;
}
let lines = win.lines;
if (lines == null || lines.length == 0) {
return null;
}
for (const line of lines) {
if (line.linenum == lineNum) {
return line;
}
}
return null;
}
getLineById(lineId: string): LineType {
if (lineId == null) {
return null;
}
let win = this.getScreenLines();
if (win == null) {
return null;
}
let lines = win.lines;
if (lines == null || lines.length == 0) {
return null;
}
for (const line of lines) {
if (line.lineid == lineId) {
return line;
}
}
return null;
}
getPresentLineNum(lineNum: number): number {
let win = this.getScreenLines();
if (win == null || !win.loaded.get()) {
return lineNum;
}
let lines = win.lines;
if (lines == null || lines.length == 0) {
return null;
}
if (lineNum == 0) {
return null;
}
for (const line of lines) {
if (line.linenum == lineNum) {
return lineNum;
}
if (line.linenum > lineNum) {
return line.linenum;
}
}
return lines[lines.length - 1].linenum;
}
setSelectedLine(lineNum: number): void {
mobx.action(() => {
let pln = this.getPresentLineNum(lineNum);
if (pln != this.selectedLine.get()) {
this.selectedLine.set(pln);
}
})();
}
checkSelectedLine(): void {
let pln = this.getPresentLineNum(this.selectedLine.get());
if (pln != this.selectedLine.get()) {
this.setSelectedLine(pln);
}
}
updatePtyData(ptyMsg: PtyDataUpdateType) {
let lineId = ptyMsg.lineid;
let renderer = this.renderers[lineId];
if (renderer != null) {
let data = base64ToArray(ptyMsg.ptydata64);
renderer.receiveData(ptyMsg.ptypos, data, "from-sw");
}
let term = this.terminals[lineId];
if (term != null) {
let data = base64ToArray(ptyMsg.ptydata64);
term.receiveData(ptyMsg.ptypos, data, "from-sw");
}
}
isActive(): boolean {
let activeScreen = this.globalModel.getActiveScreen();
if (activeScreen == null) {
return false;
}
return this.sessionId == activeScreen.sessionId && this.screenId == activeScreen.screenId;
}
screenSizeCallback(winSize: WindowSize): void {
if (winSize.height == 0 || winSize.width == 0) {
return;
}
if (
this.lastScreenSize != null &&
this.lastScreenSize.height == winSize.height &&
this.lastScreenSize.width == winSize.width
) {
return;
}
this.lastScreenSize = winSize;
let cols = windowWidthToCols(winSize.width, this.globalModel.termFontSize.get());
let rows = windowHeightToRows(winSize.height, this.globalModel.termFontSize.get());
this._termSizeCallback(rows, cols);
}
getMaxContentSize(): WindowSize {
if (this.lastScreenSize == null) {
let width = termWidthFromCols(80, this.globalModel.termFontSize.get());
let height = termHeightFromRows(25, this.globalModel.termFontSize.get());
return { width, height };
}
let winSize = this.lastScreenSize;
let minSize = MagicLayout.ScreenMinContentSize;
let maxSize = MagicLayout.ScreenMaxContentSize;
let width = boundInt(winSize.width - MagicLayout.ScreenMaxContentWidthBuffer, minSize, maxSize);
let height = boundInt(winSize.height - MagicLayout.ScreenMaxContentHeightBuffer, minSize, maxSize);
return { width, height };
}
getIdealContentSize(): WindowSize {
if (this.lastScreenSize == null) {
let width = termWidthFromCols(80, this.globalModel.termFontSize.get());
let height = termHeightFromRows(25, this.globalModel.termFontSize.get());
return { width, height };
}
let winSize = this.lastScreenSize;
let width = boundInt(Math.ceil((winSize.width - 50) * 0.7), 100, 5000);
let height = boundInt(Math.ceil((winSize.height - 100) * 0.5), 100, 5000);
return { width, height };
}
_termSizeCallback(rows: number, cols: number): void {
if (cols == 0 || rows == 0) {
return;
}
if (rows == this.lastRows && cols == this.lastCols) {
return;
}
this.lastRows = rows;
this.lastCols = cols;
let exclude = [];
for (let lineid in this.terminals) {
let inSidebar = this.isLineIdInSidebar(lineid);
if (!inSidebar) {
this.terminals[lineid].resizeCols(cols);
} else {
exclude.push(lineid);
}
}
GlobalCommandRunner.resizeScreen(this.screenId, rows, cols, { exclude });
}
getTermWrap(lineId: string): TermWrap {
return this.terminals[lineId];
}
getRenderer(lineId: string): RendererModel {
return this.renderers[lineId];
}
registerRenderer(lineId: string, renderer: RendererModel) {
this.renderers[lineId] = renderer;
}
setLineFocus(lineNum: number, focus: boolean): void {
mobx.action(() => this.termLineNumFocus.set(focus ? lineNum : 0))();
if (focus && this.selectedLine.get() != lineNum) {
GlobalCommandRunner.screenSelectLine(String(lineNum), "cmd");
} else if (focus && this.focusType.get() == "input") {
GlobalCommandRunner.screenSetFocus("cmd");
}
}
/**
* Set the status indicator for the screen.
* @param indicator The value of the status indicator. One of "none", "error", "success", "output".
*/
setStatusIndicator(indicator: StatusIndicatorLevel): void {
mobx.action(() => {
this.statusIndicator.set(indicator);
})();
}
/**
* Set the number of running commands for the screen.
* @param numRunning The number of running commands.
*/
setNumRunningCmds(numRunning: number): void {
mobx.action(() => {
this.numRunningCmds.set(numRunning);
})();
}
termCustomKeyHandlerInternal(e: any, termWrap: TermWrap): void {
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
if (checkKeyPressed(waveEvent, "ArrowUp")) {
termWrap.terminal.scrollLines(-1);
return;
}
if (checkKeyPressed(waveEvent, "ArrowDown")) {
termWrap.terminal.scrollLines(1);
return;
}
if (checkKeyPressed(waveEvent, "PageUp")) {
termWrap.terminal.scrollPages(-1);
return;
}
if (checkKeyPressed(waveEvent, "PageDown")) {
termWrap.terminal.scrollPages(1);
return;
}
}
isTermCapturedKey(e: any): boolean {
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
if (
checkKeyPressed(waveEvent, "ArrowUp") ||
checkKeyPressed(waveEvent, "ArrowDown") ||
checkKeyPressed(waveEvent, "PageUp") ||
checkKeyPressed(waveEvent, "PageDown")
) {
return true;
}
return false;
}
termCustomKeyHandler(e: any, termWrap: TermWrap): boolean {
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
if (e.type == "keypress" && checkKeyPressed(waveEvent, "Ctrl:Shift:c")) {
e.stopPropagation();
e.preventDefault();
let sel = termWrap.terminal.getSelection();
navigator.clipboard.writeText(sel);
return false;
}
if (e.type == "keypress" && checkKeyPressed(waveEvent, "Ctrl:Shift:v")) {
e.stopPropagation();
e.preventDefault();
let p = navigator.clipboard.readText();
p.then((text) => {
termWrap.dataHandler?.(text, termWrap);
});
return false;
}
if (termWrap.isRunning) {
return true;
}
let isCaptured = this.isTermCapturedKey(e);
if (!isCaptured) {
return true;
}
if (e.type != "keydown" || isModKeyPress(e)) {
return false;
}
e.stopPropagation();
e.preventDefault();
this.termCustomKeyHandlerInternal(e, termWrap);
return false;
}
loadTerminalRenderer(elem: Element, line: LineType, cmd: Cmd, width: number) {
let lineId = cmd.lineId;
let termWrap = this.getTermWrap(lineId);
if (termWrap != null) {
console.log("term-wrap already exists for", this.screenId, lineId);
return;
}
let usedRows = this.globalModel.getContentHeight(getRendererContext(line));
if (line.contentheight != null && line.contentheight != -1) {
usedRows = line.contentheight;
}
let termContext = {
sessionId: this.sessionId,
screenId: this.screenId,
lineId: line.lineid,
lineNum: line.linenum,
};
termWrap = new TermWrap(elem, {
termContext: termContext,
usedRows: usedRows,
termOpts: cmd.getTermOpts(),
winSize: { height: 0, width: width },
dataHandler: cmd.handleData.bind(cmd),
focusHandler: (focus: boolean) => this.setLineFocus(line.linenum, focus),
isRunning: cmd.isRunning(),
customKeyHandler: this.termCustomKeyHandler.bind(this),
fontSize: this.globalModel.termFontSize.get(),
ptyDataSource: getTermPtyData,
onUpdateContentHeight: (termContext: RendererContext, height: number) => {
this.globalModel.setContentHeight(termContext, height);
},
});
this.terminals[lineId] = termWrap;
if (this.focusType.get() == "cmd" && this.selectedLine.get() == line.linenum) {
termWrap.giveFocus();
}
}
unloadRenderer(lineId: string) {
let rmodel = this.renderers[lineId];
if (rmodel != null) {
rmodel.dispose();
delete this.renderers[lineId];
}
let term = this.terminals[lineId];
if (term != null) {
term.dispose();
delete this.terminals[lineId];
}
}
getUsedRows(context: RendererContext, line: LineType, cmd: Cmd, width: number): number {
if (cmd == null) {
return 0;
}
let termOpts = cmd.getTermOpts();
if (!termOpts.flexrows) {
return termOpts.rows;
}
let termWrap = this.getTermWrap(cmd.lineId);
if (termWrap == null) {
let usedRows = this.globalModel.getContentHeight(context);
if (usedRows != null) {
return usedRows;
}
if (line.contentheight != null && line.contentheight != -1) {
return line.contentheight;
}
return cmd.isRunning() ? 1 : 0;
}
return termWrap.getUsedRows();
}
getIsFocused(lineNum: number): boolean {
return this.termLineNumFocus.get() == lineNum;
}
getSelectedLine(): number {
return this.selectedLine.get();
}
getScreenLines(): ScreenLines {
return this.globalModel.getScreenLinesById(this.screenId);
}
getFocusType(): FocusTypeStrs {
return this.focusType.get();
}
giveFocus(): void {
if (!this.isActive()) {
return;
}
let ftype = this.focusType.get();
if (ftype == "input") {
this.globalModel.inputModel.giveFocus();
} else {
let sline: LineType = null;
if (this.selectedLine.get() != 0) {
sline = this.getLineByNum(this.selectedLine.get());
}
if (sline != null) {
let renderer = this.getRenderer(sline.lineid);
if (renderer != null) {
renderer.giveFocus();
}
let termWrap = this.getTermWrap(sline.lineid);
if (termWrap != null) {
termWrap.giveFocus();
}
}
}
}
}
export { Screen };

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