merge 'main' into use-ssh-library--add-user-input

I had to reimplement a few front end changes but nothing too major aside
from that.
This commit is contained in:
Sylvia Crowe 2024-02-08 15:51:32 -08:00
commit ea253c33ae
67 changed files with 5401 additions and 114 deletions

View File

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

View File

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

View File

@ -8,7 +8,7 @@ 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 { GlobalModel } from "../../models";
import { CmdStrCode, Markdown } from "../common/elements";
import { ReactComponent as XmarkIcon } from "../assets/icons/line/xmark.svg";
@ -19,8 +19,16 @@ import { ReactComponent as FavoritesIcon } from "../assets/icons/favourites.svg"
import "./bookmarks.less";
type BookmarkProps = {
bookmark: BookmarkType;
};
@mobxReact.observer
class Bookmark extends React.Component<{ bookmark: BookmarkType }, {}> {
class Bookmark extends React.Component<BookmarkProps, {}> {
constructor(props: BookmarkProps) {
super(props);
}
@boundMethod
handleDeleteClick(): void {
let { bookmark } = this.props;
@ -179,6 +187,10 @@ class Bookmark extends React.Component<{ bookmark: BookmarkType }, {}> {
@mobxReact.observer
class BookmarksView extends React.Component<{}, {}> {
constructor(props: {}) {
super(props);
}
@boundMethod
closeView(): void {
GlobalModel.bookmarksModel.closeView();

View File

@ -6,24 +6,20 @@ import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { GlobalModel, GlobalCommandRunner, MinFontSize, MaxFontSize, RemotesModel } from "../../model/model";
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../models";
import { Toggle, InlineSettingsTextEdit, SettingsError, Dropdown } from "../common/elements";
import { CommandRtnType, ClientDataType } from "../../types/types";
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">

View File

@ -6,7 +6,7 @@ import * as mobxReact from "mobx-react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import cn from "classnames";
import { GlobalModel } from "../../../model/model";
import { GlobalModel } from "../../../models";
import "./markdown.less";

View File

@ -6,7 +6,7 @@ import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
import { GlobalModel, GlobalCommandRunner } from "../../../models";
import { MagicLayout } from "../../magiclayout";
import "./resizablesidebar.less";

View File

@ -1,7 +1,7 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { GlobalModel } from "../../../model/model";
import { GlobalModel } from "../../../models";
import * as appconst from "../../appconst";
function ShowWaveShellInstallPrompt(callbackFn: () => void) {

View File

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

View File

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

View File

@ -5,7 +5,7 @@ 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 { GlobalModel } from "../../../models";
import { Modal, Button } from "../elements";
import "./clientstop.less";

View File

@ -6,7 +6,7 @@ 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,

View File

@ -5,7 +5,7 @@ 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 { Modal, Button } from "../elements";
import "./disconnected.less";

View File

@ -6,7 +6,7 @@ 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 "../elements";
import * as util from "../../../util/util";

View File

@ -5,7 +5,7 @@ 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 "../../../model/model";
import { GlobalModel, GlobalCommandRunner } from "../../../models";
import { SettingsError, Modal, Dropdown } from "../elements";
import { LineType, RendererPluginType } from "../../../types/types";
import { PluginModel } from "../../../plugins/plugins";

View File

@ -3,7 +3,7 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import { GlobalModel } from "../../../model/model";
import { GlobalModel } from "../../../models";
import { TosModal } from "./tos";
@mobxReact.observer

View File

@ -7,7 +7,7 @@ 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, TabIcons, Screen } from "../../../model/model";
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";
@ -15,6 +15,7 @@ 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";
@ -281,7 +282,7 @@ class ScreenSettingsModal extends React.Component<{}, {}> {
<span className="tab-color-name">{screen.getTabColor()}</span>
</div>
<div className="tab-color-sep">|</div>
<For each="color" of={TabColors}>
<For each="color" of={appconst.TabColors}>
<div
key={color}
className="tab-color-select"
@ -307,7 +308,7 @@ class ScreenSettingsModal extends React.Component<{}, {}> {
<span className="tab-icon-name">{screen.getTabIcon()}</span>
</div>
<div className="tab-icon-sep">|</div>
<For each="icon" index="index" of={TabIcons}>
<For each="icon" index="index" of={appconst.TabIcons}>
<div
key={`${color}-${index}`}
className="tab-icon-select"

View File

@ -5,7 +5,7 @@ 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 "../../../model/model";
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";

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import * as React from "react";
import { GlobalModel } from "../../../model/model";
import { GlobalModel } from "../../../models";
import { Choose, When, If } from "tsx-control-statements";
import { Modal, PasswordField, Markdown } from "../elements";
import { UserInputRequest } from "../../../types/types";

View File

@ -7,7 +7,7 @@ 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 "../elements";
import * as util from "../../../util/util";

View File

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

View File

@ -7,7 +7,7 @@ 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 { GlobalModel, RemotesModel, GlobalCommandRunner } from "../../models";
import { Button, Status, ShowWaveShellInstallPrompt } from "../common/elements";
import * as T from "../../types/types";
import * as util from "../../util/util";

View File

@ -8,7 +8,7 @@ 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";

View File

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

View File

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

View File

@ -5,7 +5,7 @@ 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/elements";

View File

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

View File

@ -4,7 +4,7 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { GlobalModel } from "../../../model/model";
import { GlobalModel } from "../../../models";
import { isBlank } from "../../../util/util";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";

View File

@ -10,7 +10,7 @@ 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 { GlobalModel, GlobalCommandRunner, Screen } from "../../../models";
import { renderCmdText } from "../../common/elements";
import { TextAreaInput } from "./textareainput";
import { InfoMsg } from "./infomsg";

View File

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

View File

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

View File

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

View File

@ -10,16 +10,7 @@ import { If, For } from "tsx-control-statements/components";
import cn from "classnames";
import { debounce } from "throttle-debounce";
import dayjs from "dayjs";
import {
GlobalCommandRunner,
TabColors,
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";
@ -442,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}
@ -469,7 +460,7 @@ class NewTabSettings extends React.Component<{ screen: Screen }, {}> {
<>
<div className="text-s1 unselectable">Select the color</div>
<div className="control-iconlist">
<For each="color" of={TabColors}>
<For each="color" of={appconst.TabColors}>
<div
className="icondiv"
key={color}

View File

@ -6,7 +6,7 @@ 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/elements";
import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg";

View File

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

View File

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

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

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

View File

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

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

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

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

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

View File

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

View File

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

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

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

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

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

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

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

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

@ -0,0 +1,803 @@
// 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,
} from "../types/types";
import { StrWithPos } from "../types/types";
import * as appconst from "../app/appconst";
import { OV } from "../types/types";
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);
return;
})();
}
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 {
this.aIChatShow.set(true);
this.setAIChatFocus();
}
closeAIAssistantChat(): void {
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 (val == "\" ") {
// this.setInputMode("comment");
// val = "";
// }
// if (val == "//") {
// this.setInputMode("global");
// val = "";
// }
if (this.modHistory.length <= hidx) {
this.modHistory.length = hidx + 1;
}
this.modHistory[hidx] = val;
})();
}
resetInput(): void {
mobx.action(() => {
this.setHistoryShow(false);
this.closeAIAssistantChat();
this.infoShow.set(false);
this.inputMode.set(null);
this.resetHistory();
this.dropModHistory(false);
this.infoMsg.set(null);
this.inputExpanded.set(false);
this._clearInfoTimeout();
})();
}
@boundMethod
toggleExpandInput(): void {
mobx.action(() => {
this.inputExpanded.set(!this.inputExpanded.get());
this.forceInputFocus = true;
})();
}
getCurLine(): string {
let hidx = this.historyIndex.get();
if (hidx < this.modHistory.length && this.modHistory[hidx] != null) {
return this.modHistory[hidx];
}
let hitems = this.getFilteredHistoryItems();
if (hidx == 0 || hitems == null || hidx > hitems.length) {
return "";
}
let hitem = hitems[hidx - 1];
if (hitem == null) {
return "";
}
return hitem.cmdstr;
}
dropModHistory(keepLine0: boolean): void {
mobx.action(() => {
if (keepLine0) {
if (this.modHistory.length > 1) {
this.modHistory.splice(1, this.modHistory.length - 1);
}
} else {
this.modHistory.replace([""]);
}
})();
}
resetHistory(): void {
mobx.action(() => {
this.setHistoryShow(false);
this.historyLoading.set(false);
this.historyType.set("screen");
this.historyItems.set(null);
this.historyIndex.set(0);
this.historyQueryOpts.set(getDefaultHistoryQueryOpts());
this.historyAfterLoadIndex = 0;
this.dropModHistory(true);
})();
}
}
export { InputModel };

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

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

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

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

1430
src/models/model.ts Normal file

File diff suppressed because it is too large Load Diff

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

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

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

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

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

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

163
src/models/screenlines.ts Normal file
View File

@ -0,0 +1,163 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as mobx from "mobx";
import { sprintf } from "sprintf-js";
import { genMergeSimpleData } from "../util/util";
import { LineType, CmdDataType, ScreenLinesType } from "../types/types";
import { cmdStatusIsRunning } from "../app/line/lineutil";
import { OV, OArr } from "../types/types";
import { Cmd } from "./cmd";
class ScreenLines {
screenId: string;
loaded: OV<boolean> = mobx.observable.box(false, { name: "slines-loaded" });
loadError: OV<string> = mobx.observable.box(null);
lines: OArr<LineType> = mobx.observable.array([], {
name: "slines-lines",
deep: false,
});
cmds: Record<string, Cmd> = {}; // lineid => Cmd
constructor(screenId: string) {
this.screenId = screenId;
}
getNonArchivedLines(): LineType[] {
let rtn: LineType[] = [];
for (const line of this.lines) {
if (line.archived) {
continue;
}
rtn.push(line);
}
return rtn;
}
updateData(slines: ScreenLinesType, load: boolean) {
mobx.action(() => {
if (load) {
this.loaded.set(true);
}
genMergeSimpleData(
this.lines,
slines.lines,
(l: LineType) => String(l.lineid),
(l: LineType) => sprintf("%013d:%s", l.ts, l.lineid)
);
let cmds = slines.cmds || [];
for (const cmd of cmds) {
this.cmds[cmd.lineid] = new Cmd(cmd);
}
})();
}
setLoadError(errStr: string) {
mobx.action(() => {
this.loaded.set(true);
this.loadError.set(errStr);
})();
}
dispose() {}
getCmd(lineId: string): Cmd {
return this.cmds[lineId];
}
/**
* Get all running cmds in the screen.
* @param returnFirst If true, return the first running cmd found.
* @returns An array of running cmds, or the first running cmd if returnFirst is true.
*/
getRunningCmdLines(returnFirst?: boolean): LineType[] {
let rtn: LineType[] = [];
for (const line of this.lines) {
const cmd = this.getCmd(line.lineid);
if (cmd == null) {
continue;
}
const status = cmd.getStatus();
if (cmdStatusIsRunning(status)) {
if (returnFirst) {
return [line];
}
rtn.push(line);
}
}
return rtn;
}
/**
* Check if there are any running cmds in the screen.
* @returns True if there are any running cmds.
*/
hasRunningCmdLines(): boolean {
return this.getRunningCmdLines(true).length > 0;
}
updateCmd(cmd: CmdDataType): void {
if (cmd.remove) {
throw new Error("cannot remove cmd with updateCmd call [" + cmd.lineid + "]");
}
let origCmd = this.cmds[cmd.lineid];
if (origCmd != null) {
origCmd.setCmd(cmd);
}
}
mergeCmd(cmd: CmdDataType): void {
if (cmd.remove) {
delete this.cmds[cmd.lineid];
return;
}
let origCmd = this.cmds[cmd.lineid];
if (origCmd == null) {
this.cmds[cmd.lineid] = new Cmd(cmd);
return;
}
origCmd.setCmd(cmd);
}
addLineCmd(line: LineType, cmd: CmdDataType, interactive: boolean) {
if (!this.loaded.get()) {
return;
}
mobx.action(() => {
if (cmd != null) {
this.mergeCmd(cmd);
}
if (line != null) {
let lines = this.lines;
if (line.remove) {
for (let i = 0; i < lines.length; i++) {
if (lines[i].lineid == line.lineid) {
this.lines.splice(i, 1);
break;
}
}
return;
}
let lineIdx = 0;
for (lineIdx = 0; lineIdx < lines.length; lineIdx++) {
let lineId = lines[lineIdx].lineid;
let curTs = lines[lineIdx].ts;
if (lineId == line.lineid) {
this.lines[lineIdx] = line;
return;
}
if (curTs > line.ts || (curTs == line.ts && lineId > line.lineid)) {
break;
}
}
if (lineIdx == lines.length) {
this.lines.push(line);
return;
}
this.lines.splice(lineIdx, 0, line);
}
})();
}
}
export { ScreenLines };

117
src/models/session.ts Normal file
View File

@ -0,0 +1,117 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as mobx from "mobx";
import { sprintf } from "sprintf-js";
import { genMergeSimpleData, isBlank, ces } from "../util/util";
import { SessionDataType, RemoteInstanceType, RemotePtrType } from "../types/types";
import { OV, OArr } from "../types/types";
import { Model } from "./model";
import { Screen } from "./screen";
class Session {
sessionId: string;
name: OV<string>;
activeScreenId: OV<string>;
sessionIdx: OV<number>;
notifyNum: OV<number> = mobx.observable.box(0);
remoteInstances: OArr<RemoteInstanceType>;
archived: OV<boolean>;
globalModel: Model;
constructor(sdata: SessionDataType, globalModel: Model) {
this.globalModel = globalModel;
this.sessionId = sdata.sessionid;
this.name = mobx.observable.box(sdata.name);
this.sessionIdx = mobx.observable.box(sdata.sessionidx);
this.archived = mobx.observable.box(!!sdata.archived);
this.activeScreenId = mobx.observable.box(ces(sdata.activescreenid));
let remotes = sdata.remotes || [];
this.remoteInstances = mobx.observable.array(remotes);
}
dispose(): void {}
// session updates only contain screens (no windows)
mergeData(sdata: SessionDataType) {
if (sdata.sessionid != this.sessionId) {
throw new Error(
sprintf(
"cannot merge session data, sessionids don't match sid=%s, data-sid=%s",
this.sessionId,
sdata.sessionid
)
);
}
mobx.action(() => {
if (!isBlank(sdata.name)) {
this.name.set(sdata.name);
}
if (sdata.sessionidx > 0) {
this.sessionIdx.set(sdata.sessionidx);
}
if (sdata.notifynum >= 0) {
this.notifyNum.set(sdata.notifynum);
}
this.archived.set(!!sdata.archived);
if (!isBlank(sdata.activescreenid)) {
let screen = this.getScreenById(sdata.activescreenid);
if (screen == null) {
console.log(
sprintf("got session update, activescreenid=%s, screen not found", sdata.activescreenid)
);
} else {
this.activeScreenId.set(sdata.activescreenid);
}
}
genMergeSimpleData(this.remoteInstances, sdata.remotes, (r) => r.riid, null);
})();
}
getActiveScreen(): Screen {
return this.getScreenById(this.activeScreenId.get());
}
setActiveScreenId(screenId: string) {
this.activeScreenId.set(screenId);
}
getScreenById(screenId: string): Screen {
if (screenId == null) {
return null;
}
return this.globalModel.getScreenById(this.sessionId, screenId);
}
getRemoteInstance(screenId: string, rptr: RemotePtrType): RemoteInstanceType {
if (rptr.name.startsWith("*")) {
screenId = "";
}
for (const rdata of this.remoteInstances) {
if (
rdata.screenid == screenId &&
rdata.remoteid == rptr.remoteid &&
rdata.remoteownerid == rptr.ownerid &&
rdata.name == rptr.name
) {
return rdata;
}
}
let remote = this.globalModel.getRemote(rptr.remoteid);
if (remote != null) {
return {
riid: "",
sessionid: this.sessionId,
screenid: screenId,
remoteownerid: rptr.ownerid,
remoteid: rptr.remoteid,
name: rptr.name,
festate: remote.defaultfestate,
shelltype: remote.defaultshelltype,
};
}
return null;
}
}
export { Session };

View File

@ -0,0 +1,164 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { TermWrap } from "../plugins/terminal/term";
import { LineType, RendererContext, RendererModel, FocusTypeStrs, WindowSize, LineContainerStrs } from "../types/types";
import { windowWidthToCols } from "../util/textmeasure";
import { getRendererContext } from "../app/line/lineutil";
import { getTermPtyData } from "../util/modelutil";
import { Cmd } from "./cmd";
import { Model } from "./model";
type CmdFinder = {
getCmdById(cmdId: string): Cmd;
};
class SpecialLineContainer {
globalModel: Model;
wsize: WindowSize;
allowInput: boolean;
terminal: TermWrap;
renderer: RendererModel;
cmd: Cmd;
cmdFinder: CmdFinder;
containerType: LineContainerStrs;
constructor(cmdFinder: CmdFinder, wsize: WindowSize, allowInput: boolean, containerType: LineContainerStrs) {
this.globalModel = Model.getInstance();
this.cmdFinder = cmdFinder;
this.wsize = wsize;
this.allowInput = allowInput;
}
getCmd(line: LineType): Cmd {
if (this.cmd == null) {
this.cmd = this.cmdFinder.getCmdById(line.lineid);
}
return this.cmd;
}
getContainerType(): LineContainerStrs {
return this.containerType;
}
isSidebarOpen(): boolean {
return false;
}
isLineIdInSidebar(lineId: string): boolean {
return false;
}
setLineFocus(lineNum: number, focus: boolean): void {
return;
}
setContentHeight(context: RendererContext, height: number): void {
return;
}
getMaxContentSize(): WindowSize {
return this.wsize;
}
getIdealContentSize(): WindowSize {
return this.wsize;
}
loadTerminalRenderer(elem: Element, line: LineType, cmd: Cmd, width: number): void {
this.unloadRenderer(null);
let lineId = cmd.lineId;
let termWrap = this.getTermWrap(lineId);
if (termWrap != null) {
console.log("term-wrap already exists for", line.screenid, lineId);
return;
}
let usedRows = this.globalModel.getContentHeight(getRendererContext(line));
if (line.contentheight != null && line.contentheight != -1) {
usedRows = line.contentheight;
}
let termContext = {
screenId: line.screenid,
lineId: line.lineid,
lineNum: line.linenum,
};
termWrap = new TermWrap(elem, {
termContext: termContext,
usedRows: usedRows,
termOpts: cmd.getTermOpts(),
winSize: { height: 0, width: width },
dataHandler: null,
focusHandler: null,
isRunning: cmd.isRunning(),
customKeyHandler: null,
fontSize: this.globalModel.termFontSize.get(),
ptyDataSource: getTermPtyData,
onUpdateContentHeight: null,
});
this.terminal = termWrap;
}
registerRenderer(lineId: string, renderer: RendererModel): void {
this.renderer = renderer;
}
unloadRenderer(lineId: string): void {
if (this.renderer != null) {
this.renderer.dispose();
this.renderer = null;
}
if (this.terminal != null) {
this.terminal.dispose();
this.terminal = null;
}
}
getContentHeight(context: RendererContext): number {
return this.globalModel.getContentHeight(context);
}
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 cols = windowWidthToCols(width, this.globalModel.termFontSize.get());
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 false;
}
getRenderer(lineId: string): RendererModel {
return this.renderer;
}
getTermWrap(lineId: string): TermWrap {
return this.terminal;
}
getFocusType(): FocusTypeStrs {
return "input";
}
getSelectedLine(): number {
return null;
}
}
export { SpecialLineContainer };

View File

@ -6,7 +6,7 @@ import * as T from "../../types/types";
import Editor, { Monaco } from "@monaco-editor/react";
import type * as MonacoTypes from "monaco-editor/esm/vs/editor/editor.api";
import { Markdown } from "../../app/common/elements";
import { GlobalModel, GlobalCommandRunner } from "../../model/model";
import { GlobalModel, GlobalCommandRunner } from "../../models";
import Split from "react-split-it";
import loader from "@monaco-editor/loader";
loader.config({ paths: { vs: "./node_modules/monaco-editor/min/vs" } });

View File

@ -20,7 +20,7 @@ import type {
import * as T from "../../types/types";
import { debounce, throttle } 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>;
@ -56,7 +56,7 @@ class SimpleBlobRendererModel {
this.savedHeight = params.savedHeight;
this.ptyDataSource = params.ptyDataSource;
if (this.isClosed) {
this.dataBlob = (new Blob() as T.ExtBlob);
this.dataBlob = new Blob() as T.ExtBlob;
this.dataBlob.notFound = false; // TODO
} else {
if (this.isDone.get()) {

View File

@ -4,7 +4,7 @@
import React, { FC, useEffect, useState, useRef, useMemo } from "react";
import { RendererContext, RendererOpts, LineStateType, RendererModelContainerApi } from "../../types/types";
import * as T from "../../types/types";
import { GlobalModel } from "../../model/model";
import { GlobalModel } from "../../models";
import Papa from "papaparse";
import {
createColumnHelper,

View File

@ -9,7 +9,7 @@ import * as T from "../../types/types";
import { isBlank } from "../../util/util";
import mustache from "mustache";
import * as DOMPurify from "dompurify";
import { GlobalModel } from "../../model/model";
import { GlobalModel } from "../../models";
import "./mustache.less";
@ -17,7 +17,13 @@ type OV<V> = mobx.IObservableValue<V>;
@mobxReact.observer
class SimpleMustacheRenderer extends React.Component<
{ data: T.ExtBlob; context: T.RendererContext; opts: T.RendererOpts; savedHeight: number; lineState: T.LineStateType },
{
data: T.ExtBlob;
context: T.RendererContext;
opts: T.RendererOpts;
savedHeight: number;
lineState: T.LineStateType;
},
{}
> {
templateLoading: OV<boolean> = mobx.observable.box(true, { name: "templateLoading" });

View File

@ -9,7 +9,8 @@ import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import { windowWidthToCols, windowHeightToRows } from "../../util/textmeasure";
import { boundInt } from "../../util/util";
import { GlobalModel } from "../../model/model"
import { GlobalModel } from "../../models";
import { Model } from "../../models/model";
import type {
TermContextUnion,
TermOptsType,
@ -99,21 +100,23 @@ class TermWrap {
fontFamily: "JetBrains Mono",
theme: { foreground: terminal.foreground, background: terminal.background },
});
this.terminal.loadAddon(new WebLinksAddon((e, uri) => {
e.preventDefault();
switch (GlobalModel.platform) {
case "darwin":
if (e.metaKey) {
GlobalModel.openExternalLink(uri);
}
break;
default:
if (e.ctrlKey) {
GlobalModel.openExternalLink(uri);
}
break;
}
}));
this.terminal.loadAddon(
new WebLinksAddon((e, uri) => {
e.preventDefault();
switch (GlobalModel.platform) {
case "darwin":
if (e.metaKey) {
GlobalModel.openExternalLink(uri);
}
break;
default:
if (e.ctrlKey) {
GlobalModel.openExternalLink(uri);
}
break;
}
})
);
this.terminal._core._inputHandler._parser.setErrorHandler((state) => {
this.numParseErrors++;
return state;

View File

@ -8,9 +8,9 @@ import { boundMethod } from "autobind-decorator";
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { If } from "tsx-control-statements/components";
import { GlobalModel, LineContainerModel } from "../../model/model";
import { GlobalModel } from "../../models";
import { termHeightFromRows } from "../../util/textmeasure";
import type { LineType } from "../../types/types";
import type { LineType, LineContainerType } from "../../types/types";
import cn from "classnames";
import * as lineutil from "../../app/line/lineutil";
@ -25,7 +25,7 @@ type OMap<K, V> = mobx.ObservableMap<K, V>;
@mobxReact.observer
class TerminalRenderer extends React.Component<
{
screen: LineContainerModel;
screen: LineContainerType;
line: LineType;
width: number;
staticRender: boolean;

View File

@ -3,6 +3,8 @@
import * as React from "react";
import * as mobx from "mobx";
import { TermWrap } from "../plugins/terminal/term";
import { Cmd, Model } from "../models";
type ShareModeType = "local" | "web";
type FocusTypeStrs = "input" | "cmd";
@ -754,10 +756,39 @@ type StrWithPos = {
pos: number;
};
type LineFocusType = {
cmdInputFocus: boolean;
lineid?: string;
linenum?: number;
screenid?: string;
};
type LineContainerType = {
loadTerminalRenderer: (elem: Element, line: LineType, cmd: Cmd, width: number) => void;
registerRenderer: (lineId: string, renderer: RendererModel) => void;
unloadRenderer: (lineId: string) => void;
getIsFocused: (lineNum: number) => boolean;
getTermWrap: (lineId: string) => TermWrap;
getRenderer: (lineId: string) => RendererModel;
getFocusType: () => FocusTypeStrs;
getSelectedLine: () => number;
getCmd: (line: LineType) => Cmd;
setLineFocus: (lineNum: number, focus: boolean) => void;
getUsedRows: (context: RendererContext, line: LineType, cmd: Cmd, width: number) => number;
getContentHeight: (context: RendererContext) => number;
setContentHeight: (context: RendererContext, height: number) => void;
getMaxContentSize(): WindowSize;
getIdealContentSize(): WindowSize;
isSidebarOpen(): boolean;
isLineIdInSidebar(lineId: string): boolean;
getContainerType(): LineContainerStrs;
};
export type {
SessionDataType,
LineStateType,
LineType,
LineFocusType,
RemoteType,
RemoteStateType,
RemoteInstanceType,
@ -836,6 +867,10 @@ export type {
OpenAICmdInfoChatMessageType,
ScreenStatusIndicatorUpdateType,
OV,
OArr,
OMap,
CV,
LineContainerType,
};
export { StatusIndicatorLevel };

80
src/util/modelutil.ts Normal file
View File

@ -0,0 +1,80 @@
import { sprintf } from "sprintf-js";
import { GlobalModel } from "../models";
import { RemotePtrType, FeCmdPacketType, PtyDataType, TermContextUnion } from "../types/types";
import { isBlank } from "./util";
function getTermPtyData(termContext: TermContextUnion): Promise<PtyDataType> {
if ("remoteId" in termContext) {
return getRemotePtyData(termContext.remoteId);
}
return getPtyData(termContext.screenId, termContext.lineId, termContext.lineNum);
}
function getPtyData(screenId: string, lineId: string, lineNum: number): Promise<PtyDataType> {
let url = sprintf(
GlobalModel.getBaseHostPort() + "/api/ptyout?linenum=%d&screenid=%s&lineid=%s",
lineNum,
screenId,
lineId
);
return getPtyDataFromUrl(url);
}
function getRemotePtyData(remoteId: string): Promise<PtyDataType> {
let url = sprintf(GlobalModel.getBaseHostPort() + "/api/remote-pty?remoteid=%s", remoteId);
return getPtyDataFromUrl(url);
}
function getPtyDataFromUrl(url: string): Promise<PtyDataType> {
let ptyOffset = 0;
let fetchHeaders = GlobalModel.getFetchHeaders();
return fetch(url, { headers: fetchHeaders })
.then((resp) => {
if (!resp.ok) {
throw new Error(sprintf("Bad fetch response for /api/ptyout: %d %s", resp.status, resp.statusText));
}
let ptyOffsetStr = resp.headers.get("X-PtyDataOffset");
if (ptyOffsetStr != null && !isNaN(parseInt(ptyOffsetStr))) {
ptyOffset = parseInt(ptyOffsetStr);
}
return resp.arrayBuffer();
})
.then((buf) => {
return { pos: ptyOffset, data: new Uint8Array(buf) };
});
}
function remotePtrToString(rptr: RemotePtrType): string {
if (rptr == null || isBlank(rptr.remoteid)) {
return null;
}
if (isBlank(rptr.ownerid) && isBlank(rptr.name)) {
return rptr.remoteid;
}
if (!isBlank(rptr.ownerid) && isBlank(rptr.name)) {
return sprintf("@%s:%s", rptr.ownerid, rptr.remoteid);
}
if (isBlank(rptr.ownerid) && !isBlank(rptr.name)) {
return sprintf("%s:%s", rptr.remoteid, rptr.name);
}
return sprintf("@%s:%s:%s", rptr.ownerid, rptr.remoteid, rptr.name);
}
function cmdPacketString(pk: FeCmdPacketType): string {
let cmd = pk.metacmd;
if (pk.metasubcmd != null) {
cmd += ":" + pk.metasubcmd;
}
let parts = [cmd];
if (pk.kwargs != null) {
for (let key in pk.kwargs) {
parts.push(sprintf("%s=%s", key, pk.kwargs[key]));
}
}
if (pk.args != null) {
parts.push(...pk.args);
}
return parts.join(" ");
}
export { getTermPtyData, remotePtrToString, cmdPacketString };

View File

@ -72,13 +72,13 @@ function handleJsonFetchResponse(url: URL, resp: any): Promise<any> {
}
function base64ToString(b64: string): string {
let stringBytes = base64.toByteArray(b64)
return new TextDecoder().decode(stringBytes)
let stringBytes = base64.toByteArray(b64);
return new TextDecoder().decode(stringBytes);
}
function stringToBase64(input: string): string {
let stringBytes = new TextEncoder().encode(input)
return base64.fromByteArray(stringBytes)
let stringBytes = new TextEncoder().encode(input);
return base64.fromByteArray(stringBytes);
}
function base64ToArray(b64: string): Uint8Array {
@ -403,6 +403,14 @@ function getRemoteName(remote: RemoteType): string {
return remotealias ? `${remotealias} [${remotecanonicalname}]` : remotecanonicalname;
}
// clean empty string
function ces(s: string) {
if (s == "") {
return null;
}
return s;
}
export {
handleJsonFetchResponse,
base64ToString,
@ -429,4 +437,5 @@ export {
commandRtnHandler,
getRemoteConnVal,
getRemoteName,
ces,
};