diff --git a/src/app/app.tsx b/src/app/app.tsx index cdea6288..2d11394c 100644 --- a/src/app/app.tsx +++ b/src/app/app.tsx @@ -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 = mobx.observable.box(false, { name: "dcWait" }); mainContentRef: React.RefObject = React.createRef(); - constructor(props: any) { + constructor(props: {}) { super(props); if (GlobalModel.isDev) document.body.className = "is-dev"; } diff --git a/src/app/appconst.ts b/src/app/appconst.ts index 69bcf604..6a0df44b 100644 --- a/src/app/appconst.ts +++ b/src/app/appconst.ts @@ -16,3 +16,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__; diff --git a/src/app/bookmarks/bookmarks.tsx b/src/app/bookmarks/bookmarks.tsx index 9dd75e1b..6809ff3b 100644 --- a/src/app/bookmarks/bookmarks.tsx +++ b/src/app/bookmarks/bookmarks.tsx @@ -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 { + 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(); diff --git a/src/app/clientsettings/clientsettings.tsx b/src/app/clientsettings/clientsettings.tsx index 21807c0e..3786e594 100644 --- a/src/app/clientsettings/clientsettings.tsx +++ b/src/app/clientsettings/clientsettings.tsx @@ -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 = mobx.IObservableValue; - -// @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 = mobx.observable.box(false, { name: "clientSettings-fontSizeDropdownActive" }); - errorMessage: OV = mobx.observable.box(null, { name: "ClientSettings-errorMessage" }); + fontSizeDropdownActive: types.OV = mobx.observable.box(false, { + name: "clientSettings-fontSizeDropdownActive", + }); + errorMessage: types.OV = 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 = null; + let prtn: Promise = 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 = null; + let prtn: Promise = 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
Client Version
- {VERSION} {BUILD} + {appconst.VERSION} {appconst.BUILD}
diff --git a/src/app/common/elements/markdown.tsx b/src/app/common/elements/markdown.tsx index 66804d2c..89ea05a3 100644 --- a/src/app/common/elements/markdown.tsx +++ b/src/app/common/elements/markdown.tsx @@ -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"; diff --git a/src/app/common/elements/resizablesidebar.tsx b/src/app/common/elements/resizablesidebar.tsx index 5617f9c5..e34f7424 100644 --- a/src/app/common/elements/resizablesidebar.tsx +++ b/src/app/common/elements/resizablesidebar.tsx @@ -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"; diff --git a/src/app/common/elements/showwaveshellinstallprompt.tsx b/src/app/common/elements/showwaveshellinstallprompt.tsx index 34fbb0e1..42679ecd 100644 --- a/src/app/common/elements/showwaveshellinstallprompt.tsx +++ b/src/app/common/elements/showwaveshellinstallprompt.tsx @@ -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) { diff --git a/src/app/common/modals/about.tsx b/src/app/common/modals/about.tsx index c74477b9..c8ab58f7 100644 --- a/src/app/common/modals/about.tsx +++ b/src/app/common/modals/about.tsx @@ -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 (
- Client Version {VERSION} ({BUILD}) + Client Version {appconst.VERSION} ({appconst.BUILD})
); @@ -55,7 +51,7 @@ class AboutModal extends React.Component<{}, {}> { Up to Date
- Client Version {VERSION} ({BUILD}) + Client Version {appconst.VERSION} ({appconst.BUILD})
); @@ -67,7 +63,7 @@ class AboutModal extends React.Component<{}, {}> { Outdated Version
- Client Version {VERSION} ({BUILD}) + Client Version {appconst.VERSION} ({appconst.BUILD})
|
- +
{ {screen.getTabIcon()}
|
- +
= mobx.IObservableArray; type OMap = mobx.ObservableMap; type RendererComponentProps = { - screen: LineContainerModel; + screen: LineContainerType; line: LineType; width: number; staticRender: boolean; diff --git a/src/app/connections/connections.tsx b/src/app/connections/connections.tsx index c21b38ed..0d01f703 100644 --- a/src/app/connections/connections.tsx +++ b/src/app/connections/connections.tsx @@ -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"; diff --git a/src/app/history/history.tsx b/src/app/history/history.tsx index 31bd7d31..ddb9ffb1 100644 --- a/src/app/history/history.tsx +++ b/src/app/history/history.tsx @@ -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"; diff --git a/src/app/line/linecomps.tsx b/src/app/line/linecomps.tsx index c6c43ced..43aa2a58 100644 --- a/src/app/line/linecomps.tsx +++ b/src/app/line/linecomps.tsx @@ -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; diff --git a/src/app/line/renderer/basicrenderer.tsx b/src/app/line/renderer/basicrenderer.tsx index df760944..e704e115 100644 --- a/src/app/line/renderer/basicrenderer.tsx +++ b/src/app/line/renderer/basicrenderer.tsx @@ -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 = mobx.IObservableValue; type CV = mobx.IComputedValue; @@ -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} diff --git a/src/app/pluginsview/pluginsview.tsx b/src/app/pluginsview/pluginsview.tsx index cdb1faf4..377d8d66 100644 --- a/src/app/pluginsview/pluginsview.tsx +++ b/src/app/pluginsview/pluginsview.tsx @@ -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"; diff --git a/src/app/sidebar/sidebar.tsx b/src/app/sidebar/sidebar.tsx index 26fe7219..af236370 100644 --- a/src/app/sidebar/sidebar.tsx +++ b/src/app/sidebar/sidebar.tsx @@ -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 { 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 { 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(); diff --git a/src/app/workspace/cmdinput/aichat.tsx b/src/app/workspace/cmdinput/aichat.tsx index 6692cd11..8bc3471f 100644 --- a/src/app/workspace/cmdinput/aichat.tsx +++ b/src/app/workspace/cmdinput/aichat.tsx @@ -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"; diff --git a/src/app/workspace/cmdinput/cmdinput.tsx b/src/app/workspace/cmdinput/cmdinput.tsx index a183d3f4..1b1789a8 100644 --- a/src/app/workspace/cmdinput/cmdinput.tsx +++ b/src/app/workspace/cmdinput/cmdinput.tsx @@ -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"; diff --git a/src/app/workspace/cmdinput/historyinfo.tsx b/src/app/workspace/cmdinput/historyinfo.tsx index 59874ee0..31f70ef1 100644 --- a/src/app/workspace/cmdinput/historyinfo.tsx +++ b/src/app/workspace/cmdinput/historyinfo.tsx @@ -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); diff --git a/src/app/workspace/cmdinput/infomsg.tsx b/src/app/workspace/cmdinput/infomsg.tsx index 7552c9f9..b1a9d1ed 100644 --- a/src/app/workspace/cmdinput/infomsg.tsx +++ b/src/app/workspace/cmdinput/infomsg.tsx @@ -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); diff --git a/src/app/workspace/cmdinput/textareainput.tsx b/src/app/workspace/cmdinput/textareainput.tsx index dc1a119f..3f8c0318 100644 --- a/src/app/workspace/cmdinput/textareainput.tsx +++ b/src/app/workspace/cmdinput/textareainput.tsx @@ -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"; diff --git a/src/app/workspace/screen/screenview.tsx b/src/app/workspace/screen/screenview.tsx index 19c7cf6a..02e0c4db 100644 --- a/src/app/workspace/screen/screenview.tsx +++ b/src/app/workspace/screen/screenview.tsx @@ -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 }, {}> {
this.selectTabIcon("square")}>
- +
{ <>
Select the color
- +
= mobx.observable.array([], { + name: "Bookmarks", + }); + activeBookmark: OV = mobx.observable.box(null, { + name: "activeBookmark", + }); + editingBookmark: OV = mobx.observable.box(null, { + name: "editingBookmark", + }); + pendingDelete: OV = mobx.observable.box(null, { + name: "pendingDelete", + }); + copiedIndicator: OV = mobx.observable.box(null, { + name: "copiedIndicator", + }); + + tempDesc: OV = mobx.observable.box("", { + name: "bookmarkEdit-tempDesc", + }); + tempCmd: OV = 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 }; diff --git a/src/models/clientsettingsview.ts b/src/models/clientsettingsview.ts new file mode 100644 index 00000000..ad177e7a --- /dev/null +++ b/src/models/clientsettingsview.ts @@ -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 }; diff --git a/src/models/cmd.ts b/src/models/cmd.ts new file mode 100644 index 00000000..d9ba8e1a --- /dev/null +++ b/src/models/cmd.ts @@ -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; + + 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 { + 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 }; diff --git a/src/models/commandrunner.ts b/src/models/commandrunner.ts new file mode 100644 index 00000000..2140a784 --- /dev/null +++ b/src/models/commandrunner.ts @@ -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 { + 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 { + let kwargs = { nohist: "1" }; + let archiveStr = archive ? "1" : "0"; + return GlobalModel.submitCommand("line", "archive", [lineArg, archiveStr], kwargs, false); + } + + lineDelete(lineArg: string, interactive: boolean): Promise { + return GlobalModel.submitCommand("line", "delete", [lineArg], { nohist: "1" }, interactive); + } + + lineRestart(lineArg: string, interactive: boolean): Promise { + return GlobalModel.submitCommand("line", "restart", [lineArg], { nohist: "1" }, interactive); + } + + lineSet(lineArg: string, opts: { renderer?: string }): Promise { + 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 = { + 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 { + return GlobalModel.submitCommand( + "screen", + "archive", + [screenId, shouldArchive ? "1" : "0"], + { nohist: "1" }, + false + ); + } + + screenDelete(screenId: string, interactive: boolean): Promise { + return GlobalModel.submitCommand("screen", "delete", [screenId], { nohist: "1" }, interactive); + } + + screenWebShare(screenId: string, shouldShare: boolean): Promise { + let kwargs: Record = { 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, interactive: boolean): Promise { + 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 { + let kwargs = {}; + if (nohist) { + kwargs["nohist"] = "1"; + } + return GlobalModel.submitCommand("connect", null, [remoteArg], kwargs, interactive); + } + + editRemote(remoteid: string, kwargsArg: Record): 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 = { + 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 = { + nohist: "1", + screenId: screenId, + index: index, + }; + GlobalModel.submitCommand("screen", "reorder", null, kwargs, false); + } + + setTermUsedRows(termContext: RendererContext, height: number) { + let kwargs: Record = {}; + 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 { + 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 { + return GlobalModel.submitCommand( + "session", + "archive", + [sessionId, shouldArchive ? "1" : "0"], + { nohist: "1" }, + false + ); + } + + sessionDelete(sessionId: string): Promise { + return GlobalModel.submitCommand("session", "delete", [sessionId], { nohist: "1" }, false); + } + + sessionSetSettings(sessionId: string, settings: { name?: string }, interactive: boolean): Promise { + 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 { + return GlobalModel.submitCommand("telemetry", "off", null, { nohist: "1" }, interactive); + } + + telemetryOn(interactive: boolean): Promise { + return GlobalModel.submitCommand("telemetry", "on", null, { nohist: "1" }, interactive); + } + + releaseCheckAutoOff(interactive: boolean): Promise { + return GlobalModel.submitCommand("releasecheck", "autooff", null, { nohist: "1" }, interactive); + } + + releaseCheckAutoOn(interactive: boolean): Promise { + return GlobalModel.submitCommand("releasecheck", "autoon", null, { nohist: "1" }, interactive); + } + + setTermFontSize(fsize: number, interactive: boolean): Promise { + let kwargs = { + nohist: "1", + termfontsize: String(fsize), + }; + return GlobalModel.submitCommand("client", "set", null, kwargs, interactive); + } + + setClientOpenAISettings(opts: { model?: string; apitoken?: string; maxtokens?: string }): Promise { + 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 { + let kwargs = { nohist: "1" }; + let valueStr = value ? "1" : "0"; + return GlobalModel.submitCommand("client", "setconfirmflag", [flag, valueStr], kwargs, false); + } + + clientSetSidebar(width: number, collapsed: boolean): Promise { + 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 { + 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 = { nohist: "1" }; + if (width != null) { + kwargs.width = width; + } + GlobalModel.submitCommand("sidebar", "open", null, kwargs, false); + } +} + +export { CommandRunner }; diff --git a/src/models/connectionsview.ts b/src/models/connectionsview.ts new file mode 100644 index 00000000..6d819b88 --- /dev/null +++ b/src/models/connectionsview.ts @@ -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 }; diff --git a/src/models/forwardlinecontainer.ts b/src/models/forwardlinecontainer.ts new file mode 100644 index 00000000..b84c039d --- /dev/null +++ b/src/models/forwardlinecontainer.ts @@ -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 }; diff --git a/src/models/global.ts b/src/models/global.ts new file mode 100644 index 00000000..91c26326 --- /dev/null +++ b/src/models/global.ts @@ -0,0 +1,6 @@ +import { Model } from "./model"; +import { CommandRunner } from "./commandrunner"; + +const GlobalModel = Model.getInstance(); +const GlobalCommandRunner = CommandRunner.getInstance(); +export { GlobalModel, GlobalCommandRunner }; diff --git a/src/models/historyview.ts b/src/models/historyview.ts new file mode 100644 index 00000000..d3a5aa5b --- /dev/null +++ b/src/models/historyview.ts @@ -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 = mobx.observable.array([], { + name: "HistoryItems", + }); + hasMore: OV = mobx.observable.box(false, { + name: "historyview-hasmore", + }); + offset: OV = mobx.observable.box(0, { name: "historyview-offset" }); + searchText: OV = mobx.observable.box("", { + name: "historyview-searchtext", + }); + activeSearchText: string = null; + selectedItems: OMap = mobx.observable.map({}, { name: "historyview-selectedItems" }); + deleteActive: OV = mobx.observable.box(false, { + name: "historyview-deleteActive", + }); + activeItem: OV = mobx.observable.box(null, { + name: "historyview-activeItem", + }); + searchSessionId: OV = mobx.observable.box(null, { + name: "historyview-searchSessionId", + }); + searchRemoteId: OV = mobx.observable.box(null, { + name: "historyview-searchRemoteId", + }); + searchShowMeta: OV = mobx.observable.box(true, { + name: "historyview-searchShowMeta", + }); + searchFromDate: OV = mobx.observable.box(null, { + name: "historyview-searchfromts", + }); + searchFilterCmds: OV = 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 }; diff --git a/src/models/index.ts b/src/models/index.ts new file mode 100644 index 00000000..d4d05197 --- /dev/null +++ b/src/models/index.ts @@ -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"; diff --git a/src/models/input.ts b/src/models/input.ts new file mode 100644 index 00000000..db35b20d --- /dev/null +++ b/src/models/input.ts @@ -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 = mobx.observable.box(false); + infoShow: OV = mobx.observable.box(false); + aIChatShow: OV = mobx.observable.box(false); + cmdInputHeight: OV = mobx.observable.box(0); + aiChatTextAreaRef: React.RefObject; + aiChatWindowRef: React.RefObject; + codeSelectBlockRefArray: Array>; + codeSelectSelectedIndex: OV = mobx.observable.box(-1); + + AICmdInfoChatItems: mobx.IObservableArray = mobx.observable.array([], { + name: "aicmdinfo-chat", + }); + readonly codeSelectTop: number = -2; + readonly codeSelectBottom: number = -1; + + historyType: mobx.IObservableValue = mobx.observable.box("screen"); + historyLoading: mobx.IObservableValue = mobx.observable.box(false); + historyAfterLoadIndex: number = 0; + historyItems: mobx.IObservableValue = mobx.observable.box(null, { + name: "history-items", + deep: false, + }); // sorted in reverse (most recent is index 0) + filteredHistoryItems: mobx.IComputedValue = null; + historyIndex: mobx.IObservableValue = mobx.observable.box(0, { + name: "history-index", + }); // 1-indexed (because 0 is current) + modHistory: mobx.IObservableArray = mobx.observable.array([""], { + name: "mod-history", + }); + historyQueryOpts: OV = mobx.observable.box(getDefaultHistoryQueryOpts()); + + infoMsg: OV = mobx.observable.box(null); + infoTimeoutId: any = null; + inputMode: OV = mobx.observable.box(null); + inputExpanded: OV = mobx.observable.box(false, { + name: "inputExpanded", + }); + + // cursor + forceCursorPos: OV = mobx.observable.box(null); + + // focus + inputFocused: OV = mobx.observable.box(false); + lineFocused: OV = mobx.observable.box(false); + physicalInputFocused: OV = 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, + chatWindowRef: React.RefObject + ) { + 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): 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 }; diff --git a/src/models/mainsidebar.ts b/src/models/mainsidebar.ts new file mode 100644 index 00000000..90769304 --- /dev/null +++ b/src/models/mainsidebar.ts @@ -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 = mobx.observable.box(null, { + name: "MainSidebarModel-tempWidth", + }); + tempCollapsed: OV = mobx.observable.box(null, { + name: "MainSidebarModel-tempCollapsed", + }); + isDragging: OV = 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 }; diff --git a/src/models/modals.ts b/src/models/modals.ts new file mode 100644 index 00000000..faae94ba --- /dev/null +++ b/src/models/modals.ts @@ -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 = mobx.observable.array([], { name: "ModalsModel-store" }); + + pushModal(modalId: string) { + const modalFactory = modalsRegistry[modalId]; + + if (modalFactory && !this.store.some((modal) => modal.id === modalId)) { + mobx.action(() => { + this.store.push({ id: modalId, component: modalFactory, uniqueKey: uuidv4() }); + })(); + } + } + + popModal() { + mobx.action(() => { + this.store.pop(); + })(); + } +} + +export { ModalsModel }; diff --git a/src/models/model.ts b/src/models/model.ts new file mode 100644 index 00000000..8b281617 --- /dev/null +++ b/src/models/model.ts @@ -0,0 +1,1420 @@ +// Copyright 2023, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import * as mobx from "mobx"; +import { sprintf } from "sprintf-js"; +import { + handleJsonFetchResponse, + base64ToString, + base64ToArray, + genMergeData, + genMergeDataMap, + genMergeSimpleData, + isModKeyPress, + isBlank, +} from "../util/util"; +import { + SessionDataType, + LineType, + RemoteType, + RemoteInstanceType, + CmdDataType, + FeCmdPacketType, + ScreenDataType, + PtyDataUpdateType, + ModelUpdateType, + UpdateMessage, + InfoType, + StrWithPos, + UIContextType, + ContextMenuOpts, + RendererContext, + ClientDataType, + AlertMessageType, + ScreenLinesType, + RemoteViewType, + CommandRtnType, + LineFocusType, + CmdInputTextPacketType, + FileInfoType, + ExtFile, + HistorySearchParams, + LineStateType, +} from "../types/types"; +import { WSControl } from "./ws"; +import { cmdStatusIsRunning } from "../app/line/lineutil"; +import * as appconst from "../app/appconst"; +import { remotePtrToString, cmdPacketString } from "../util/modelutil"; +import { checkKeyPressed, adaptFromReactOrNativeKeyEvent, setKeyUtilPlatform } from "../util/keyutil"; +import { OV, OArr, OMap, CV } from "../types/types"; +import { Session } from "./session"; +import { ScreenLines } from "./screenlines"; +import { InputModel } from "./input"; +import { PluginsModel } from "./plugins"; +import { BookmarksModel } from "./bookmarks"; +import { HistoryViewModel } from "./historyview"; +import { ConnectionsViewModel } from "./connectionsview"; +import { ClientSettingsViewModel } from "./clientsettingsview"; +import { RemotesModel } from "./remotes"; +import { ModalsModel } from "./modals"; +import { MainSidebarModel } from "./mainsidebar"; +import { Screen } from "./screen"; +import { Cmd } from "./cmd"; +import { GlobalCommandRunner } from "./global"; + +type KeyModsType = { + meta?: boolean; + ctrl?: boolean; + alt?: boolean; + shift?: boolean; +}; + +type SWLinePtr = { + line: LineType; + slines: ScreenLines; + screen: Screen; +}; + +type ElectronApi = { + getId: () => string; + getIsDev: () => boolean; + getPlatform: () => string; + getAuthKey: () => string; + getWaveSrvStatus: () => boolean; + restartWaveSrv: () => boolean; + reloadWindow: () => void; + openExternalLink: (url: string) => void; + onTCmd: (callback: (mods: KeyModsType) => void) => void; + onICmd: (callback: (mods: KeyModsType) => void) => void; + onLCmd: (callback: (mods: KeyModsType) => void) => void; + onHCmd: (callback: (mods: KeyModsType) => void) => void; + onPCmd: (callback: (mods: KeyModsType) => void) => void; + onRCmd: (callback: (mods: KeyModsType) => void) => void; + onWCmd: (callback: (mods: KeyModsType) => void) => void; + onMenuItemAbout: (callback: () => void) => void; + onMetaArrowUp: (callback: () => void) => void; + onMetaArrowDown: (callback: () => void) => void; + onMetaPageUp: (callback: () => void) => void; + onMetaPageDown: (callback: () => void) => void; + onBracketCmd: (callback: (event: any, arg: { relative: number }, mods: KeyModsType) => void) => void; + onDigitCmd: (callback: (event: any, arg: { digit: number }, mods: KeyModsType) => void) => void; + contextScreen: (screenOpts: { screenId: string }, position: { x: number; y: number }) => void; + contextEditMenu: (position: { x: number; y: number }, opts: ContextMenuOpts) => void; + onWaveSrvStatusChange: (callback: (status: boolean, pid: number) => void) => void; + getLastLogs: (numOfLines: number, callback: (logs: any) => void) => void; +}; + +function getApi(): ElectronApi { + return (window as any).api; +} + +class Model { + clientId: string; + activeSessionId: OV = mobx.observable.box(null, { + name: "activeSessionId", + }); + sessionListLoaded: OV = mobx.observable.box(false, { + name: "sessionListLoaded", + }); + sessionList: OArr = mobx.observable.array([], { + name: "SessionList", + deep: false, + }); + screenMap: OMap = mobx.observable.map({}, { name: "ScreenMap", deep: false }); + ws: WSControl; + remotes: OArr = mobx.observable.array([], { + name: "remotes", + deep: false, + }); + remotesLoaded: OV = mobx.observable.box(false, { + name: "remotesLoaded", + }); + screenLines: OMap = mobx.observable.map({}, { name: "screenLines", deep: false }); // key = "sessionid/screenid" (screenlines) + termUsedRowsCache: Record = {}; // key = "screenid/lineid" + debugCmds: number = 0; + debugScreen: OV = mobx.observable.box(false); + waveSrvRunning: OV; + authKey: string; + isDev: boolean; + platform: string; + activeMainView: OV< + "plugins" | "session" | "history" | "bookmarks" | "webshare" | "connections" | "clientsettings" + > = mobx.observable.box("session", { + name: "activeMainView", + }); + termFontSize: CV; + alertMessage: OV = mobx.observable.box(null, { + name: "alertMessage", + }); + alertPromiseResolver: (result: boolean) => void; + aboutModalOpen: OV = mobx.observable.box(false, { + name: "aboutModalOpen", + }); + screenSettingsModal: OV<{ sessionId: string; screenId: string }> = mobx.observable.box(null, { + name: "screenSettingsModal", + }); + sessionSettingsModal: OV = mobx.observable.box(null, { + name: "sessionSettingsModal", + }); + clientSettingsModal: OV = mobx.observable.box(false, { + name: "clientSettingsModal", + }); + lineSettingsModal: OV = mobx.observable.box(null, { + name: "lineSettingsModal", + }); // linenum + remotesModel: RemotesModel; + + inputModel: InputModel; + pluginsModel: PluginsModel; + bookmarksModel: BookmarksModel; + historyViewModel: HistoryViewModel; + connectionViewModel: ConnectionsViewModel; + clientSettingsViewModel: ClientSettingsViewModel; + modalsModel: ModalsModel; + mainSidebarModel: MainSidebarModel; + clientData: OV = mobx.observable.box(null, { + name: "clientData", + }); + showLinks: OV = mobx.observable.box(true, { + name: "model-showLinks", + }); + packetSeqNum: number = 0; + + private constructor() { + this.clientId = getApi().getId(); + this.isDev = getApi().getIsDev(); + this.authKey = getApi().getAuthKey(); + this.ws = new WSControl(this.getBaseWsHostPort(), this.clientId, this.authKey, (message: any) => { + let interactive = message?.interactive ?? false; + this.runUpdate(message, interactive); + }); + this.ws.reconnect(); + this.inputModel = new InputModel(this); + this.pluginsModel = new PluginsModel(this); + this.bookmarksModel = new BookmarksModel(this); + this.historyViewModel = new HistoryViewModel(this); + this.connectionViewModel = new ConnectionsViewModel(this); + this.clientSettingsViewModel = new ClientSettingsViewModel(this); + this.remotesModel = new RemotesModel(this); + this.modalsModel = new ModalsModel(); + this.mainSidebarModel = new MainSidebarModel(this); + let isWaveSrvRunning = getApi().getWaveSrvStatus(); + this.waveSrvRunning = mobx.observable.box(isWaveSrvRunning, { + name: "model-wavesrv-running", + }); + this.platform = this.getPlatform(); + this.termFontSize = mobx.computed(() => { + let cdata = this.clientData.get(); + if (cdata == null || cdata.feopts == null || cdata.feopts.termfontsize == null) { + return appconst.DefaultTermFontSize; + } + let fontSize = Math.ceil(cdata.feopts.termfontsize); + if (fontSize < appconst.MinFontSize) { + return appconst.MinFontSize; + } + if (fontSize > appconst.MaxFontSize) { + return appconst.MaxFontSize; + } + return fontSize; + }); + getApi().onTCmd(this.onTCmd.bind(this)); + getApi().onICmd(this.onICmd.bind(this)); + getApi().onLCmd(this.onLCmd.bind(this)); + getApi().onHCmd(this.onHCmd.bind(this)); + getApi().onPCmd(this.onPCmd.bind(this)); + getApi().onWCmd(this.onWCmd.bind(this)); + getApi().onRCmd(this.onRCmd.bind(this)); + getApi().onMenuItemAbout(this.onMenuItemAbout.bind(this)); + getApi().onMetaArrowUp(this.onMetaArrowUp.bind(this)); + getApi().onMetaArrowDown(this.onMetaArrowDown.bind(this)); + getApi().onMetaPageUp(this.onMetaPageUp.bind(this)); + getApi().onMetaPageDown(this.onMetaPageDown.bind(this)); + getApi().onBracketCmd(this.onBracketCmd.bind(this)); + getApi().onDigitCmd(this.onDigitCmd.bind(this)); + getApi().onWaveSrvStatusChange(this.onWaveSrvStatusChange.bind(this)); + document.addEventListener("keydown", this.docKeyDownHandler.bind(this)); + document.addEventListener("selectionchange", this.docSelectionChangeHandler.bind(this)); + setTimeout(() => this.getClientDataLoop(1), 10); + } + + static getInstance() { + if (!(window as any).GlobalModel) { + (window as any).GlobalModel = new Model(); + } + return (window as any).GlobalModel; + } + + getNextPacketSeqNum(): number { + this.packetSeqNum++; + return this.packetSeqNum; + } + + getPlatform(): string { + if (this.platform != null) { + return this.platform; + } + this.platform = getApi().getPlatform(); + setKeyUtilPlatform(this.platform); + return this.platform; + } + + testGlobalModel() { + return ""; + } + + needsTos(): boolean { + let cdata = this.clientData.get(); + if (cdata == null) { + return false; + } + return cdata.clientopts == null || !cdata.clientopts.acceptedtos; + } + + refreshClient(): void { + getApi().reloadWindow(); + } + + /** + * Opens a new default browser window to the given url + * @param {string} url The url to open + */ + openExternalLink(url: string): void { + console.log("opening external link: " + url); + getApi().openExternalLink(url); + console.log("finished opening external link"); + } + + refocus() { + // givefocus() give back focus to cmd or input + let activeScreen = this.getActiveScreen(); + if (screen == null) { + return; + } + activeScreen.giveFocus(); + } + + getWebSharedScreens(): Screen[] { + let rtn: Screen[] = []; + for (let screen of this.screenMap.values()) { + if (screen.shareMode.get() == "web") { + rtn.push(screen); + } + } + return rtn; + } + + getHasClientStop(): boolean { + if (this.clientData.get() == null) { + return true; + } + let cdata = this.clientData.get(); + if (cdata.cmdstoretype == "session") { + return true; + } + return false; + } + + showAlert(alertMessage: AlertMessageType): Promise { + if (alertMessage.confirmflag != null) { + let cdata = this.clientData.get(); + let noConfirm = cdata.clientopts?.confirmflags?.[alertMessage.confirmflag]; + if (noConfirm) { + return Promise.resolve(true); + } + } + mobx.action(() => { + this.alertMessage.set(alertMessage); + this.modalsModel.pushModal(appconst.ALERT); + })(); + let prtn = new Promise((resolve, reject) => { + this.alertPromiseResolver = resolve; + }); + return prtn; + } + + cancelAlert(): void { + mobx.action(() => { + this.alertMessage.set(null); + this.modalsModel.popModal(); + })(); + if (this.alertPromiseResolver != null) { + this.alertPromiseResolver(false); + this.alertPromiseResolver = null; + } + } + + confirmAlert(): void { + mobx.action(() => { + this.alertMessage.set(null); + this.modalsModel.popModal(); + })(); + if (this.alertPromiseResolver != null) { + this.alertPromiseResolver(true); + this.alertPromiseResolver = null; + } + } + + showSessionView(): void { + mobx.action(() => { + this.activeMainView.set("session"); + })(); + } + + showWebShareView(): void { + mobx.action(() => { + this.activeMainView.set("webshare"); + })(); + } + + getBaseHostPort(): string { + if (this.isDev) { + return appconst.DevServerEndpoint; + } + return appconst.ProdServerEndpoint; + } + + setTermFontSize(fontSize: number) { + if (fontSize < appconst.MinFontSize) { + fontSize = appconst.MinFontSize; + } + if (fontSize > appconst.MaxFontSize) { + fontSize = appconst.MaxFontSize; + } + mobx.action(() => { + this.termFontSize.set(fontSize); + })(); + } + + getBaseWsHostPort(): string { + if (this.isDev) { + return appconst.DevServerWsEndpoint; + } + return appconst.ProdServerWsEndpoint; + } + + getFetchHeaders(): Record { + return { + "x-authkey": this.authKey, + }; + } + + docSelectionChangeHandler(e: any) { + // nothing for now + } + + docKeyDownHandler(e: KeyboardEvent) { + let waveEvent = adaptFromReactOrNativeKeyEvent(e); + if (isModKeyPress(e)) { + return; + } + if (this.alertMessage.get() != null) { + if (checkKeyPressed(waveEvent, "Escape")) { + e.preventDefault(); + this.cancelAlert(); + return; + } + if (checkKeyPressed(waveEvent, "Enter")) { + e.preventDefault(); + this.confirmAlert(); + return; + } + return; + } + if (this.activeMainView.get() == "bookmarks") { + this.bookmarksModel.handleDocKeyDown(e); + return; + } + if (this.activeMainView.get() == "history") { + this.historyViewModel.handleDocKeyDown(e); + return; + } + if (this.activeMainView.get() == "connections") { + this.historyViewModel.handleDocKeyDown(e); + return; + } + if (this.activeMainView.get() == "clientsettings") { + this.historyViewModel.handleDocKeyDown(e); + return; + } + if (checkKeyPressed(waveEvent, "Escape")) { + e.preventDefault(); + if (this.activeMainView.get() == "webshare") { + this.showSessionView(); + return; + } + if (this.clearModals()) { + return; + } + let inputModel = this.inputModel; + inputModel.toggleInfoMsg(); + if (inputModel.inputMode.get() != null) { + inputModel.resetInputMode(); + } + return; + } + if (checkKeyPressed(waveEvent, "Cmd:b")) { + e.preventDefault(); + GlobalCommandRunner.bookmarksView(); + } + if (this.activeMainView.get() == "session" && checkKeyPressed(waveEvent, "Cmd:Ctrl:s")) { + e.preventDefault(); + let activeScreen = this.getActiveScreen(); + if (activeScreen != null) { + let isSidebarOpen = activeScreen.isSidebarOpen(); + if (isSidebarOpen) { + GlobalCommandRunner.screenSidebarClose(); + } else { + GlobalCommandRunner.screenSidebarOpen(); + } + } + } + if (checkKeyPressed(waveEvent, "Cmd:d")) { + let ranDelete = this.deleteActiveLine(); + if (ranDelete) { + e.preventDefault(); + } + } + } + + deleteActiveLine(): boolean { + let activeScreen = this.getActiveScreen(); + if (activeScreen == null || activeScreen.getFocusType() != "cmd") { + return false; + } + let selectedLine = activeScreen.selectedLine.get(); + if (selectedLine == null || selectedLine <= 0) { + return false; + } + let line = activeScreen.getLineByNum(selectedLine); + if (line == null) { + return false; + } + let cmd = activeScreen.getCmd(line); + if (cmd != null) { + if (cmd.isRunning()) { + let info: InfoType = { infomsg: "Cannot delete a running command" }; + this.inputModel.flashInfoMsg(info, 2000); + return false; + } + } + GlobalCommandRunner.lineDelete(String(selectedLine), true); + return true; + } + + onWCmd(e: any, mods: KeyModsType) { + if (this.activeMainView.get() != "session") { + return; + } + let activeScreen = this.getActiveScreen(); + if (activeScreen == null) { + return; + } + let rtnp = this.showAlert({ + message: "Are you sure you want to delete this screen?", + confirm: true, + }); + rtnp.then((result) => { + if (!result) { + return; + } + GlobalCommandRunner.screenDelete(activeScreen.screenId, true); + }); + } + + onRCmd(e: any, mods: KeyModsType) { + if (this.activeMainView.get() != "session") { + return; + } + let activeScreen = this.getActiveScreen(); + if (activeScreen == null) { + return; + } + if (mods.shift) { + // restart last line + GlobalCommandRunner.lineRestart("E", true); + } else { + // restart selected line + let selectedLine = activeScreen.selectedLine.get(); + if (selectedLine == null || selectedLine == 0) { + return; + } + GlobalCommandRunner.lineRestart(String(selectedLine), true); + } + } + + clearModals(): boolean { + let didSomething = false; + mobx.action(() => { + if (this.screenSettingsModal.get()) { + this.screenSettingsModal.set(null); + didSomething = true; + } + if (this.sessionSettingsModal.get()) { + this.sessionSettingsModal.set(null); + didSomething = true; + } + if (this.screenSettingsModal.get()) { + this.screenSettingsModal.set(null); + didSomething = true; + } + if (this.clientSettingsModal.get()) { + this.clientSettingsModal.set(false); + didSomething = true; + } + if (this.lineSettingsModal.get()) { + this.lineSettingsModal.set(null); + didSomething = true; + } + })(); + return didSomething; + } + + restartWaveSrv(): void { + getApi().restartWaveSrv(); + } + + getLocalRemote(): RemoteType { + for (const remote of this.remotes) { + if (remote.local) { + return remote; + } + } + return null; + } + + getCurRemoteInstance(): RemoteInstanceType { + let screen = this.getActiveScreen(); + if (screen == null) { + return null; + } + return screen.getCurRemoteInstance(); + } + + onWaveSrvStatusChange(status: boolean): void { + mobx.action(() => { + this.waveSrvRunning.set(status); + })(); + } + + getLastLogs(numbOfLines: number, cb: (logs: any) => void): void { + getApi().getLastLogs(numbOfLines, cb); + } + + getContentHeight(context: RendererContext): number { + let key = context.screenId + "/" + context.lineId; + return this.termUsedRowsCache[key]; + } + + setContentHeight(context: RendererContext, height: number): void { + let key = context.screenId + "/" + context.lineId; + this.termUsedRowsCache[key] = height; + GlobalCommandRunner.setTermUsedRows(context, height); + } + + contextScreen(e: any, screenId: string) { + getApi().contextScreen({ screenId: screenId }, { x: e.x, y: e.y }); + } + + contextEditMenu(e: any, opts: ContextMenuOpts) { + getApi().contextEditMenu({ x: e.x, y: e.y }, opts); + } + + getUIContext(): UIContextType { + let rtn: UIContextType = { + sessionid: null, + screenid: null, + remote: null, + winsize: null, + linenum: null, + build: appconst.VERSION + " " + appconst.BUILD, + }; + let session = this.getActiveSession(); + if (session != null) { + rtn.sessionid = session.sessionId; + let screen = session.getActiveScreen(); + if (screen != null) { + rtn.screenid = screen.screenId; + rtn.remote = screen.curRemote.get(); + rtn.winsize = { rows: screen.lastRows, cols: screen.lastCols }; + rtn.linenum = screen.selectedLine.get(); + } + } + return rtn; + } + + onTCmd(e: any, mods: KeyModsType) { + GlobalCommandRunner.createNewScreen(); + } + + onICmd(e: any, mods: KeyModsType) { + this.inputModel.giveFocus(); + } + + onLCmd(e: any, mods: KeyModsType) { + let screen = this.getActiveScreen(); + if (screen != null) { + GlobalCommandRunner.screenSetFocus("cmd"); + } + } + + onHCmd(e: any, mods: KeyModsType) { + this.historyViewModel.reSearch(); + } + + onPCmd(e: any, mods: KeyModsType) { + this.modalsModel.pushModal(appconst.TAB_SWITCHER); + } + + getFocusedLine(): LineFocusType { + if (this.inputModel.hasFocus()) { + return { cmdInputFocus: true }; + } + let lineElem: any = document.activeElement.closest(".line[data-lineid]"); + if (lineElem == null) { + return { cmdInputFocus: false }; + } + let lineNum = parseInt(lineElem.dataset.linenum); + return { + cmdInputFocus: false, + lineid: lineElem.dataset.lineid, + linenum: isNaN(lineNum) ? null : lineNum, + screenid: lineElem.dataset.screenid, + }; + } + + cmdStatusUpdate(screenId: string, lineId: string, origStatus: string, newStatus: string) { + let wasRunning = cmdStatusIsRunning(origStatus); + let isRunning = cmdStatusIsRunning(newStatus); + if (wasRunning && !isRunning) { + let ptr = this.getActiveLine(screenId, lineId); + if (ptr != null) { + let screen = ptr.screen; + let renderer = screen.getRenderer(lineId); + if (renderer != null) { + renderer.setIsDone(); + } + let term = screen.getTermWrap(lineId); + if (term != null) { + term.cmdDone(); + } + } + } + } + + onMenuItemAbout(): void { + mobx.action(() => { + this.modalsModel.pushModal(appconst.ABOUT); + })(); + } + + onMetaPageUp(): void { + GlobalCommandRunner.screenSelectLine("-1"); + } + + onMetaPageDown(): void { + GlobalCommandRunner.screenSelectLine("+1"); + } + + onMetaArrowUp(): void { + GlobalCommandRunner.screenSelectLine("-1"); + } + + onMetaArrowDown(): void { + GlobalCommandRunner.screenSelectLine("+1"); + } + + onBracketCmd(e: any, arg: { relative: number }, mods: KeyModsType) { + if (arg.relative == 1) { + GlobalCommandRunner.switchScreen("+"); + } else if (arg.relative == -1) { + GlobalCommandRunner.switchScreen("-"); + } + } + + onDigitCmd(e: any, arg: { digit: number }, mods: KeyModsType) { + if (mods.meta && mods.ctrl) { + GlobalCommandRunner.switchSession(String(arg.digit)); + return; + } + GlobalCommandRunner.switchScreen(String(arg.digit)); + } + + isConnected(): boolean { + return this.ws.open.get(); + } + + runUpdate(genUpdate: UpdateMessage, interactive: boolean) { + mobx.action(() => { + let oldContext = this.getUIContext(); + try { + this.runUpdate_internal(genUpdate, oldContext, interactive); + } catch (e) { + console.log("error running update", e, genUpdate); + throw e; + } + let newContext = this.getUIContext(); + if (oldContext.sessionid != newContext.sessionid || oldContext.screenid != newContext.screenid) { + this.inputModel.resetInput(); + if ("cmdline" in genUpdate) { + // TODO a bit of a hack since this update gets applied in runUpdate_internal. + // we then undo that update with the resetInput, and then redo it with the line below + // not sure how else to handle this for now though + this.inputModel.updateCmdLine(genUpdate.cmdline); + } + } else if (remotePtrToString(oldContext.remote) != remotePtrToString(newContext.remote)) { + this.inputModel.resetHistory(); + } + })(); + } + + runUpdate_internal(genUpdate: UpdateMessage, uiContext: UIContextType, interactive: boolean) { + if ("ptydata64" in genUpdate) { + let ptyMsg: PtyDataUpdateType = genUpdate; + if (isBlank(ptyMsg.remoteid)) { + // regular update + this.updatePtyData(ptyMsg); + } else { + // remote update + let ptyData = base64ToArray(ptyMsg.ptydata64); + this.remotesModel.receiveData(ptyMsg.remoteid, ptyMsg.ptypos, ptyData); + } + return; + } + let update: ModelUpdateType = genUpdate; + if ("screens" in update) { + if (update.connect) { + this.screenMap.clear(); + } + let mods = genMergeDataMap( + this.screenMap, + update.screens, + (s: Screen) => s.screenId, + (sdata: ScreenDataType) => sdata.screenid, + (sdata: ScreenDataType) => new Screen(sdata, this) + ); + for (const screenId of mods.removed) { + this.removeScreenLinesByScreenId(screenId); + } + } + if ("sessions" in update || "activesessionid" in update) { + if (update.connect) { + this.sessionList.clear(); + } + let [oldActiveSessionId, oldActiveScreenId] = this.getActiveIds(); + genMergeData( + this.sessionList, + update.sessions, + (s: Session) => s.sessionId, + (sdata: SessionDataType) => sdata.sessionid, + (sdata: SessionDataType) => new Session(sdata, this), + (s: Session) => s.sessionIdx.get() + ); + if ("activesessionid" in update) { + let newSessionId = update.activesessionid; + if (this.activeSessionId.get() != newSessionId) { + this.activeSessionId.set(newSessionId); + } + } + let [newActiveSessionId, newActiveScreenId] = this.getActiveIds(); + if (oldActiveSessionId != newActiveSessionId || oldActiveScreenId != newActiveScreenId) { + this.activeMainView.set("session"); + this.deactivateScreenLines(); + this.ws.watchScreen(newActiveSessionId, newActiveScreenId); + } + } + if ("line" in update) { + this.addLineCmd(update.line, update.cmd, interactive); + } else if ("cmd" in update) { + this.updateCmd(update.cmd); + } + if ("lines" in update) { + for (const line of update.lines) { + this.addLineCmd(line, null, interactive); + } + } + if ("screenlines" in update) { + this.updateScreenLines(update.screenlines, false); + } + if ("remotes" in update) { + if (update.connect) { + this.remotes.clear(); + } + this.updateRemotes(update.remotes); + // This code's purpose is to show view remote connection modal when a new connection is added + if (update.remotes?.length && this.remotesModel.recentConnAddedState.get()) { + this.remotesModel.openReadModal(update.remotes[0].remoteid); + } + } + if ("mainview" in update) { + if (update.mainview == "plugins") { + this.pluginsModel.showPluginsView(); + } else if (update.mainview == "bookmarks") { + this.bookmarksModel.showBookmarksView(update.bookmarks, update.selectedbookmark); + } else if (update.mainview == "session") { + this.activeMainView.set("session"); + } else if (update.mainview == "history") { + this.historyViewModel.showHistoryView(update.historyviewdata); + } else { + console.log("invalid mainview in update:", update.mainview); + } + } else if ("bookmarks" in update) { + this.bookmarksModel.mergeBookmarks(update.bookmarks); + } + if ("clientdata" in update) { + this.clientData.set(update.clientdata); + } + if (interactive && "info" in update) { + let info: InfoType = update.info; + this.inputModel.flashInfoMsg(info, info.timeoutms); + } + if (interactive && "remoteview" in update) { + let rview: RemoteViewType = update.remoteview; + if (rview.remoteedit != null) { + this.remotesModel.openEditModal({ ...rview.remoteedit }); + } + } + if (interactive && "alertmessage" in update) { + let alertMessage: AlertMessageType = update.alertmessage; + this.showAlert(alertMessage); + } + if ("cmdline" in update) { + this.inputModel.updateCmdLine(update.cmdline); + } + if (interactive && "history" in update) { + if (uiContext.sessionid == update.history.sessionid && uiContext.screenid == update.history.screenid) { + this.inputModel.setHistoryInfo(update.history); + } + } + if ("connect" in update) { + this.sessionListLoaded.set(true); + this.remotesLoaded.set(true); + } + if ("openaicmdinfochat" in update) { + this.inputModel.setOpenAICmdInfoChat(update.openaicmdinfochat); + } + if ("screenstatusindicators" in update) { + for (const indicator of update.screenstatusindicators) { + this.getScreenById_single(indicator.screenid)?.setStatusIndicator(indicator.status); + } + } + if ("screennumrunningcommands" in update) { + for (const snc of update.screennumrunningcommands) { + this.getScreenById_single(snc.screenid)?.setNumRunningCmds(snc.num); + } + } + } + + updateRemotes(remotes: RemoteType[]): void { + genMergeSimpleData(this.remotes, remotes, (r) => r.remoteid, null); + } + + getActiveSession(): Session { + return this.getSessionById(this.activeSessionId.get()); + } + + getSessionNames(): Record { + let rtn: Record = {}; + for (const session of this.sessionList) { + rtn[session.sessionId] = session.name.get(); + } + return rtn; + } + + getScreenNames(): Record { + let rtn: Record = {}; + for (let screen of this.screenMap.values()) { + rtn[screen.screenId] = screen.name.get(); + } + return rtn; + } + + getSessionById(sessionId: string): Session { + if (sessionId == null) { + return null; + } + for (const session of this.sessionList) { + if (session.sessionId == sessionId) { + return session; + } + } + return null; + } + + deactivateScreenLines() { + mobx.action(() => { + this.screenLines.clear(); + })(); + } + + getScreenLinesById(screenId: string): ScreenLines { + return this.screenLines.get(screenId); + } + + updateScreenLines(slines: ScreenLinesType, load: boolean) { + mobx.action(() => { + let existingWin = this.screenLines.get(slines.screenid); + if (existingWin == null) { + if (!load) { + console.log("cannot update screen-lines that does not exist", slines.screenid); + return; + } + let newWindow = new ScreenLines(slines.screenid); + this.screenLines.set(slines.screenid, newWindow); + newWindow.updateData(slines, load); + } else { + existingWin.updateData(slines, load); + existingWin.loaded.set(true); + } + })(); + } + + removeScreenLinesByScreenId(screenId: string) { + mobx.action(() => { + this.screenLines.delete(screenId); + })(); + } + + getScreenById(sessionId: string, screenId: string): Screen { + return this.screenMap.get(screenId); + } + + getScreenById_single(screenId: string): Screen { + return this.screenMap.get(screenId); + } + + getSessionScreens(sessionId: string): Screen[] { + let rtn: Screen[] = []; + for (let screen of this.screenMap.values()) { + if (screen.sessionId == sessionId) { + rtn.push(screen); + } + } + return rtn; + } + + getScreenLinesForActiveScreen(): ScreenLines { + let screen = this.getActiveScreen(); + if (screen == null) { + return null; + } + return this.screenLines.get(screen.screenId); + } + + getActiveScreen(): Screen { + let session = this.getActiveSession(); + if (session == null) { + return null; + } + return session.getActiveScreen(); + } + + handleCmdRestart(cmd: CmdDataType) { + if (cmd == null || !cmd.restarted) { + return; + } + let screen = this.screenMap.get(cmd.screenid); + if (screen == null) { + return; + } + let termWrap = screen.getTermWrap(cmd.lineid); + if (termWrap == null) { + return; + } + termWrap.reload(0); + } + + addLineCmd(line: LineType, cmd: CmdDataType, interactive: boolean) { + let slines = this.getScreenLinesById(line.screenid); + if (slines == null) { + return; + } + slines.addLineCmd(line, cmd, interactive); + this.handleCmdRestart(cmd); + } + + updateCmd(cmd: CmdDataType) { + let slines = this.screenLines.get(cmd.screenid); + if (slines != null) { + slines.updateCmd(cmd); + } + this.handleCmdRestart(cmd); + } + + isInfoUpdate(update: UpdateMessage): boolean { + if (update == null || "ptydata64" in update) { + return false; + } + return update.info != null || update.history != null; + } + + getClientDataLoop(loopNum: number): void { + this.getClientData(); + let clientStop = this.getHasClientStop(); + if (this.clientData.get() != null && !clientStop) { + return; + } + let timeoutMs = 1000; + if (!clientStop && loopNum > 5) { + timeoutMs = 3000; + } + if (!clientStop && loopNum > 10) { + timeoutMs = 10000; + } + if (!clientStop && loopNum > 15) { + timeoutMs = 30000; + } + setTimeout(() => this.getClientDataLoop(loopNum + 1), timeoutMs); + } + + getClientData(): void { + let url = new URL(this.getBaseHostPort() + "/api/get-client-data"); + let fetchHeaders = this.getFetchHeaders(); + fetch(url, { method: "post", body: null, headers: fetchHeaders }) + .then((resp) => handleJsonFetchResponse(url, resp)) + .then((data) => { + mobx.action(() => { + let clientData: ClientDataType = data.data; + this.clientData.set(clientData); + })(); + }) + .catch((err) => { + this.errorHandler("calling get-client-data", err, true); + }); + } + + submitCommandPacket(cmdPk: FeCmdPacketType, interactive: boolean): Promise { + if (this.debugCmds > 0) { + console.log("[cmd]", cmdPacketString(cmdPk)); + if (this.debugCmds > 1) { + console.trace(); + } + } + // adding cmdStr for debugging only (easily filter run-command calls in the network tab of debugger) + let cmdStr = cmdPk.metacmd + (cmdPk.metasubcmd ? ":" + cmdPk.metasubcmd : ""); + let url = new URL(this.getBaseHostPort() + "/api/run-command?cmd=" + cmdStr); + let fetchHeaders = this.getFetchHeaders(); + let prtn = fetch(url, { + method: "post", + body: JSON.stringify(cmdPk), + headers: fetchHeaders, + }) + .then((resp) => handleJsonFetchResponse(url, resp)) + .then((data) => { + mobx.action(() => { + let update = data.data; + if (update != null) { + this.runUpdate(update, interactive); + } + if (interactive && !this.isInfoUpdate(update)) { + this.inputModel.clearInfoMsg(true); + } + })(); + return { success: true }; + }) + .catch((err) => { + this.errorHandler("calling run-command", err, interactive); + let errMessage = "error running command"; + if (err != null && !isBlank(err.message)) { + errMessage = err.message; + } + return { success: false, error: errMessage }; + }); + return prtn; + } + + submitCommand( + metaCmd: string, + metaSubCmd: string, + args: string[], + kwargs: Record, + interactive: boolean + ): Promise { + let pk: FeCmdPacketType = { + type: "fecmd", + metacmd: metaCmd, + metasubcmd: metaSubCmd, + args: args, + kwargs: { ...kwargs }, + uicontext: this.getUIContext(), + interactive: interactive, + }; + /** + console.log( + "CMD", + pk.metacmd + (pk.metasubcmd != null ? ":" + pk.metasubcmd : ""), + pk.args, + pk.kwargs, + pk.interactive + ); + */ + return this.submitCommandPacket(pk, interactive); + } + + submitChatInfoCommand(chatMsg: string, curLineStr: string, clear: boolean): Promise { + let commandStr = "/chat " + chatMsg; + let interactive = false; + let pk: FeCmdPacketType = { + type: "fecmd", + metacmd: "eval", + args: [commandStr], + kwargs: {}, + uicontext: this.getUIContext(), + interactive: interactive, + rawstr: chatMsg, + }; + pk.kwargs["nohist"] = "1"; + if (clear) { + pk.kwargs["cmdinfoclear"] = "1"; + } else { + pk.kwargs["cmdinfo"] = "1"; + } + pk.kwargs["curline"] = curLineStr; + return this.submitCommandPacket(pk, interactive); + } + + submitRawCommand(cmdStr: string, addToHistory: boolean, interactive: boolean): Promise { + let pk: FeCmdPacketType = { + type: "fecmd", + metacmd: "eval", + args: [cmdStr], + kwargs: null, + uicontext: this.getUIContext(), + interactive: interactive, + rawstr: cmdStr, + }; + if (!addToHistory && pk.kwargs) { + pk.kwargs["nohist"] = "1"; + } + return this.submitCommandPacket(pk, interactive); + } + + // returns [sessionId, screenId] + getActiveIds(): [string, string] { + let activeSession = this.getActiveSession(); + let activeScreen = this.getActiveScreen(); + return [activeSession?.sessionId, activeScreen?.screenId]; + } + + _loadScreenLinesAsync(newWin: ScreenLines) { + this.screenLines.set(newWin.screenId, newWin); + let usp = new URLSearchParams({ screenid: newWin.screenId }); + let url = new URL(this.getBaseHostPort() + "/api/get-screen-lines?" + usp.toString()); + let fetchHeaders = this.getFetchHeaders(); + fetch(url, { headers: fetchHeaders }) + .then((resp) => handleJsonFetchResponse(url, resp)) + .then((data) => { + if (data.data == null) { + console.log("null screen-lines returned from get-screen-lines"); + return; + } + let slines: ScreenLinesType = data.data; + this.updateScreenLines(slines, true); + }) + .catch((err) => { + this.errorHandler(sprintf("getting screen-lines=%s", newWin.screenId), err, false); + }); + } + + loadScreenLines(screenId: string): ScreenLines { + let newWin = new ScreenLines(screenId); + setTimeout(() => this._loadScreenLinesAsync(newWin), 0); + return newWin; + } + + getRemote(remoteId: string): RemoteType { + if (remoteId == null) { + return null; + } + return this.remotes.find((remote) => remote.remoteid === remoteId); + } + + getRemoteNames(): Record { + let rtn: Record = {}; + for (const remote of this.remotes) { + if (!isBlank(remote.remotealias)) { + rtn[remote.remoteid] = remote.remotealias; + } else { + rtn[remote.remoteid] = remote.remotecanonicalname; + } + } + return rtn; + } + + getRemoteByName(name: string): RemoteType { + for (const remote of this.remotes) { + if (remote.remotecanonicalname == name || remote.remotealias == name) { + return remote; + } + } + return null; + } + + getCmd(line: LineType): Cmd { + return this.getCmdByScreenLine(line.screenid, line.lineid); + } + + getCmdByScreenLine(screenId: string, lineId: string): Cmd { + let slines = this.getScreenLinesById(screenId); + if (slines == null) { + return null; + } + return slines.getCmd(lineId); + } + + getActiveLine(screenId: string, lineid: string): SWLinePtr { + let slines = this.screenLines.get(screenId); + if (slines == null) { + return null; + } + if (!slines.loaded.get()) { + return null; + } + let cmd = slines.getCmd(lineid); + if (cmd == null) { + return null; + } + let line: LineType = null; + for (const element of slines.lines) { + if (element.lineid == lineid) { + line = element; + break; + } + } + if (line == null) { + return null; + } + let screen = this.getScreenById_single(slines.screenId); + return { line: line, slines: slines, screen: screen }; + } + + updatePtyData(ptyMsg: PtyDataUpdateType): void { + let linePtr = this.getActiveLine(ptyMsg.screenid, ptyMsg.lineid); + if (linePtr != null) { + linePtr.screen.updatePtyData(ptyMsg); + } + } + + errorHandler(str: string, err: any, interactive: boolean) { + console.log("[error]", str, err); + if (interactive) { + let errMsg = "error running command"; + if (err?.message) { + errMsg = err.message; + } + this.inputModel.flashInfoMsg({ infoerror: errMsg }, null); + } + } + + sendInputPacket(inputPacket: any) { + this.ws.pushMessage(inputPacket); + } + + sendCmdInputText(screenId: string, sp: StrWithPos) { + let pk: CmdInputTextPacketType = { + type: "cmdinputtext", + seqnum: this.getNextPacketSeqNum(), + screenid: screenId, + text: sp, + }; + this.ws.pushMessage(pk); + } + + resolveUserIdToName(userid: string): string { + return "@[unknown]"; + } + + resolveRemoteIdToRef(remoteId: string) { + let remote = this.getRemote(remoteId); + if (remote == null) { + return "[unknown]"; + } + if (!isBlank(remote.remotealias)) { + return remote.remotealias; + } + return remote.remotecanonicalname; + } + + resolveRemoteIdToFullRef(remoteId: string) { + let remote = this.getRemote(remoteId); + if (remote == null) { + return "[unknown]"; + } + if (!isBlank(remote.remotealias)) { + return remote.remotealias + " (" + remote.remotecanonicalname + ")"; + } + return remote.remotecanonicalname; + } + + readRemoteFile(screenId: string, lineId: string, path: string): Promise { + let urlParams = { + screenid: screenId, + lineid: lineId, + path: path, + }; + let usp = new URLSearchParams(urlParams); + let url = new URL(this.getBaseHostPort() + "/api/read-file?" + usp.toString()); + let fetchHeaders = this.getFetchHeaders(); + let fileInfo: FileInfoType = null; + let badResponseStr: string = null; + let prtn = fetch(url, { method: "get", headers: fetchHeaders }) + .then((resp) => { + if (!resp.ok) { + badResponseStr = sprintf( + "Bad fetch response for /api/read-file: %d %s", + resp.status, + resp.statusText + ); + return resp.text() as any; + } + fileInfo = JSON.parse(base64ToString(resp.headers.get("X-FileInfo"))); + return resp.blob(); + }) + .then((blobOrText: any) => { + if (blobOrText instanceof Blob) { + let blob: Blob = blobOrText; + let file = new File([blob], fileInfo.name, { type: blob.type, lastModified: fileInfo.modts }); + let isWriteable = (fileInfo.perm & 0o222) > 0; // checks for unix permission "w" bits + (file as any).readOnly = !isWriteable; + (file as any).notFound = !!fileInfo.notfound; + return file as ExtFile; + } else { + let textError: string = blobOrText; + if (textError == null || textError.length == 0) { + throw new Error(badResponseStr); + } + throw new Error(textError); + } + }); + return prtn; + } + + writeRemoteFile( + screenId: string, + lineId: string, + path: string, + data: Uint8Array, + opts?: { useTemp?: boolean } + ): Promise { + opts = opts || {}; + let params = { + screenid: screenId, + lineid: lineId, + path: path, + usetemp: !!opts.useTemp, + }; + let formData = new FormData(); + formData.append("params", JSON.stringify(params)); + let blob = new Blob([data], { type: "application/octet-stream" }); + formData.append("data", blob); + let url = new URL(this.getBaseHostPort() + "/api/write-file"); + let fetchHeaders = this.getFetchHeaders(); + let prtn = fetch(url, { method: "post", headers: fetchHeaders, body: formData }); + return prtn + .then((resp) => handleJsonFetchResponse(url, resp)) + .then((_) => { + return; + }); + } +} + +export { Model }; diff --git a/src/model/model.ts b/src/models/model_old.ts similarity index 100% rename from src/model/model.ts rename to src/models/model_old.ts diff --git a/src/models/plugins.ts b/src/models/plugins.ts new file mode 100644 index 00000000..4767dd7b --- /dev/null +++ b/src/models/plugins.ts @@ -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 = 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 }; diff --git a/src/models/remotes.ts b/src/models/remotes.ts new file mode 100644 index 00000000..f1e28f78 --- /dev/null +++ b/src/models/remotes.ts @@ -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 = mobx.observable.box(null, { + name: "RemotesModel-selectedRemoteId", + }); + remoteTermWrap: TermWrap = null; + remoteTermWrapFocus: OV = mobx.observable.box(false, { + name: "RemotesModel-remoteTermWrapFocus", + }); + showNoInputMsg: OV = mobx.observable.box(false, { + name: "RemotesModel-showNoInputMg", + }); + showNoInputTimeoutId: any = null; + remoteEdit: OV = mobx.observable.box(null, { + name: "RemotesModel-remoteEdit", + }); + recentConnAddedState: OV = 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 }; diff --git a/src/models/screen.ts b/src/models/screen.ts new file mode 100644 index 00000000..1dfda441 --- /dev/null +++ b/src/models/screen.ts @@ -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; + opts: OV; + viewOpts: OV; + name: OV; + archived: OV; + curRemote: OV; + nextLineNum: OV; + lastScreenSize: WindowSize; + lastCols: number; + lastRows: number; + selectedLine: OV; + focusType: OV; + anchor: OV<{ anchorLine: number; anchorOffset: number }>; + termLineNumFocus: OV; + setAnchor_debounced: (anchorLine: number, anchorOffset: number) => void; + terminals: Record = {}; // lineid => TermWrap + renderers: Record = {}; // lineid => RendererModel + shareMode: OV; + webShareOpts: OV; + filterRunning: OV; + statusIndicator: OV; + numRunningCmds: OV; + + 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 }; diff --git a/src/models/screenlines.ts b/src/models/screenlines.ts new file mode 100644 index 00000000..5e29baa0 --- /dev/null +++ b/src/models/screenlines.ts @@ -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 = mobx.observable.box(false, { name: "slines-loaded" }); + loadError: OV = mobx.observable.box(null); + lines: OArr = mobx.observable.array([], { + name: "slines-lines", + deep: false, + }); + cmds: Record = {}; // 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 }; diff --git a/src/models/session.ts b/src/models/session.ts new file mode 100644 index 00000000..40ec8128 --- /dev/null +++ b/src/models/session.ts @@ -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; + activeScreenId: OV; + sessionIdx: OV; + notifyNum: OV = mobx.observable.box(0); + remoteInstances: OArr; + archived: OV; + 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 }; diff --git a/src/models/speciallinecontainer.ts b/src/models/speciallinecontainer.ts new file mode 100644 index 00000000..dfa89561 --- /dev/null +++ b/src/models/speciallinecontainer.ts @@ -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 }; diff --git a/src/model/ws.ts b/src/models/ws.ts similarity index 100% rename from src/model/ws.ts rename to src/models/ws.ts diff --git a/src/plugins/code/code.tsx b/src/plugins/code/code.tsx index c7d9d37d..41fbb470 100644 --- a/src/plugins/code/code.tsx +++ b/src/plugins/code/code.tsx @@ -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" } }); diff --git a/src/plugins/core/basicrenderer.tsx b/src/plugins/core/basicrenderer.tsx index ea8211d0..cd228f8f 100644 --- a/src/plugins/core/basicrenderer.tsx +++ b/src/plugins/core/basicrenderer.tsx @@ -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 = mobx.IObservableValue; type CV = mobx.IComputedValue; @@ -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()) { diff --git a/src/plugins/csv/csv.tsx b/src/plugins/csv/csv.tsx index 62f537c9..bc88ce53 100644 --- a/src/plugins/csv/csv.tsx +++ b/src/plugins/csv/csv.tsx @@ -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, diff --git a/src/plugins/mustache/mustache.tsx b/src/plugins/mustache/mustache.tsx index 971f4f32..371488e9 100644 --- a/src/plugins/mustache/mustache.tsx +++ b/src/plugins/mustache/mustache.tsx @@ -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 = mobx.IObservableValue; @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 = mobx.observable.box(true, { name: "templateLoading" }); diff --git a/src/plugins/terminal/term.ts b/src/plugins/terminal/term.ts index 8c9d4ca8..7ec8989d 100644 --- a/src/plugins/terminal/term.ts +++ b/src/plugins/terminal/term.ts @@ -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; diff --git a/src/plugins/terminal/terminal.tsx b/src/plugins/terminal/terminal.tsx index 2e3c657d..7a992b35 100644 --- a/src/plugins/terminal/terminal.tsx +++ b/src/plugins/terminal/terminal.tsx @@ -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 = mobx.ObservableMap; @mobxReact.observer class TerminalRenderer extends React.Component< { - screen: LineContainerModel; + screen: LineContainerType; line: LineType; width: number; staticRender: boolean; diff --git a/src/types/types.ts b/src/types/types.ts index b06d8c4b..dcda0a69 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -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"; @@ -735,10 +737,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, @@ -815,6 +846,10 @@ export type { OpenAICmdInfoChatMessageType, ScreenStatusIndicatorUpdateType, OV, + OArr, + OMap, + CV, + LineContainerType, }; export { StatusIndicatorLevel }; diff --git a/src/util/modelutil.ts b/src/util/modelutil.ts new file mode 100644 index 00000000..5dc59443 --- /dev/null +++ b/src/util/modelutil.ts @@ -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 { + 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 { + let url = sprintf( + GlobalModel.getBaseHostPort() + "/api/ptyout?linenum=%d&screenid=%s&lineid=%s", + lineNum, + screenId, + lineId + ); + return getPtyDataFromUrl(url); +} + +function getRemotePtyData(remoteId: string): Promise { + let url = sprintf(GlobalModel.getBaseHostPort() + "/api/remote-pty?remoteid=%s", remoteId); + return getPtyDataFromUrl(url); +} + +function getPtyDataFromUrl(url: string): Promise { + 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 }; diff --git a/src/util/util.ts b/src/util/util.ts index b4b80ef1..baed8faa 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -72,13 +72,13 @@ function handleJsonFetchResponse(url: URL, resp: any): Promise { } 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, };