mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-19 21:11:32 +01:00
Merge branch 'main' of github.com:wavetermdev/waveterm into fix-connections-modal
This commit is contained in:
commit
818bcd5094
@ -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
|
||||
|
||||
|
158
src/app/app.less
158
src/app/app.less
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -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__;
|
||||
|
@ -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();
|
||||
|
@ -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
123
src/app/common/elements/button.less
Normal file
123
src/app/common/elements/button.less
Normal file
@ -0,0 +1,123 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.wave-button {
|
||||
background: none;
|
||||
color: inherit;
|
||||
border: none;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
outline: inherit;
|
||||
|
||||
display: flex;
|
||||
padding: 6px 16px;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border-radius: 6px;
|
||||
height: auto;
|
||||
|
||||
&:hover {
|
||||
color: @term-white;
|
||||
}
|
||||
|
||||
i {
|
||||
fill: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
&.primary {
|
||||
color: @term-green;
|
||||
background: none;
|
||||
|
||||
i {
|
||||
fill: @term-green;
|
||||
}
|
||||
|
||||
&.solid {
|
||||
color: @term-bright-white;
|
||||
background: @term-green;
|
||||
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
|
||||
0px 0px 0.5px 0px rgba(255, 255, 255, 0.8) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.6) inset;
|
||||
|
||||
i {
|
||||
fill: @term-white;
|
||||
}
|
||||
}
|
||||
|
||||
&.outlined {
|
||||
border: 1px solid @term-green;
|
||||
}
|
||||
|
||||
&.ghost {
|
||||
// Styles for .ghost are already defined above
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: @term-bright-white;
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
color: @term-white;
|
||||
background: none;
|
||||
|
||||
&.solid {
|
||||
background: rgba(255, 255, 255, 0.09);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&.outlined {
|
||||
border: 1px solid rgba(255, 255, 255, 0.09);
|
||||
}
|
||||
|
||||
&.ghost {
|
||||
padding: 6px 10px;
|
||||
|
||||
i {
|
||||
fill: @term-green;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.color-yellow {
|
||||
&.solid {
|
||||
border-color: @warning-yellow;
|
||||
background-color: mix(@warning-yellow, @term-white, 50%);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&.outlined {
|
||||
color: @warning-yellow;
|
||||
border-color: @warning-yellow;
|
||||
&:hover {
|
||||
color: @term-white;
|
||||
border-color: @term-white;
|
||||
}
|
||||
}
|
||||
|
||||
&.ghost {
|
||||
}
|
||||
}
|
||||
|
||||
&.color-red {
|
||||
&.solid {
|
||||
border-color: @term-red;
|
||||
background-color: mix(@term-red, @term-white, 50%);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&.outlined {
|
||||
color: @term-red;
|
||||
border-color: @term-red;
|
||||
}
|
||||
|
||||
&.ghost {
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.link-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
63
src/app/common/elements/button.tsx
Normal file
63
src/app/common/elements/button.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import cn from "classnames";
|
||||
|
||||
import "./button.less";
|
||||
|
||||
type ButtonVariantType = "outlined" | "solid" | "ghost";
|
||||
type ButtonThemeType = "primary" | "secondary";
|
||||
|
||||
interface ButtonProps {
|
||||
theme?: ButtonThemeType;
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
variant?: ButtonVariantType;
|
||||
leftIcon?: React.ReactNode;
|
||||
rightIcon?: React.ReactNode;
|
||||
color?: string;
|
||||
style?: React.CSSProperties;
|
||||
autoFocus?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
class Button extends React.Component<ButtonProps> {
|
||||
static defaultProps = {
|
||||
theme: "primary",
|
||||
variant: "solid",
|
||||
color: "",
|
||||
style: {},
|
||||
};
|
||||
|
||||
@boundMethod
|
||||
handleClick() {
|
||||
if (this.props.onClick && !this.props.disabled) {
|
||||
this.props.onClick();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { leftIcon, rightIcon, theme, children, disabled, variant, color, style, autoFocus, className } =
|
||||
this.props;
|
||||
|
||||
return (
|
||||
<button
|
||||
className={cn("wave-button", theme, variant, color, { disabled: disabled }, className)}
|
||||
onClick={this.handleClick}
|
||||
disabled={disabled}
|
||||
style={style}
|
||||
autoFocus={autoFocus}
|
||||
>
|
||||
{leftIcon && <span className="icon-left">{leftIcon}</span>}
|
||||
{children}
|
||||
{rightIcon && <span className="icon-right">{rightIcon}</span>}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { Button };
|
||||
export type { ButtonProps };
|
68
src/app/common/elements/checkbox.less
Normal file
68
src/app/common/elements/checkbox.less
Normal file
@ -0,0 +1,68 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.checkbox {
|
||||
display: flex;
|
||||
|
||||
input[type="checkbox"] {
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
input[type="checkbox"] + label {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: @term-bright-white;
|
||||
transition: color 250ms cubic-bezier(0.4, 0, 0.23, 1);
|
||||
}
|
||||
input[type="checkbox"] + label > span {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: 10px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: transparent;
|
||||
border: 2px solid #9e9e9e;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
transition: all 250ms cubic-bezier(0.4, 0, 0.23, 1);
|
||||
}
|
||||
|
||||
input[type="checkbox"] + label:hover > span,
|
||||
input[type="checkbox"]:focus + label > span {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
input[type="checkbox"]:checked + label > ins {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked + label > span {
|
||||
border: 10px solid @term-green;
|
||||
}
|
||||
input[type="checkbox"]:checked + label > span:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: 3px;
|
||||
width: 7px;
|
||||
height: 12px;
|
||||
border-right: 2px solid #fff;
|
||||
border-bottom: 2px solid #fff;
|
||||
transform: rotate(45deg);
|
||||
transform-origin: 0% 100%;
|
||||
animation: checkbox-check 500ms cubic-bezier(0.4, 0, 0.23, 1);
|
||||
}
|
||||
|
||||
@keyframes checkbox-check {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
33% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
70
src/app/common/elements/checkbox.tsx
Normal file
70
src/app/common/elements/checkbox.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobx from "mobx";
|
||||
import cn from "classnames";
|
||||
|
||||
import "./checkbox.less";
|
||||
|
||||
class Checkbox extends React.Component<
|
||||
{
|
||||
checked?: boolean;
|
||||
defaultChecked?: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
label: React.ReactNode;
|
||||
className?: string;
|
||||
id?: string;
|
||||
},
|
||||
{ checkedInternal: boolean }
|
||||
> {
|
||||
generatedId;
|
||||
static idCounter = 0;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
checkedInternal: this.props.checked ?? Boolean(this.props.defaultChecked),
|
||||
};
|
||||
this.generatedId = `checkbox-${Checkbox.idCounter++}`;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.checked !== undefined && this.props.checked !== prevProps.checked) {
|
||||
this.setState({ checkedInternal: this.props.checked });
|
||||
}
|
||||
}
|
||||
|
||||
handleChange = (e) => {
|
||||
const newChecked = e.target.checked;
|
||||
if (this.props.checked === undefined) {
|
||||
this.setState({ checkedInternal: newChecked });
|
||||
}
|
||||
this.props.onChange(newChecked);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { label, className, id } = this.props;
|
||||
const { checkedInternal } = this.state;
|
||||
const checkboxId = id || this.generatedId;
|
||||
|
||||
return (
|
||||
<div className={cn("checkbox", className)}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={checkboxId}
|
||||
checked={checkedInternal}
|
||||
onChange={this.handleChange}
|
||||
aria-checked={checkedInternal}
|
||||
role="checkbox"
|
||||
/>
|
||||
<label htmlFor={checkboxId}>
|
||||
<span></span>
|
||||
{label}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { Checkbox };
|
102
src/app/common/elements/cmdstrcode.less
Normal file
102
src/app/common/elements/cmdstrcode.less
Normal file
@ -0,0 +1,102 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.cmdstr-code {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 0px 10px 0px 0;
|
||||
|
||||
&.is-large {
|
||||
.use-button {
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
}
|
||||
|
||||
.code-div code {
|
||||
}
|
||||
}
|
||||
|
||||
&.limit-height .code-div {
|
||||
max-height: 58px;
|
||||
}
|
||||
|
||||
&.limit-height.is-large .code-div {
|
||||
max-height: 68px;
|
||||
}
|
||||
|
||||
.use-button {
|
||||
flex-grow: 0;
|
||||
padding: 3px;
|
||||
border-radius: 3px 0 0 3px;
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-self: flex-start;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.code-div {
|
||||
background-color: @term-black;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-width: 100px;
|
||||
overflow: auto;
|
||||
border-left: 1px solid #777;
|
||||
|
||||
code {
|
||||
flex-shrink: 0;
|
||||
min-width: 100px;
|
||||
color: @term-white;
|
||||
white-space: pre;
|
||||
padding: 2px 8px 2px 8px;
|
||||
background-color: @term-black;
|
||||
font-size: 1em;
|
||||
font-family: @fixed-font;
|
||||
}
|
||||
}
|
||||
|
||||
.copy-control {
|
||||
width: 0;
|
||||
position: relative;
|
||||
display: block;
|
||||
visibility: hidden;
|
||||
|
||||
.inner-copy {
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
right: -20px;
|
||||
|
||||
padding: 2px;
|
||||
padding-left: 4px;
|
||||
cursor: pointer;
|
||||
width: 20px;
|
||||
|
||||
&:hover {
|
||||
color: @term-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .copy-control {
|
||||
visibility: visible !important;
|
||||
}
|
||||
}
|
||||
|
||||
.copied-indicator {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: @term-white;
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
animation-name: fade-in-out;
|
||||
animation-duration: 0.3s;
|
||||
}
|
66
src/app/common/elements/cmdstrcode.tsx
Normal file
66
src/app/common/elements/cmdstrcode.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import cn from "classnames";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
|
||||
import { ReactComponent as CheckIcon } from "../../assets/icons/line/check.svg";
|
||||
import { ReactComponent as CopyIcon } from "../../assets/icons/history/copy.svg";
|
||||
|
||||
import "./cmdstrcode.less";
|
||||
|
||||
class CmdStrCode extends React.Component<
|
||||
{
|
||||
cmdstr: string;
|
||||
onUse: () => void;
|
||||
onCopy: () => void;
|
||||
isCopied: boolean;
|
||||
fontSize: "normal" | "large";
|
||||
limitHeight: boolean;
|
||||
},
|
||||
{}
|
||||
> {
|
||||
@boundMethod
|
||||
handleUse(e: any) {
|
||||
e.stopPropagation();
|
||||
if (this.props.onUse != null) {
|
||||
this.props.onUse();
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleCopy(e: any) {
|
||||
e.stopPropagation();
|
||||
if (this.props.onCopy != null) {
|
||||
this.props.onCopy();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let { isCopied, cmdstr, fontSize, limitHeight } = this.props;
|
||||
return (
|
||||
<div className={cn("cmdstr-code", { "is-large": fontSize == "large" }, { "limit-height": limitHeight })}>
|
||||
<If condition={isCopied}>
|
||||
<div key="copied" className="copied-indicator">
|
||||
<div>copied</div>
|
||||
</div>
|
||||
</If>
|
||||
<div key="use" className="use-button hoverEffect" title="Use Command" onClick={this.handleUse}>
|
||||
<CheckIcon className="icon" />
|
||||
</div>
|
||||
<div key="code" className="code-div">
|
||||
<code>{cmdstr}</code>
|
||||
</div>
|
||||
<div key="copy" className="copy-control hoverEffect">
|
||||
<div className="inner-copy" onClick={this.handleCopy} title="copy">
|
||||
<CopyIcon className="icon" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { CmdStrCode };
|
10
src/app/common/elements/cmdtext.tsx
Normal file
10
src/app/common/elements/cmdtext.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
function renderCmdText(text: string): any {
|
||||
return <span>⌘{text}</span>;
|
||||
}
|
||||
|
||||
export { renderCmdText };
|
127
src/app/common/elements/dropdown.less
Normal file
127
src/app/common/elements/dropdown.less
Normal file
@ -0,0 +1,127 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.wave-dropdown {
|
||||
position: relative;
|
||||
height: 44px;
|
||||
min-width: 150px;
|
||||
width: 100%;
|
||||
border: 1px solid rgba(241, 246, 243, 0.15);
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
|
||||
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||
|
||||
&.no-label {
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
&-label {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 16px;
|
||||
font-size: 12.5px;
|
||||
transition: all 0.3s;
|
||||
color: @term-white;
|
||||
line-height: 10px;
|
||||
|
||||
&.float {
|
||||
font-size: 10px;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
&.offset-left {
|
||||
left: 42px;
|
||||
}
|
||||
}
|
||||
|
||||
&-display {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
bottom: 5px;
|
||||
|
||||
&.offset-left {
|
||||
left: 42px;
|
||||
}
|
||||
}
|
||||
|
||||
&-arrow {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
transition: transform 0.3s;
|
||||
pointer-events: none;
|
||||
|
||||
i {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&-arrow-rotate {
|
||||
transform: translateY(-50%) rotate(180deg); // Rotate the arrow when dropdown is open
|
||||
}
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
min-width: 120px;
|
||||
padding: 5px 8px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
border-radius: 6px;
|
||||
|
||||
&-highlighted,
|
||||
&:hover {
|
||||
background: rgba(241, 246, 243, 0.08);
|
||||
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
|
||||
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||
}
|
||||
}
|
||||
|
||||
.wave-input-decoration {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.wave-input-decoration.end-position {
|
||||
margin-right: 44px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.wave-input-decoration.start-position {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&-error {
|
||||
border-color: @term-red;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: @term-green;
|
||||
}
|
||||
}
|
||||
|
||||
.wave-dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-top: 2px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
padding: 6px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
border-radius: 6px;
|
||||
background: #151715;
|
||||
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5), 0px 3px 8px 0px rgba(0, 0, 0, 0.35), 0px 0px 0.5px 0px #fff inset,
|
||||
0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||
animation-fill-mode: forwards;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.wave-dropdown-menu-close {
|
||||
z-index: 0;
|
||||
}
|
259
src/app/common/elements/dropdown.tsx
Normal file
259
src/app/common/elements/dropdown.tsx
Normal file
@ -0,0 +1,259 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import cn from "classnames";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
import "./dropdown.less";
|
||||
|
||||
interface DropdownDecorationProps {
|
||||
startDecoration?: React.ReactNode;
|
||||
endDecoration?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface DropdownProps {
|
||||
label?: string;
|
||||
options: { value: string; label: string }[];
|
||||
value?: string;
|
||||
className?: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
decoration?: DropdownDecorationProps;
|
||||
defaultValue?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
interface DropdownState {
|
||||
isOpen: boolean;
|
||||
internalValue: string;
|
||||
highlightedIndex: number;
|
||||
isTouched: boolean;
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class Dropdown extends React.Component<DropdownProps, DropdownState> {
|
||||
wrapperRef: React.RefObject<HTMLDivElement>;
|
||||
menuRef: React.RefObject<HTMLDivElement>;
|
||||
timeoutId: any;
|
||||
|
||||
constructor(props: DropdownProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isOpen: false,
|
||||
internalValue: props.defaultValue || "",
|
||||
highlightedIndex: -1,
|
||||
isTouched: false,
|
||||
};
|
||||
this.wrapperRef = React.createRef();
|
||||
this.menuRef = React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener("mousedown", this.handleClickOutside);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener("mousedown", this.handleClickOutside);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<DropdownProps>, prevState: Readonly<DropdownState>, snapshot?: any): void {
|
||||
// If the dropdown was open but now is closed, start the timeout
|
||||
if (prevState.isOpen && !this.state.isOpen) {
|
||||
this.timeoutId = setTimeout(() => {
|
||||
if (this.menuRef.current) {
|
||||
this.menuRef.current.style.display = "none";
|
||||
}
|
||||
}, 300); // Time is equal to the animation duration
|
||||
}
|
||||
// If the dropdown is now open, cancel any existing timeout and show the menu
|
||||
else if (!prevState.isOpen && this.state.isOpen) {
|
||||
if (this.timeoutId !== null) {
|
||||
clearTimeout(this.timeoutId); // Cancel any existing timeout
|
||||
this.timeoutId = null;
|
||||
}
|
||||
if (this.menuRef.current) {
|
||||
this.menuRef.current.style.display = "inline-flex";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleClickOutside(event: MouseEvent) {
|
||||
// Check if the click is outside both the wrapper and the menu
|
||||
if (
|
||||
this.wrapperRef.current &&
|
||||
!this.wrapperRef.current.contains(event.target as Node) &&
|
||||
this.menuRef.current &&
|
||||
!this.menuRef.current.contains(event.target as Node)
|
||||
) {
|
||||
this.setState({ isOpen: false });
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleClick() {
|
||||
this.toggleDropdown();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleFocus() {
|
||||
this.setState({ isTouched: true });
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleKeyDown(event: React.KeyboardEvent) {
|
||||
const { options } = this.props;
|
||||
const { isOpen, highlightedIndex } = this.state;
|
||||
|
||||
switch (event.key) {
|
||||
case "Enter":
|
||||
case " ":
|
||||
if (isOpen) {
|
||||
const option = options[highlightedIndex];
|
||||
if (option) {
|
||||
this.handleSelect(option.value, undefined);
|
||||
}
|
||||
} else {
|
||||
this.toggleDropdown();
|
||||
}
|
||||
break;
|
||||
case "Escape":
|
||||
this.setState({ isOpen: false });
|
||||
break;
|
||||
case "ArrowUp":
|
||||
if (isOpen) {
|
||||
this.setState((prevState) => ({
|
||||
highlightedIndex:
|
||||
prevState.highlightedIndex > 0 ? prevState.highlightedIndex - 1 : options.length - 1,
|
||||
}));
|
||||
}
|
||||
break;
|
||||
case "ArrowDown":
|
||||
if (isOpen) {
|
||||
this.setState((prevState) => ({
|
||||
highlightedIndex:
|
||||
prevState.highlightedIndex < options.length - 1 ? prevState.highlightedIndex + 1 : 0,
|
||||
}));
|
||||
}
|
||||
break;
|
||||
case "Tab":
|
||||
this.setState({ isOpen: false });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleSelect(value: string, event?: React.MouseEvent | React.KeyboardEvent) {
|
||||
const { onChange } = this.props;
|
||||
if (event) {
|
||||
event.stopPropagation(); // This stops the event from bubbling up to the wrapper
|
||||
}
|
||||
|
||||
if (!("value" in this.props)) {
|
||||
this.setState({ internalValue: value });
|
||||
}
|
||||
onChange(value);
|
||||
this.setState({ isOpen: false, isTouched: true });
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
toggleDropdown() {
|
||||
this.setState((prevState) => ({ isOpen: !prevState.isOpen, isTouched: true }));
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
calculatePosition(): React.CSSProperties {
|
||||
if (this.wrapperRef.current) {
|
||||
const rect = this.wrapperRef.current.getBoundingClientRect();
|
||||
return {
|
||||
position: "absolute",
|
||||
top: `${rect.bottom + window.scrollY}px`,
|
||||
left: `${rect.left + window.scrollX}px`,
|
||||
width: `${rect.width}px`,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { label, options, value, placeholder, decoration, className, required } = this.props;
|
||||
const { isOpen, internalValue, highlightedIndex, isTouched } = this.state;
|
||||
|
||||
const currentValue = value ?? internalValue;
|
||||
const selectedOptionLabel =
|
||||
options.find((option) => option.value === currentValue)?.label || placeholder || internalValue;
|
||||
|
||||
// Determine if the dropdown should be marked as having an error
|
||||
const isError =
|
||||
required &&
|
||||
(value === undefined || value === "") &&
|
||||
(internalValue === undefined || internalValue === "") &&
|
||||
isTouched;
|
||||
|
||||
// Determine if the label should float
|
||||
const shouldLabelFloat = !!value || !!internalValue || !!placeholder || isOpen;
|
||||
|
||||
const dropdownMenu = isOpen
|
||||
? ReactDOM.createPortal(
|
||||
<div className={cn("wave-dropdown-menu")} ref={this.menuRef} style={this.calculatePosition()}>
|
||||
{options.map((option, index) => (
|
||||
<div
|
||||
key={option.value}
|
||||
className={cn("wave-dropdown-item unselectable", {
|
||||
"wave-dropdown-item-highlighted": index === highlightedIndex,
|
||||
})}
|
||||
onClick={(e) => this.handleSelect(option.value, e)}
|
||||
onMouseEnter={() => this.setState({ highlightedIndex: index })}
|
||||
onMouseLeave={() => this.setState({ highlightedIndex: -1 })}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
))}
|
||||
</div>,
|
||||
document.getElementById("app")!
|
||||
)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("wave-dropdown", className, {
|
||||
"wave-dropdown-error": isError,
|
||||
"no-label": !label,
|
||||
})}
|
||||
ref={this.wrapperRef}
|
||||
tabIndex={0}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onClick={this.handleClick}
|
||||
onFocus={this.handleFocus}
|
||||
>
|
||||
{decoration?.startDecoration && <>{decoration.startDecoration}</>}
|
||||
<If condition={label}>
|
||||
<div
|
||||
className={cn("wave-dropdown-label unselectable", {
|
||||
float: shouldLabelFloat,
|
||||
"offset-left": decoration?.startDecoration,
|
||||
})}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
</If>
|
||||
<div
|
||||
className={cn("wave-dropdown-display unselectable", { "offset-left": decoration?.startDecoration })}
|
||||
>
|
||||
{selectedOptionLabel}
|
||||
</div>
|
||||
<div className={cn("wave-dropdown-arrow", { "wave-dropdown-arrow-rotate": isOpen })}>
|
||||
<i className="fa-sharp fa-solid fa-chevron-down"></i>
|
||||
</div>
|
||||
{dropdownMenu}
|
||||
{decoration?.endDecoration && <>{decoration.endDecoration}</>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { Dropdown };
|
21
src/app/common/elements/iconbutton.tsx
Normal file
21
src/app/common/elements/iconbutton.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import { Button } from "./button";
|
||||
class IconButton extends Button {
|
||||
render() {
|
||||
const { children, theme, variant = "solid", ...rest } = this.props;
|
||||
const className = `wave-button icon-button ${theme} ${variant}`;
|
||||
|
||||
return (
|
||||
<button {...rest} className={className}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default IconButton;
|
||||
|
||||
export { IconButton };
|
20
src/app/common/elements/index.tsx
Normal file
20
src/app/common/elements/index.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
export { Button } from "./button";
|
||||
export { Checkbox } from "./checkbox";
|
||||
export { CmdStrCode } from "./cmdstrcode";
|
||||
export { renderCmdText } from "./cmdtext";
|
||||
export { Dropdown } from "./dropdown";
|
||||
export { IconButton } from "./iconbutton";
|
||||
export { InlineSettingsTextEdit } from "./inlinesettingstextedit";
|
||||
export { InputDecoration } from "./inputdecoration";
|
||||
export { LinkButton } from "./linkbutton";
|
||||
export { Markdown } from "./markdown";
|
||||
export { Modal } from "./modal";
|
||||
export { NumberField } from "./numberfield";
|
||||
export { PasswordField } from "./passwordfield";
|
||||
export { ResizableSidebar } from "./resizablesidebar";
|
||||
export { SettingsError } from "./settingserror";
|
||||
export { ShowWaveShellInstallPrompt } from "./showwaveshellinstallprompt";
|
||||
export { Status } from "./status";
|
||||
export { TextField } from "./textfield";
|
||||
export { Toggle } from "./toggle";
|
||||
export { Tooltip } from "./tooltip";
|
40
src/app/common/elements/inlinesettingstextedit.less
Normal file
40
src/app/common/elements/inlinesettingstextedit.less
Normal file
@ -0,0 +1,40 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.inline-edit {
|
||||
.icon {
|
||||
display: inline;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-left: 1em;
|
||||
vertical-align: middle;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
&.edit-not-active {
|
||||
cursor: pointer;
|
||||
|
||||
i.fa-pen {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dotted;
|
||||
}
|
||||
}
|
||||
|
||||
&.edit-active {
|
||||
input.input {
|
||||
padding: 0;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.button {
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
149
src/app/common/elements/inlinesettingstextedit.tsx
Normal file
149
src/app/common/elements/inlinesettingstextedit.tsx
Normal file
@ -0,0 +1,149 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import * as mobx from "mobx";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import cn from "classnames";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../../util/keyutil";
|
||||
|
||||
import "./inlinesettingstextedit.less";
|
||||
|
||||
type OV<V> = mobx.IObservableValue<V>;
|
||||
|
||||
@mobxReact.observer
|
||||
class InlineSettingsTextEdit extends React.Component<
|
||||
{
|
||||
text: string;
|
||||
value: string;
|
||||
onChange: (val: string) => void;
|
||||
maxLength: number;
|
||||
placeholder: string;
|
||||
showIcon?: boolean;
|
||||
},
|
||||
{}
|
||||
> {
|
||||
isEditing: OV<boolean> = mobx.observable.box(false, { name: "inlineedit-isEditing" });
|
||||
tempText: OV<string>;
|
||||
shouldFocus: boolean = false;
|
||||
inputRef: React.RefObject<any> = React.createRef();
|
||||
|
||||
componentDidUpdate(): void {
|
||||
if (this.shouldFocus) {
|
||||
this.shouldFocus = false;
|
||||
if (this.inputRef.current != null) {
|
||||
this.inputRef.current.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleChangeText(e: any): void {
|
||||
mobx.action(() => {
|
||||
this.tempText.set(e.target.value);
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
confirmChange(): void {
|
||||
mobx.action(() => {
|
||||
let newText = this.tempText.get();
|
||||
this.isEditing.set(false);
|
||||
this.tempText = null;
|
||||
this.props.onChange(newText);
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
cancelChange(): void {
|
||||
mobx.action(() => {
|
||||
this.isEditing.set(false);
|
||||
this.tempText = null;
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleKeyDown(e: any): void {
|
||||
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||
if (checkKeyPressed(waveEvent, "Enter")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.confirmChange();
|
||||
return;
|
||||
}
|
||||
if (checkKeyPressed(waveEvent, "Escape")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.cancelChange();
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
clickEdit(): void {
|
||||
mobx.action(() => {
|
||||
this.isEditing.set(true);
|
||||
this.shouldFocus = true;
|
||||
this.tempText = mobx.observable.box(this.props.value, { name: "inlineedit-tempText" });
|
||||
})();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.isEditing.get()) {
|
||||
return (
|
||||
<div className={cn("settings-input inline-edit", "edit-active")}>
|
||||
<div className="field has-addons">
|
||||
<div className="control">
|
||||
<input
|
||||
ref={this.inputRef}
|
||||
className="input"
|
||||
type="text"
|
||||
onKeyDown={this.handleKeyDown}
|
||||
placeholder={this.props.placeholder}
|
||||
onChange={this.handleChangeText}
|
||||
value={this.tempText.get()}
|
||||
maxLength={this.props.maxLength}
|
||||
/>
|
||||
</div>
|
||||
<div className="control">
|
||||
<div
|
||||
onClick={this.cancelChange}
|
||||
title="Cancel (Esc)"
|
||||
className="button is-prompt-danger is-outlined is-small"
|
||||
>
|
||||
<span className="icon is-small">
|
||||
<i className="fa-sharp fa-solid fa-xmark" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="control">
|
||||
<div
|
||||
onClick={this.confirmChange}
|
||||
title="Confirm (Enter)"
|
||||
className="button is-wave-green is-outlined is-small"
|
||||
>
|
||||
<span className="icon is-small">
|
||||
<i className="fa-sharp fa-solid fa-check" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div onClick={this.clickEdit} className={cn("settings-input inline-edit", "edit-not-active")}>
|
||||
{this.props.text}
|
||||
<If condition={this.props.showIcon}>
|
||||
<i className="fa-sharp fa-solid fa-pen" />
|
||||
</If>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { InlineSettingsTextEdit };
|
19
src/app/common/elements/inputdecoration.less
Normal file
19
src/app/common/elements/inputdecoration.less
Normal file
@ -0,0 +1,19 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.wave-input-decoration {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
i {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.wave-input-decoration.start-position {
|
||||
margin: 0 4px 0 16px;
|
||||
}
|
||||
|
||||
.wave-input-decoration.end-position {
|
||||
margin: 0 16px 0 8px;
|
||||
}
|
32
src/app/common/elements/inputdecoration.tsx
Normal file
32
src/app/common/elements/inputdecoration.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import cn from "classnames";
|
||||
|
||||
import "./inputdecoration.less";
|
||||
|
||||
interface InputDecorationProps {
|
||||
position?: "start" | "end";
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class InputDecoration extends React.Component<InputDecorationProps, {}> {
|
||||
render() {
|
||||
const { children, position = "end" } = this.props;
|
||||
return (
|
||||
<div
|
||||
className={cn("wave-input-decoration", {
|
||||
"start-position": position === "start",
|
||||
"end-position": position === "end",
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { InputDecoration };
|
28
src/app/common/elements/linkbutton.tsx
Normal file
28
src/app/common/elements/linkbutton.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import cn from "classnames";
|
||||
import { ButtonProps } from "./button";
|
||||
|
||||
interface LinkButtonProps extends ButtonProps {
|
||||
href: string;
|
||||
rel?: string;
|
||||
target?: string;
|
||||
}
|
||||
|
||||
class LinkButton extends React.Component<LinkButtonProps> {
|
||||
render() {
|
||||
const { leftIcon, rightIcon, children, className, ...rest } = this.props;
|
||||
|
||||
return (
|
||||
<a {...rest} className={cn(`wave-button link-button`, className)}>
|
||||
{leftIcon && <span className="icon-left">{leftIcon}</span>}
|
||||
{children}
|
||||
{rightIcon && <span className="icon-right">{rightIcon}</span>}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { LinkButton };
|
92
src/app/common/elements/markdown.less
Normal file
92
src/app/common/elements/markdown.less
Normal file
@ -0,0 +1,92 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.markdown {
|
||||
color: @term-white;
|
||||
margin-bottom: 10px;
|
||||
font-family: @markdown-font;
|
||||
font-size: 14px;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
code {
|
||||
background-color: @markdown-highlight;
|
||||
color: @term-white;
|
||||
font-family: @terminal-font;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
code.inline {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
font-family: @terminal-font;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: @term-white;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: @term-white;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #32afff;
|
||||
}
|
||||
|
||||
table {
|
||||
tr th {
|
||||
color: @term-white;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
list-style-position: outside;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-position: outside;
|
||||
margin-left: 19px;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 4px 10px 4px 10px;
|
||||
border-radius: 3px;
|
||||
background-color: @markdown-highlight;
|
||||
padding: 2px 4px 2px 6px;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: @markdown-highlight;
|
||||
margin: 4px 10px 4px 10px;
|
||||
padding: 6px 6px 6px 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
pre.selected {
|
||||
outline: 2px solid @term-green;
|
||||
}
|
||||
|
||||
.title.is-1 {
|
||||
border-bottom: 1px solid #777;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
.title.is-2 {
|
||||
border-bottom: 1px solid #777;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
.title.is-3 {
|
||||
}
|
||||
.title.is-4 {
|
||||
}
|
||||
.title.is-5 {
|
||||
}
|
||||
.title.is-6 {
|
||||
}
|
||||
}
|
||||
|
||||
.markdown > *:first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
105
src/app/common/elements/markdown.tsx
Normal file
105
src/app/common/elements/markdown.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import cn from "classnames";
|
||||
import { GlobalModel } from "../../../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 };
|
79
src/app/common/elements/modal.less
Normal file
79
src/app/common/elements/modal.less
Normal file
@ -0,0 +1,79 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.wave-modal-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 500;
|
||||
|
||||
.wave-modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(21, 23, 21, 0.7);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.wave-modal {
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
border-radius: 10px;
|
||||
background: #151715;
|
||||
box-shadow: 0px 3px 5px 0px rgba(0, 0, 0, 0.35), 0px 10px 24px 0px rgba(0, 0, 0, 0.45),
|
||||
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||
|
||||
.wave-modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.wave-modal-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 14px 12px 20px;
|
||||
justify-content: space-between;
|
||||
line-height: 20px;
|
||||
border-bottom: 1px solid rgba(250, 250, 250, 0.1);
|
||||
|
||||
.wave-modal-title {
|
||||
color: #eceeec;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
button {
|
||||
i {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wave-modal-body {
|
||||
width: 100%;
|
||||
padding: 0px 20px;
|
||||
}
|
||||
|
||||
.wave-modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
padding: 0 20px 20px;
|
||||
|
||||
button:last-child {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
81
src/app/common/elements/modal.tsx
Normal file
81
src/app/common/elements/modal.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobx from "mobx";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Button } from "./button";
|
||||
import { IconButton } from "./iconbutton";
|
||||
|
||||
import "./modal.less";
|
||||
|
||||
type OV<V> = mobx.IObservableValue<V>;
|
||||
|
||||
interface ModalHeaderProps {
|
||||
onClose?: () => void;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const ModalHeader: React.FC<ModalHeaderProps> = ({ onClose, title }) => (
|
||||
<div className="wave-modal-header">
|
||||
{<div className="wave-modal-title">{title}</div>}
|
||||
<If condition={onClose}>
|
||||
<IconButton theme="secondary" variant="ghost" onClick={onClose}>
|
||||
<i className="fa-sharp fa-solid fa-xmark"></i>
|
||||
</IconButton>
|
||||
</If>
|
||||
</div>
|
||||
);
|
||||
|
||||
interface ModalFooterProps {
|
||||
onCancel?: () => void;
|
||||
onOk?: () => void;
|
||||
cancelLabel?: string;
|
||||
okLabel?: string;
|
||||
}
|
||||
|
||||
const ModalFooter: React.FC<ModalFooterProps> = ({ onCancel, onOk, cancelLabel = "Cancel", okLabel = "Ok" }) => (
|
||||
<div className="wave-modal-footer">
|
||||
{onCancel && (
|
||||
<Button theme="secondary" onClick={onCancel}>
|
||||
{cancelLabel}
|
||||
</Button>
|
||||
)}
|
||||
{onOk && <Button onClick={onOk}>{okLabel}</Button>}
|
||||
</div>
|
||||
);
|
||||
|
||||
interface ModalProps {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
onClickBackdrop?: () => void;
|
||||
}
|
||||
|
||||
class Modal extends React.Component<ModalProps> {
|
||||
static Header = ModalHeader;
|
||||
static Footer = ModalFooter;
|
||||
|
||||
renderBackdrop(onClick: (() => void) | undefined) {
|
||||
return <div className="wave-modal-backdrop" onClick={onClick}></div>;
|
||||
}
|
||||
|
||||
renderModal() {
|
||||
const { className, children } = this.props;
|
||||
|
||||
return (
|
||||
<div className="wave-modal-container">
|
||||
{this.renderBackdrop(this.props.onClickBackdrop)}
|
||||
<div className={`wave-modal ${className}`}>
|
||||
<div className="wave-modal-content">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return ReactDOM.createPortal(this.renderModal(), document.getElementById("app"));
|
||||
}
|
||||
}
|
||||
|
||||
export { Modal };
|
39
src/app/common/elements/numberfield.tsx
Normal file
39
src/app/common/elements/numberfield.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
|
||||
import { TextField } from "./textfield";
|
||||
|
||||
class NumberField extends TextField {
|
||||
@boundMethod
|
||||
handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const { required, onChange } = this.props;
|
||||
const inputValue = e.target.value;
|
||||
|
||||
// Allow only numeric input
|
||||
if (inputValue === "" || /^\d*$/.test(inputValue)) {
|
||||
// Update the internal state only if the component is not controlled.
|
||||
if (this.props.value === undefined) {
|
||||
const isError = required ? inputValue.trim() === "" : false;
|
||||
|
||||
this.setState({
|
||||
internalValue: inputValue,
|
||||
error: isError,
|
||||
hasContent: Boolean(inputValue),
|
||||
});
|
||||
}
|
||||
|
||||
onChange && onChange(inputValue);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
// Use the render method from TextField but add the onKeyDown handler
|
||||
const renderedTextField = super.render();
|
||||
return React.cloneElement(renderedTextField);
|
||||
}
|
||||
}
|
||||
|
||||
export { NumberField };
|
30
src/app/common/elements/passwordfield.less
Normal file
30
src/app/common/elements/passwordfield.less
Normal file
@ -0,0 +1,30 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.wave-password {
|
||||
.wave-textfield-inner-eye {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 52%;
|
||||
transform: translateY(-50%);
|
||||
transition: transform 0.3s;
|
||||
|
||||
i {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.wave-input-decoration {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.wave-input-decoration.end-position {
|
||||
margin-right: 47px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.wave-input-decoration.start-position {
|
||||
left: 0;
|
||||
}
|
||||
}
|
106
src/app/common/elements/passwordfield.tsx
Normal file
106
src/app/common/elements/passwordfield.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import cn from "classnames";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
import { TextFieldState, TextField } from "./textfield";
|
||||
|
||||
import "./passwordfield.less";
|
||||
|
||||
interface PasswordFieldState extends TextFieldState {
|
||||
passwordVisible: boolean;
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class PasswordField extends TextField {
|
||||
state: PasswordFieldState;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
...this.state,
|
||||
passwordVisible: false,
|
||||
};
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
togglePasswordVisibility() {
|
||||
//@ts-ignore
|
||||
this.setState((prevState) => ({
|
||||
//@ts-ignore
|
||||
passwordVisible: !prevState.passwordVisible,
|
||||
}));
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
// Call the parent handleInputChange method
|
||||
super.handleInputChange(e);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { decoration, className, placeholder, maxLength, label } = this.props;
|
||||
const { focused, internalValue, error, passwordVisible } = this.state;
|
||||
const inputValue = this.props.value ?? internalValue;
|
||||
|
||||
// The input should always receive the real value
|
||||
const inputProps = {
|
||||
className: cn("wave-textfield-inner-input", { "offset-left": decoration?.startDecoration }),
|
||||
ref: this.inputRef,
|
||||
id: label,
|
||||
value: inputValue, // Always use the real value here
|
||||
onChange: this.handleInputChange,
|
||||
onFocus: this.handleFocus,
|
||||
onBlur: this.handleBlur,
|
||||
placeholder: placeholder,
|
||||
maxLength: maxLength,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(`wave-textfield wave-password ${className || ""}`, {
|
||||
focused: focused,
|
||||
error: error,
|
||||
"no-label": !label,
|
||||
})}
|
||||
>
|
||||
{decoration?.startDecoration && <>{decoration.startDecoration}</>}
|
||||
<div className="wave-textfield-inner">
|
||||
<label
|
||||
className={cn("wave-textfield-inner-label", {
|
||||
float: this.state.hasContent || this.state.focused || placeholder,
|
||||
"offset-left": decoration?.startDecoration,
|
||||
})}
|
||||
htmlFor={label}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
<If condition={passwordVisible}>
|
||||
<input {...inputProps} type="text" />
|
||||
</If>
|
||||
<If condition={!passwordVisible}>
|
||||
<input {...inputProps} type="password" />
|
||||
</If>
|
||||
<div
|
||||
className="wave-textfield-inner-eye"
|
||||
onClick={this.togglePasswordVisibility}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<If condition={passwordVisible}>
|
||||
<i className="fa-sharp fa-solid fa-eye"></i>
|
||||
</If>
|
||||
<If condition={!passwordVisible}>
|
||||
<i className="fa-sharp fa-solid fa-eye-slash"></i>
|
||||
</If>
|
||||
</div>
|
||||
</div>
|
||||
{decoration?.endDecoration && <>{decoration.endDecoration}</>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { PasswordField };
|
9
src/app/common/elements/resizablesidebar.less
Normal file
9
src/app/common/elements/resizablesidebar.less
Normal file
@ -0,0 +1,9 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.sidebar-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 5px;
|
||||
cursor: col-resize;
|
||||
}
|
171
src/app/common/elements/resizablesidebar.tsx
Normal file
171
src/app/common/elements/resizablesidebar.tsx
Normal file
@ -0,0 +1,171 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import * as mobx from "mobx";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import cn from "classnames";
|
||||
import { GlobalModel, GlobalCommandRunner } from "../../../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 };
|
36
src/app/common/elements/settingserror.tsx
Normal file
36
src/app/common/elements/settingserror.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import * as mobx from "mobx";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
|
||||
type OV<V> = mobx.IObservableValue<V>;
|
||||
|
||||
@mobxReact.observer
|
||||
class SettingsError extends React.Component<{ errorMessage: OV<string> }, {}> {
|
||||
@boundMethod
|
||||
dismissError(): void {
|
||||
mobx.action(() => {
|
||||
this.props.errorMessage.set(null);
|
||||
})();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.errorMessage.get() == null) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="settings-field settings-error">
|
||||
<div>Error: {this.props.errorMessage.get()}</div>
|
||||
<div className="flex-spacer" />
|
||||
<div onClick={this.dismissError} className="error-dismiss">
|
||||
<i className="fa-sharp fa-solid fa-xmark" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { SettingsError };
|
28
src/app/common/elements/showwaveshellinstallprompt.tsx
Normal file
28
src/app/common/elements/showwaveshellinstallprompt.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { GlobalModel } from "../../../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 };
|
30
src/app/common/elements/status.less
Normal file
30
src/app/common/elements/status.less
Normal file
@ -0,0 +1,30 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.wave-status-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.dot {
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dot.green {
|
||||
background-color: @status-connected;
|
||||
}
|
||||
|
||||
.dot.red {
|
||||
background-color: @status-error;
|
||||
}
|
||||
|
||||
.dot.gray {
|
||||
background-color: @status-disconnected;
|
||||
}
|
||||
|
||||
.dot.yellow {
|
||||
background-color: @status-connecting;
|
||||
}
|
||||
}
|
34
src/app/common/elements/status.tsx
Normal file
34
src/app/common/elements/status.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
|
||||
import "./status.less";
|
||||
|
||||
interface StatusProps {
|
||||
status: "green" | "red" | "gray" | "yellow";
|
||||
text: string;
|
||||
}
|
||||
|
||||
class Status extends React.Component<StatusProps> {
|
||||
@boundMethod
|
||||
renderDot() {
|
||||
const { status } = this.props;
|
||||
|
||||
return <div className={`dot ${status}`} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { text } = this.props;
|
||||
|
||||
return (
|
||||
<div className="wave-status-container">
|
||||
{this.renderDot()}
|
||||
<span>{text}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { Status };
|
82
src/app/common/elements/textfield.less
Normal file
82
src/app/common/elements/textfield.less
Normal file
@ -0,0 +1,82 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.wave-textfield {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 6px;
|
||||
position: relative;
|
||||
height: 44px;
|
||||
min-width: 412px;
|
||||
gap: 6px;
|
||||
border: 1px solid rgba(241, 246, 243, 0.15);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
|
||||
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||
|
||||
&:hover {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
&.focused {
|
||||
border-color: @term-green;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-color: @term-red;
|
||||
}
|
||||
|
||||
&-inner {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
|
||||
&-label {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 16px;
|
||||
font-size: 12.5px;
|
||||
transition: all 0.3s;
|
||||
color: @text-secondary;
|
||||
line-height: 10px;
|
||||
|
||||
&.float {
|
||||
font-size: 10px;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
&.offset-left {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-input {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
border: none;
|
||||
padding: 5px 0 5px 16px;
|
||||
font-size: 16px;
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
color: @term-bright-white;
|
||||
line-height: 20px;
|
||||
|
||||
&.offset-left {
|
||||
padding: 5px 16px 5px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.no-label {
|
||||
height: 34px;
|
||||
|
||||
input {
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
173
src/app/common/elements/textfield.tsx
Normal file
173
src/app/common/elements/textfield.tsx
Normal file
@ -0,0 +1,173 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import cn from "classnames";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
|
||||
import "./textfield.less";
|
||||
|
||||
interface TextFieldDecorationProps {
|
||||
startDecoration?: React.ReactNode;
|
||||
endDecoration?: React.ReactNode;
|
||||
}
|
||||
interface TextFieldProps {
|
||||
label?: string;
|
||||
value?: string;
|
||||
className?: string;
|
||||
onChange?: (value: string) => void;
|
||||
placeholder?: string;
|
||||
defaultValue?: string;
|
||||
decoration?: TextFieldDecorationProps;
|
||||
required?: boolean;
|
||||
maxLength?: number;
|
||||
autoFocus?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface TextFieldState {
|
||||
focused: boolean;
|
||||
internalValue: string;
|
||||
error: boolean;
|
||||
showHelpText: boolean;
|
||||
hasContent: boolean;
|
||||
}
|
||||
|
||||
class TextField extends React.Component<TextFieldProps, TextFieldState> {
|
||||
inputRef: React.RefObject<HTMLInputElement>;
|
||||
state: TextFieldState;
|
||||
|
||||
constructor(props: TextFieldProps) {
|
||||
super(props);
|
||||
const hasInitialContent = Boolean(props.value || props.defaultValue);
|
||||
this.state = {
|
||||
focused: false,
|
||||
hasContent: hasInitialContent,
|
||||
internalValue: props.defaultValue || "",
|
||||
error: false,
|
||||
showHelpText: false,
|
||||
};
|
||||
this.inputRef = React.createRef();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: TextFieldProps) {
|
||||
// Only update the focus state if using as controlled
|
||||
if (this.props.value !== undefined && this.props.value !== prevProps.value) {
|
||||
this.setState({ focused: Boolean(this.props.value) });
|
||||
}
|
||||
}
|
||||
|
||||
// Method to handle focus at the component level
|
||||
@boundMethod
|
||||
handleComponentFocus() {
|
||||
if (this.inputRef.current && !this.inputRef.current.contains(document.activeElement)) {
|
||||
this.inputRef.current.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Method to handle blur at the component level
|
||||
@boundMethod
|
||||
handleComponentBlur() {
|
||||
if (this.inputRef.current?.contains(document.activeElement)) {
|
||||
this.inputRef.current.blur();
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleFocus() {
|
||||
this.setState({ focused: true });
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleBlur() {
|
||||
const { required } = this.props;
|
||||
if (this.inputRef.current) {
|
||||
const value = this.inputRef.current.value;
|
||||
if (required && !value) {
|
||||
this.setState({ error: true, focused: false });
|
||||
} else {
|
||||
this.setState({ error: false, focused: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleHelpTextClick() {
|
||||
this.setState((prevState) => ({ showHelpText: !prevState.showHelpText }));
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const { required, onChange } = this.props;
|
||||
const inputValue = e.target.value;
|
||||
|
||||
// Check if value is empty and the field is required
|
||||
if (required && !inputValue) {
|
||||
this.setState({ error: true, hasContent: false });
|
||||
} else {
|
||||
this.setState({ error: false, hasContent: Boolean(inputValue) });
|
||||
}
|
||||
|
||||
// Update the internal state for uncontrolled version
|
||||
if (this.props.value === undefined) {
|
||||
this.setState({ internalValue: inputValue });
|
||||
}
|
||||
|
||||
onChange && onChange(inputValue);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { label, value, placeholder, decoration, className, maxLength, autoFocus, disabled } = this.props;
|
||||
const { focused, internalValue, error } = this.state;
|
||||
|
||||
// Decide if the input should behave as controlled or uncontrolled
|
||||
const inputValue = value ?? internalValue;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("wave-textfield", className, {
|
||||
focused: focused,
|
||||
error: error,
|
||||
disabled: disabled,
|
||||
"no-label": !label,
|
||||
})}
|
||||
onFocus={this.handleComponentFocus}
|
||||
onBlur={this.handleComponentBlur}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{decoration?.startDecoration && <>{decoration.startDecoration}</>}
|
||||
<div className="wave-textfield-inner">
|
||||
<If condition={label}>
|
||||
<label
|
||||
className={cn("wave-textfield-inner-label", {
|
||||
float: this.state.hasContent || this.state.focused || placeholder,
|
||||
"offset-left": decoration?.startDecoration,
|
||||
})}
|
||||
htmlFor={label}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
</If>
|
||||
<input
|
||||
className={cn("wave-textfield-inner-input", { "offset-left": decoration?.startDecoration })}
|
||||
ref={this.inputRef}
|
||||
id={label}
|
||||
value={inputValue}
|
||||
onChange={this.handleInputChange}
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleBlur}
|
||||
placeholder={placeholder}
|
||||
maxLength={maxLength}
|
||||
autoFocus={autoFocus}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
{decoration?.endDecoration && <>{decoration.endDecoration}</>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { TextField };
|
||||
export type { TextFieldProps, TextFieldDecorationProps, TextFieldState };
|
47
src/app/common/elements/toggle.less
Normal file
47
src/app/common/elements/toggle.less
Normal file
@ -0,0 +1,47 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.checkbox-toggle {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 22px;
|
||||
|
||||
input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
content: "";
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: #333;
|
||||
transition: 0.5s;
|
||||
border-radius: 33px;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
left: 2px;
|
||||
bottom: 2px;
|
||||
background-color: @term-white;
|
||||
transition: 0.5s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: @term-green;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(18px);
|
||||
}
|
||||
}
|
28
src/app/common/elements/toggle.tsx
Normal file
28
src/app/common/elements/toggle.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
|
||||
import "./toggle.less";
|
||||
|
||||
class Toggle extends React.Component<{ checked: boolean; onChange: (value: boolean) => void }, {}> {
|
||||
@boundMethod
|
||||
handleChange(e: any): void {
|
||||
let { onChange } = this.props;
|
||||
if (onChange != null) {
|
||||
onChange(e.target.checked);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<label className="checkbox-toggle">
|
||||
<input type="checkbox" checked={this.props.checked} onChange={this.handleChange} />
|
||||
<span className="slider" />
|
||||
</label>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { Toggle };
|
23
src/app/common/elements/tooltip.less
Normal file
23
src/app/common/elements/tooltip.less
Normal file
@ -0,0 +1,23 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.wave-tooltip {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
border: 1px solid #777;
|
||||
background-color: #444;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
width: 300px;
|
||||
|
||||
i {
|
||||
display: inline;
|
||||
font-size: 13px;
|
||||
fill: @base-color;
|
||||
padding-top: 0.2em;
|
||||
}
|
||||
}
|
84
src/app/common/elements/tooltip.tsx
Normal file
84
src/app/common/elements/tooltip.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import cn from "classnames";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
import "./tooltip.less";
|
||||
|
||||
interface TooltipProps {
|
||||
message: React.ReactNode;
|
||||
icon?: React.ReactNode; // Optional icon property
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface TooltipState {
|
||||
isVisible: boolean;
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class Tooltip extends React.Component<TooltipProps, TooltipState> {
|
||||
iconRef: React.RefObject<HTMLDivElement>;
|
||||
|
||||
constructor(props: TooltipProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isVisible: false,
|
||||
};
|
||||
this.iconRef = React.createRef();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
showBubble() {
|
||||
this.setState({ isVisible: true });
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
hideBubble() {
|
||||
this.setState({ isVisible: false });
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
calculatePosition() {
|
||||
// Get the position of the icon element
|
||||
const iconElement = this.iconRef.current;
|
||||
if (iconElement) {
|
||||
const rect = iconElement.getBoundingClientRect();
|
||||
return {
|
||||
top: `${rect.bottom + window.scrollY - 29}px`,
|
||||
left: `${rect.left + window.scrollX + rect.width / 2 - 17.5}px`,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
renderBubble() {
|
||||
if (!this.state.isVisible) return null;
|
||||
|
||||
const style = this.calculatePosition();
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<div className={cn("wave-tooltip", this.props.className)} style={style}>
|
||||
{this.props.icon && <div className="wave-tooltip-icon">{this.props.icon}</div>}
|
||||
<div className="wave-tooltip-message">{this.props.message}</div>
|
||||
</div>,
|
||||
document.getElementById("app")!
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div onMouseEnter={this.showBubble} onMouseLeave={this.hideBubble} ref={this.iconRef}>
|
||||
{this.props.children}
|
||||
{this.renderBubble()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { Tooltip };
|
@ -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">
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
23
src/app/common/modals/linesettings.less
Normal file
23
src/app/common/modals/linesettings.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
151
src/app/common/modals/linesettings.tsx
Normal file
151
src/app/common/modals/linesettings.tsx
Normal 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 };
|
@ -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}</>;
|
||||
}
|
||||
|
@ -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 };
|
||||
|
52
src/app/common/modals/screensettings.less
Normal file
52
src/app/common/modals/screensettings.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
366
src/app/common/modals/screensettings.tsx
Normal file
366
src/app/common/modals/screensettings.tsx
Normal 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 };
|
28
src/app/common/modals/sessionsettings.less
Normal file
28
src/app/common/modals/sessionsettings.less
Normal 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;
|
||||
}
|
155
src/app/common/modals/sessionsettings.tsx
Normal file
155
src/app/common/modals/sessionsettings.tsx
Normal 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
@ -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,
|
||||
};
|
@ -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";
|
||||
|
@ -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";
|
||||
|
||||
|
15
src/app/common/modals/userinput.less
Normal file
15
src/app/common/modals/userinput.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
88
src/app/common/modals/userinput.tsx
Normal file
88
src/app/common/modals/userinput.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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";
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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";
|
||||
|
@ -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
@ -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";
|
||||
|
@ -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;
|
||||
|
@ -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}
|
||||
|
@ -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() {
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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";
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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";
|
||||
|
@ -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}
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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
287
src/models/bookmarks.ts
Normal 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 };
|
26
src/models/clientsettingsview.ts
Normal file
26
src/models/clientsettingsview.ts
Normal 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
145
src/models/cmd.ts
Normal 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
431
src/models/commandrunner.ts
Normal 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 };
|
26
src/models/connectionsview.ts
Normal file
26
src/models/connectionsview.ts
Normal 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 };
|
115
src/models/forwardlinecontainer.ts
Normal file
115
src/models/forwardlinecontainer.ts
Normal 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
6
src/models/global.ts
Normal 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
326
src/models/historyview.ts
Normal 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
16
src/models/index.ts
Normal 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
798
src/models/input.ts
Normal 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
85
src/models/mainsidebar.ts
Normal 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
30
src/models/modals.ts
Normal 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
1477
src/models/model.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
47
src/models/plugins.ts
Normal 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
206
src/models/remotes.ts
Normal 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
697
src/models/screen.ts
Normal 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
Loading…
Reference in New Issue
Block a user