Refactored now (#29)

* redy to recover Terminal dir

* as Mike asked ... commit what you have :)

* fir anyone to have a look - DONT RUN

* works !!!
This commit is contained in:
anandamarsh 2023-10-05 11:25:32 -07:00 committed by GitHub
parent 9f284083bb
commit daee5c7c36
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
106 changed files with 851 additions and 3339 deletions

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<base href="../" /> <base href="../" />
<script charset="UTF-8" src="dist-dev/prompt.js"></script> <script charset="UTF-8" src="dist-dev/prompt.js"></script>
<link rel="stylesheet" href="static/bulma-0.9.4.min.css" /> <link rel="stylesheet" href="public/bulma-0.9.4.min.css" />
<link rel="stylesheet" href="dist-dev/prompt.css" /> <link rel="stylesheet" href="dist-dev/prompt.css" />
</head> </head>
<body> <body>

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<base href="../" /> <base href="../" />
<script charset="UTF-8" src="dist/prompt.js"></script> <script charset="UTF-8" src="dist/prompt.js"></script>
<link rel="stylesheet" href="static/bulma-0.9.4.min.css" /> <link rel="stylesheet" href="public/bulma-0.9.4.min.css" />
<link rel="stylesheet" href="dist/prompt.css" /> <link rel="stylesheet" href="dist/prompt.css" />
</head> </head>
<body> <body>

View File

@ -6,17 +6,21 @@ import { If } from "tsx-control-statements/components";
import dayjs from "dayjs"; import dayjs from "dayjs";
import type { ContextMenuOpts } from "../types/types"; import type { ContextMenuOpts } from "../types/types";
import localizedFormat from "dayjs/plugin/localizedFormat"; import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel } from "../model"; import { GlobalModel } from "../model/model";
import { isBlank } from "../util/util"; import { isBlank } from "../util/util";
import { BookmarksView } from "./bookmarks/bookmarks"; import { BookmarksView } from "./bookmarks/bookmarks";
import { WebShareView } from "../webshare/webshare-client-view";
import { HistoryView } from "./history/history"; import { HistoryView } from "./history/history";
import { ScreenSettingsModal, SessionSettingsModal, LineSettingsModal, ClientSettingsModal } from "./modals/settings"; import {
import { RemotesModal } from "../remotes/remotes"; ScreenSettingsModal,
import { TosModal } from "./modals/Modals"; SessionSettingsModal,
import { SessionView } from "./sessionview/SessionView"; LineSettingsModal,
ClientSettingsModal,
} from "./common/modals/settings";
import { RemotesModal } from "./connections/connections";
import { TosModal } from "./common/modals/modals";
import { WorkspaceView } from "../app/workspace/workspaceview";
import { MainSideBar } from "./sidebar/MainSideBar"; import { MainSideBar } from "./sidebar/MainSideBar";
import { DisconnectedModal, ClientStopModal, AlertModal, WelcomeModal } from "./modals/Modals"; import { DisconnectedModal, ClientStopModal, AlertModal, WelcomeModal } from "./common/modals/modals";
import "../index.less"; import "../index.less";
dayjs.extend(localizedFormat); dayjs.extend(localizedFormat);
@ -24,7 +28,7 @@ dayjs.extend(localizedFormat);
type OV<V> = mobx.IObservableValue<V>; type OV<V> = mobx.IObservableValue<V>;
@mobxReact.observer @mobxReact.observer
class Main extends React.Component<{}, {}> { class App extends React.Component<{}, {}> {
dcWait: OV<boolean> = mobx.observable.box(false, { name: "dcWait" }); dcWait: OV<boolean> = mobx.observable.box(false, { name: "dcWait" });
constructor(props: any) { constructor(props: any) {
@ -102,10 +106,9 @@ class Main extends React.Component<{}, {}> {
<div id="main" onContextMenu={this.handleContextMenu}> <div id="main" onContextMenu={this.handleContextMenu}>
<div className="main-content"> <div className="main-content">
<MainSideBar /> <MainSideBar />
<SessionView /> <WorkspaceView />
<HistoryView /> <HistoryView />
<BookmarksView /> <BookmarksView />
<WebShareView />
</div> </div>
<AlertModal /> <AlertModal />
<If condition={GlobalModel.needsTos()}> <If condition={GlobalModel.needsTos()}>
@ -138,4 +141,4 @@ class Main extends React.Component<{}, {}> {
} }
} }
export { Main }; export { App };

View File

Before

Width:  |  Height:  |  Size: 471 B

After

Width:  |  Height:  |  Size: 471 B

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 448 B

After

Width:  |  Height:  |  Size: 448 B

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 305 B

After

Width:  |  Height:  |  Size: 305 B

View File

Before

Width:  |  Height:  |  Size: 237 B

After

Width:  |  Height:  |  Size: 237 B

View File

Before

Width:  |  Height:  |  Size: 327 B

After

Width:  |  Height:  |  Size: 327 B

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Before

Width:  |  Height:  |  Size: 627 B

After

Width:  |  Height:  |  Size: 627 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 638 B

After

Width:  |  Height:  |  Size: 638 B

View File

Before

Width:  |  Height:  |  Size: 476 B

After

Width:  |  Height:  |  Size: 476 B

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -5,8 +5,8 @@ import { boundMethod } from "autobind-decorator";
import { If, For } from "tsx-control-statements/components"; import { If, For } from "tsx-control-statements/components";
import cn from "classnames"; import cn from "classnames";
import type { BookmarkType } from "../../types/types"; import type { BookmarkType } from "../../types/types";
import { GlobalModel } from "../../model"; import { GlobalModel } from "../../model/model";
import { CmdStrCode, Markdown } from "../../common/common"; import { CmdStrCode, Markdown } from "../common/common";
import "./bookmarks.less"; import "./bookmarks.less";

View File

@ -1,4 +1,4 @@
@import "../index.less"; @import "../../index.less";
.info-message { .info-message {
position: relative; position: relative;

View File

@ -6,7 +6,7 @@ import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import cn from "classnames"; import cn from "classnames";
import { If } from "tsx-control-statements/components"; import { If } from "tsx-control-statements/components";
import type { RemoteType } from "../types/types"; import type { RemoteType } from "../../types/types";
import "./common.less"; import "./common.less";

View File

@ -1,4 +1,4 @@
@import "../../index.less"; @import "../../../index.less";
// modal css (also includes settings-field) // modal css (also includes settings-field)

View File

@ -6,8 +6,8 @@ import { If, For } from "tsx-control-statements/components";
import cn from "classnames"; import cn from "classnames";
import dayjs from "dayjs"; import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat"; import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel } from "../../model"; import { GlobalModel } from "../../../model/model";
import { Markdown } from "../../common/common"; import { Markdown } from "../common";
dayjs.extend(localizedFormat); dayjs.extend(localizedFormat);

View File

@ -4,11 +4,11 @@ import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator"; import { boundMethod } from "autobind-decorator";
import { If, For } from "tsx-control-statements/components"; import { If, For } from "tsx-control-statements/components";
import cn from "classnames"; import cn from "classnames";
import { GlobalModel, GlobalCommandRunner, TabColors } from "../../model"; import { GlobalModel, GlobalCommandRunner, TabColors } from "../../../model/model";
import { Toggle, InlineSettingsTextEdit, SettingsError, InfoMessage } from "../../common/common"; import { Toggle, InlineSettingsTextEdit, SettingsError, InfoMessage } from "../common";
import { LineType, RendererPluginType, ClientDataType, CommandRtnType } from "../../types/types"; import { LineType, RendererPluginType, ClientDataType, CommandRtnType } from "../../../types/types";
import { PluginModel } from "../../plugins/plugins"; import { PluginModel } from "../../../plugins/plugins";
import * as util from "../../util/util"; import * as util from "../../../util/util";
import "./modals.less"; import "./modals.less";

View File

@ -0,0 +1,70 @@
@import "../../../index.less";
.term-prompt {
font-weight: 300;
.icon {
margin: 0 4px 0 2px;
vertical-align: middle;
width: 1.2em;
height: 1.2em;
fill: @prompt-green;
}
.term-prompt-branch {
color: @term-white;
}
.term-prompt-python {
color: @term-bright-magenta;
}
.term-prompt-remote {
i {
margin-right: 0;
}
}
.term-prompt-remote {
color: @term-bright-green;
&.color-green {
color: @term-bright-green;
}
&.color-red {
color: @term-bright-red;
}
&.color-blue {
color: @term-bright-blue;
}
&.color-yellow {
color: @term-bright-yellow;
}
&.color-magenta {
color: @term-bright-magenta;
}
&.color-cyan {
color: @term-bright-cyan;
}
&.color-white {
color: @term-bright-white;
}
&.color-orange {
color: @tab-orange;
}
}
.term-prompt-cwd {
color: @term-bright-green;
}
.term-prompt-end {
color: @term-bright-green;
}
}

View File

@ -3,13 +3,13 @@ import * as mobxReact from "mobx-react";
import * as mobx from "mobx"; import * as mobx from "mobx";
import dayjs from "dayjs"; import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat"; import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel, LineContainerModel } from "../model"; import { GlobalModel, LineContainerModel } from "../../../model/model";
import type { LineType, RemoteType, RemotePtrType, LineHeightChangeCallbackType } from "../../types/types"; import type { LineType, RemoteType, RemotePtrType, LineHeightChangeCallbackType } from "../../../types/types";
import cn from "classnames"; import cn from "classnames";
import { isBlank } from "../util/util"; import { isBlank } from "../../../util/util";
import { ReactComponent as FolderIcon } from "../assets/icons/folder.svg"; import { ReactComponent as FolderIcon } from "../../assets/icons/folder.svg";
import "./terminal.less"; import "./prompt.less";
dayjs.extend(localizedFormat); dayjs.extend(localizedFormat);

View File

@ -1,4 +1,4 @@
@import "../index.less"; @import "../../index.less";
.modal.prompt-modal.remotes-modal { .modal.prompt-modal.remotes-modal {
.modal-content { .modal-content {

View File

@ -4,13 +4,13 @@ import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator"; import { boundMethod } from "autobind-decorator";
import { If, For } from "tsx-control-statements/components"; import { If, For } from "tsx-control-statements/components";
import cn from "classnames"; import cn from "classnames";
import { GlobalModel, GlobalCommandRunner, RemotesModalModel } from "../model"; import { GlobalModel, GlobalCommandRunner, RemotesModalModel } from "../../model/model";
import { Toggle, RemoteStatusLight, InfoMessage } from "../common/common"; import { Toggle, RemoteStatusLight, InfoMessage } from "../common/common";
import { RemoteType, RemoteEditType } from "../types/types"; import { RemoteType, RemoteEditType } from "../../types/types";
import * as util from "../util/util"; import * as util from "../../util/util";
import * as textmeasure from "../util/textmeasure"; import * as textmeasure from "../../util/textmeasure";
import "./remotes.less"; import "./connections.less";
type OV<V> = mobx.IObservableValue<V>; type OV<V> = mobx.IObservableValue<V>;
type OArr<V> = mobx.IObservableArray<V>; type OArr<V> = mobx.IObservableArray<V>;

View File

@ -5,13 +5,13 @@ import { If, For, When, Otherwise, Choose } from "tsx-control-statements/compone
import { sprintf } from "sprintf-js"; import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator"; import { boundMethod } from "autobind-decorator";
import cn from "classnames"; import cn from "classnames";
import { GlobalModel, GlobalCommandRunner, Cmd } from "../../model"; import { GlobalModel, GlobalCommandRunner, Cmd } from "../../model/model";
import { HistoryItem, RemotePtrType, LineType, CmdDataType } from "../../types/types"; import { HistoryItem, RemotePtrType, LineType, CmdDataType } from "../../types/types";
import dayjs from "dayjs"; import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat"; import localizedFormat from "dayjs/plugin/localizedFormat";
import customParseFormat from "dayjs/plugin/customParseFormat"; import customParseFormat from "dayjs/plugin/customParseFormat";
import { Line } from "../line/linecomps"; import { Line } from "../line/linecomps";
import { CmdStrCode } from "../../common/common"; import { CmdStrCode } from "../common/common";
import "./history.less"; import "./history.less";

View File

@ -6,17 +6,14 @@ import { boundMethod } from "autobind-decorator";
import dayjs from "dayjs"; import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat"; import localizedFormat from "dayjs/plugin/localizedFormat";
import { If } from "tsx-control-statements/components"; import { If } from "tsx-control-statements/components";
import { GlobalModel, GlobalCommandRunner, Cmd, getTermPtyData } from "../../model"; import { GlobalModel, GlobalCommandRunner, Cmd, getTermPtyData } from "../../model/model";
import { termHeightFromRows } from "../../util/textmeasure"; import { termHeightFromRows } from "../../util/textmeasure";
import type { import type {
LineType, LineType,
CmdDataType,
RemoteType, RemoteType,
RemotePtrType, RemotePtrType,
RenderModeType, RenderModeType,
RendererContext,
RendererOpts, RendererOpts,
SimpleBlobRendererComponent,
RendererPluginType, RendererPluginType,
LineHeightChangeCallbackType, LineHeightChangeCallbackType,
RendererModelInitializeParams, RendererModelInitializeParams,
@ -24,19 +21,19 @@ import type {
} from "../../types/types"; } from "../../types/types";
import cn from "classnames"; import cn from "classnames";
import { ReactComponent as FavouritesIcon } from "../../assets/icons/favourites.svg"; import { ReactComponent as FavouritesIcon } from "../assets/icons/favourites.svg";
import { ReactComponent as PinIcon } from "../../assets/icons/pin.svg"; import { ReactComponent as PinIcon } from "../assets/icons/pin.svg";
import { ReactComponent as PlusIcon } from "../../assets/icons/plus.svg"; import { ReactComponent as PlusIcon } from "../assets/icons/plus.svg";
import { ReactComponent as MinusIcon } from "../../assets/icons/minus.svg"; import { ReactComponent as MinusIcon } from "../assets/icons/minus.svg";
import type { LineContainerModel } from "../../model"; import type { LineContainerModel } from "../../model/model";
import { renderCmdText } from "../../common/common"; import { renderCmdText } from "../common/common";
import { SimpleBlobRenderer } from "./renderer/simplerenderer"; import { SimpleBlobRenderer } from "../../plugins/renderer/basicrenderer";
import { FullRenderer } from "./renderer/fullrenderer"; import { IncrementalRenderer } from "../../plugins/renderer/incrementalrenderer";
import { TerminalRenderer } from "../../common/terminal/Terminal"; import { TerminalRenderer } from "../../plugins/terminal/Terminal";
import { isBlank } from "../../util/util"; import { isBlank } from "../../util/util";
import { PluginModel } from "../../plugins/plugins"; import { PluginModel } from "../../plugins/plugins";
import { Prompt } from "../../terminal/prompt"; import { Prompt } from "../common/prompt/prompt";
import * as lineutil from "./lineutil"; import * as lineutil from "./lineutil";
import "./lines.less"; import "./lines.less";
@ -776,7 +773,7 @@ class LineCmd extends React.Component<
/> />
</If> </If>
<If condition={rendererPlugin != null && rendererPlugin.rendererType == "full"}> <If condition={rendererPlugin != null && rendererPlugin.rendererType == "full"}>
<FullRenderer <IncrementalRenderer
rendererContainer={screen} rendererContainer={screen}
lineId={line.lineid} lineId={line.lineid}
plugin={rendererPlugin} plugin={rendererPlugin}

View File

@ -19,12 +19,12 @@ import type {
LineType, LineType,
TermContextUnion, TermContextUnion,
RendererContainerType, RendererContainerType,
} from "../../../types/types"; } from "../../types/types";
import * as T from "../../../types/types"; import * as T from "../../types/types";
import { PacketDataBuffer } from "../../../terminal/ptydata"; import { PacketDataBuffer } from "../../common/prompt/ptydata";
import { debounce, throttle } from "throttle-debounce"; import { debounce, throttle } from "throttle-debounce";
import * as util from "../../../util/util"; import * as util from "../../util/util";
import { GlobalModel } from "../../../model"; import { GlobalModel } from "../../model/model";
type OV<V> = mobx.IObservableValue<V>; type OV<V> = mobx.IObservableValue<V>;
type CV<V> = mobx.IComputedValue<V>; type CV<V> = mobx.IComputedValue<V>;

View File

@ -2,27 +2,25 @@ import * as React from "react";
import * as mobxReact from "mobx-react"; import * as mobxReact from "mobx-react";
import * as mobx from "mobx"; import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator"; import { boundMethod } from "autobind-decorator";
import { If, For } from "tsx-control-statements/components";
import cn from "classnames"; import cn from "classnames";
import dayjs from "dayjs"; import dayjs from "dayjs";
import type { RemoteType } from "../../types/types"; import type { RemoteType } from "../../types/types";
import { ReactComponent as LeftChevronIcon } from "../../assets/icons/chevron_left.svg"; import { ReactComponent as LeftChevronIcon } from "../assets/icons/chevron_left.svg";
import { ReactComponent as HelpIcon } from "../../assets/icons/help.svg"; import { ReactComponent as HelpIcon } from "../assets/icons/help.svg";
import { ReactComponent as SettingsIcon } from "../../assets/icons/settings.svg"; import { ReactComponent as SettingsIcon } from "../assets/icons/settings.svg";
import { ReactComponent as DiscordIcon } from "../../assets/icons/discord.svg"; import { ReactComponent as DiscordIcon } from "../assets/icons/discord.svg";
import { ReactComponent as HistoryIcon } from "../../assets/icons/history.svg"; import { ReactComponent as HistoryIcon } from "../assets/icons/history.svg";
import { ReactComponent as FavouritesIcon } from "../../assets/icons/favourites.svg"; import { ReactComponent as FavouritesIcon } from "../assets/icons/favourites.svg";
import { ReactComponent as AppsIcon } from "../../assets/icons/apps.svg"; import { ReactComponent as AppsIcon } from "../assets/icons/apps.svg";
import { ReactComponent as ConnectionsIcon } from "../../assets/icons/connections.svg"; import { ReactComponent as ConnectionsIcon } from "../assets/icons/connections.svg";
import { ReactComponent as WorkspacesIcon } from "../../assets/icons/workspaces.svg"; import { ReactComponent as WorkspacesIcon } from "../assets/icons/workspaces.svg";
import { ReactComponent as AddIcon } from "../../assets/icons/add.svg"; import { ReactComponent as AddIcon } from "../assets/icons/add.svg";
import { ReactComponent as ActionsIcon } from "../../assets/icons/tab/actions.svg"; import { ReactComponent as ActionsIcon } from "../assets/icons/tab/actions.svg";
import localizedFormat from "dayjs/plugin/localizedFormat"; import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel, GlobalCommandRunner, Session } from "../../model"; import { GlobalModel, GlobalCommandRunner, Session } from "../../model/model";
import { sortAndFilterRemotes, isBlank, openLink } from "../../util/util"; import { sortAndFilterRemotes, isBlank, openLink } from "../../util/util";
import { RemoteStatusLight } from "../../common/common";
import "./sidebar.less"; import "./sidebar.less";

View File

@ -3,6 +3,7 @@
.main-sidebar { .main-sidebar {
padding: 0; padding: 0;
width: 20rem; width: 20rem;
min-width: 20rem;
overflow-x: hidden; overflow-x: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -253,9 +254,3 @@
} }
} }
} }
.menu-list .emote-status.status-connecting {
position: relative;
top: 0px;
margin-left: -3px;
}

View File

@ -7,13 +7,13 @@ import cn from "classnames";
import dayjs from "dayjs"; import dayjs from "dayjs";
import type { RemoteType, RemoteInstanceType, RemotePtrType } from "../../../types/types"; import type { RemoteType, RemoteInstanceType, RemotePtrType } from "../../../types/types";
import localizedFormat from "dayjs/plugin/localizedFormat"; import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel, GlobalCommandRunner } from "../../../model"; import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
import { renderCmdText } from "../../../common/common"; import { renderCmdText } from "../../common/common";
import { TextAreaInput } from "./TextareaInput"; import { TextAreaInput } from "./textareainput";
import { InfoMsg } from "../InfoMsg"; import { InfoMsg } from "./infomsg";
import { HistoryInfo } from "../HistoryInfo"; import { HistoryInfo } from "./historyinfo";
import { Prompt } from "../../../terminal/prompt"; import { Prompt } from "../../common/prompt/prompt";
import { ReactComponent as ExecIcon } from "../../../assets/icons/exec.svg"; import { ReactComponent as ExecIcon } from "../../assets/icons/exec.svg";
import "./cmdInput.less"; import "./cmdInput.less";
dayjs.extend(localizedFormat); dayjs.extend(localizedFormat);

View File

@ -6,11 +6,11 @@ import { boundMethod } from "autobind-decorator";
import { If, For } from "tsx-control-statements/components"; import { If, For } from "tsx-control-statements/components";
import cn from "classnames"; import cn from "classnames";
import dayjs from "dayjs"; import dayjs from "dayjs";
import type { HistoryItem, HistoryQueryOpts } from "../../types/types"; import type { HistoryItem, HistoryQueryOpts } from "../../../types/types";
import localizedFormat from "dayjs/plugin/localizedFormat"; import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel } from "../../model"; import { GlobalModel } from "../../../model/model";
import { isBlank } from "../../util/util"; import { isBlank } from "../../../util/util";
import "./sessionview.less"; import "./cmdInput.less";
dayjs.extend(localizedFormat); dayjs.extend(localizedFormat);

View File

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

View File

@ -3,10 +3,10 @@ import * as mobxReact from "mobx-react";
import * as mobx from "mobx"; import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator"; import { boundMethod } from "autobind-decorator";
import cn from "classnames"; import cn from "classnames";
import { GlobalModel, GlobalCommandRunner } from "../../../model"; import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
import { getMonoFontSize } from "../../../util/textmeasure"; import { getMonoFontSize } from "../../../util/textmeasure";
import { isModKeyPress, hasNoModifiers } from "../../../util/util"; import { isModKeyPress, hasNoModifiers } from "../../../util/util";
import "../sessionview.less"; import "./cmdInput.less";
function pageSize(div: any): number { function pageSize(div: any): number {
if (div == null) { if (div == null) {

View File

@ -0,0 +1,103 @@
@import "../../../index.less";
.main-content {
.screen-view {
flex-grow: 1;
border-right: 1px solid #ccc;
position: relative;
}
.window-view {
display: flex;
flex-direction: column;
position: absolute;
width: 100%;
height: calc(100% - 3em);
overflow-x: hidden;
.rendermode-tag {
position: absolute;
top: 0;
right: 0;
background-color: rgba(78, 154, 6, 0.65);
color: @term-black;
padding: 2px 8px 2px 4px;
border-bottom-left-radius: 5px;
z-index: 10;
&.is-active {
color: @term-white;
}
.render-mode {
padding-top: 2px;
position: relative;
cursor: pointer;
color: @term-white;
&:hover {
color: @term-white;
}
}
}
.share-tag {
color: @term-white;
position: absolute;
top: 0;
left: 40%;
background-color: darken(rgb(0, 177, 10), 20%);
padding: 2px 8px 2px 4px;
z-index: 11;
/* border-radius: 0 0 5px 5px; */
opacity: 0.8;
display: flex;
flex-direction: column;
.share-tag-link {
margin-top: 10px;
display: none;
}
&:hover {
.share-tag-title {
font-weight: bold;
}
opacity: 1;
padding: 20px;
width: 250px;
border: 1px solid #ccc;
border-top: 0;
.share-tag-link {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
}
}
}
.window-empty {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 10px;
height: 100%;
color: @term-white;
code {
background-color: @term-black;
color: #4e9a06;
}
&.should-fade {
opacity: 1;
animation: fade-in 2.5s;
}
}
}
}

View File

@ -9,15 +9,11 @@ import { debounce } from "throttle-debounce";
import dayjs from "dayjs"; import dayjs from "dayjs";
import type { LineType, RenderModeType, LineFactoryProps } from "../../../types/types"; import type { LineType, RenderModeType, LineFactoryProps } from "../../../types/types";
import localizedFormat from "dayjs/plugin/localizedFormat"; import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel, GlobalCommandRunner, Session, ScreenLines, Screen } from "../../../model"; import { GlobalModel, ScreenLines, Screen } from "../../../model/model";
import { Line } from "../../line/linecomps"; import { Line } from "../../line/linecomps";
import { renderCmdText } from "../../../common/common";
import { LinesView } from "../../line/linesview"; import { LinesView } from "../../line/linesview";
import { ReactComponent as SparkleIcon } from "../../../assets/icons/tab/sparkle.svg";
import { ReactComponent as ActionsIcon } from "../../../assets/icons/tab/actions.svg";
import { ReactComponent as AddIcon } from "../../../assets/icons/add.svg";
import "../sessionview.less"; import "./screenview.less";
import "./tabs.less"; import "./tabs.less";
dayjs.extend(localizedFormat); dayjs.extend(localizedFormat);
@ -255,173 +251,4 @@ class ScreenWindowView extends React.Component<{ screen: Screen }, {}> {
} }
} }
@mobxReact.observer export { ScreenView };
class ScreenTabs extends React.Component<{ session: Session }, {}> {
tabsRef: React.RefObject<any> = React.createRef();
lastActiveScreenId: string = null;
scrolling: OV<boolean> = mobx.observable.box(false, { name: "screentabs-scrolling" });
stopScrolling_debounced: () => void;
constructor(props: any) {
super(props);
this.stopScrolling_debounced = debounce(1500, this.stopScrolling.bind(this));
}
@boundMethod
handleNewScreen() {
let { session } = this.props;
GlobalCommandRunner.createNewScreen();
}
@boundMethod
handleSwitchScreen(screenId: string) {
let { session } = this.props;
if (session == null) {
return;
}
if (session.activeScreenId.get() == screenId) {
return;
}
let screen = session.getScreenById(screenId);
if (screen == null) {
return;
}
GlobalCommandRunner.switchScreen(screenId);
}
componentDidMount(): void {
this.componentDidUpdate();
}
componentDidUpdate(): void {
let { session } = this.props;
let activeScreenId = session.activeScreenId.get();
if (activeScreenId != this.lastActiveScreenId && this.tabsRef.current) {
let tabElem = this.tabsRef.current.querySelector(
sprintf('.screen-tab[data-screenid="%s"]', activeScreenId)
);
if (tabElem != null) {
tabElem.scrollIntoView();
}
}
this.lastActiveScreenId = activeScreenId;
}
stopScrolling(): void {
mobx.action(() => {
this.scrolling.set(false);
})();
}
@boundMethod
handleScroll() {
if (!this.scrolling.get()) {
mobx.action(() => {
this.scrolling.set(true);
})();
}
this.stopScrolling_debounced();
}
@boundMethod
openScreenSettings(e: any, screen: Screen): void {
e.preventDefault();
e.stopPropagation();
mobx.action(() => {
GlobalModel.screenSettingsModal.set({ sessionId: screen.sessionId, screenId: screen.screenId });
})();
}
renderTab(screen: Screen, activeScreenId: string, index: number): any {
let tabIndex = null;
if (index + 1 <= 9) {
tabIndex = <div className="tab-index">{renderCmdText(String(index + 1))}</div>;
}
let settings = (
<div onClick={(e) => this.openScreenSettings(e, screen)} title="Actions" className="tab-gear">
<ActionsIcon className="icon hoverEffect " />
</div>
);
let archived = screen.archived.get() ? (
<i title="archived" className="fa-sharp fa-solid fa-box-archive" />
) : null;
let webShared = screen.isWebShared() ? (
<i title="shared to web" className="fa-sharp fa-solid fa-share-nodes web-share-icon" />
) : null;
return (
<div
key={screen.screenId}
data-screenid={screen.screenId}
className={cn(
"screen-tab",
{ "is-active": activeScreenId == screen.screenId, "is-archived": screen.archived.get() },
"color-" + screen.getTabColor()
)}
onClick={() => this.handleSwitchScreen(screen.screenId)}
onContextMenu={(event) => this.openScreenSettings(event, screen)}
>
<SparkleIcon className="icon" />
<div className="tab-name truncate">
{archived}
{webShared}
{screen.name.get()}
</div>
{tabIndex}
{settings}
</div>
);
}
render() {
let { session } = this.props;
if (session == null) {
return null;
}
let screen: Screen = null;
let index = 0;
let showingScreens = [];
let activeScreenId = session.activeScreenId.get();
let screens = GlobalModel.getSessionScreens(session.sessionId);
for (let screen of screens) {
if (!screen.archived.get() || activeScreenId == screen.screenId) {
showingScreens.push(screen);
}
}
showingScreens.sort((a, b) => {
let aidx = a.screenIdx.get();
let bidx = b.screenIdx.get();
if (aidx < bidx) {
return -1;
}
if (aidx > bidx) {
return 1;
}
return 0;
});
return (
<div className="screen-tabs-container">
<div
className={cn("screen-tabs", { scrolling: this.scrolling.get() })}
ref={this.tabsRef}
onScroll={this.handleScroll}
>
<For each="screen" index="index" of={showingScreens}>
{this.renderTab(screen, activeScreenId, index)}
</For>
<div key="new-screen" className="screen-tab new-screen" onClick={this.handleNewScreen}>
<AddIcon className="icon hoverEffect" />
</div>
</div>
{/**<div className="cmd-hints">
<div className="hint-item color-green">move left {renderCmdText("[")}</div>
<div className="hint-item color-green">move right {renderCmdText("]")}</div>
<div className="hint-item color-green">new tab {renderCmdText("T")}</div>
</div>*/}
</div>
);
}
}
export { ScreenView, ScreenTabs };

View File

@ -0,0 +1,193 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import { For } from "tsx-control-statements/components";
import cn from "classnames";
import { debounce } from "throttle-debounce";
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel, GlobalCommandRunner, Session, ScreenLines, Screen } from "../../../model/model";
import { renderCmdText } from "../../common/common";
import { ReactComponent as SparkleIcon } from "../../assets/icons/tab/sparkle.svg";
import { ReactComponent as ActionsIcon } from "../../assets/icons/tab/actions.svg";
import { ReactComponent as AddIcon } from "../../assets/icons/add.svg";
import "../workspace.less";
import "./tabs.less";
dayjs.extend(localizedFormat);
type OV<V> = mobx.IObservableValue<V>;
@mobxReact.observer
class ScreenTabs extends React.Component<{ session: Session }, {}> {
tabsRef: React.RefObject<any> = React.createRef();
lastActiveScreenId: string = null;
scrolling: OV<boolean> = mobx.observable.box(false, { name: "screentabs-scrolling" });
stopScrolling_debounced: () => void;
constructor(props: any) {
super(props);
this.stopScrolling_debounced = debounce(1500, this.stopScrolling.bind(this));
}
@boundMethod
handleNewScreen() {
let { session } = this.props;
GlobalCommandRunner.createNewScreen();
}
@boundMethod
handleSwitchScreen(screenId: string) {
let { session } = this.props;
if (session == null) {
return;
}
if (session.activeScreenId.get() == screenId) {
return;
}
let screen = session.getScreenById(screenId);
if (screen == null) {
return;
}
GlobalCommandRunner.switchScreen(screenId);
}
componentDidMount(): void {
this.componentDidUpdate();
}
componentDidUpdate(): void {
let { session } = this.props;
let activeScreenId = session.activeScreenId.get();
if (activeScreenId != this.lastActiveScreenId && this.tabsRef.current) {
let tabElem = this.tabsRef.current.querySelector(
sprintf('.screen-tab[data-screenid="%s"]', activeScreenId)
);
if (tabElem != null) {
tabElem.scrollIntoView();
}
}
this.lastActiveScreenId = activeScreenId;
}
stopScrolling(): void {
mobx.action(() => {
this.scrolling.set(false);
})();
}
@boundMethod
handleScroll() {
if (!this.scrolling.get()) {
mobx.action(() => {
this.scrolling.set(true);
})();
}
this.stopScrolling_debounced();
}
@boundMethod
openScreenSettings(e: any, screen: Screen): void {
e.preventDefault();
e.stopPropagation();
mobx.action(() => {
GlobalModel.screenSettingsModal.set({ sessionId: screen.sessionId, screenId: screen.screenId });
})();
}
renderTab(screen: Screen, activeScreenId: string, index: number): any {
let tabIndex = null;
if (index + 1 <= 9) {
tabIndex = <div className="tab-index">{renderCmdText(String(index + 1))}</div>;
}
let settings = (
<div onClick={(e) => this.openScreenSettings(e, screen)} title="Actions" className="tab-gear">
<ActionsIcon className="icon hoverEffect " />
</div>
);
let archived = screen.archived.get() ? (
<i title="archived" className="fa-sharp fa-solid fa-box-archive" />
) : null;
let webShared = screen.isWebShared() ? (
<i title="shared to web" className="fa-sharp fa-solid fa-share-nodes web-share-icon" />
) : null;
return (
<div
key={screen.screenId}
data-screenid={screen.screenId}
className={cn(
"screen-tab",
{ "is-active": activeScreenId == screen.screenId, "is-archived": screen.archived.get() },
"color-" + screen.getTabColor()
)}
onClick={() => this.handleSwitchScreen(screen.screenId)}
onContextMenu={(event) => this.openScreenSettings(event, screen)}
>
<SparkleIcon className="icon" />
<div className="tab-name truncate">
{archived}
{webShared}
{screen.name.get()}
</div>
{tabIndex}
{settings}
</div>
);
}
render() {
let { session } = this.props;
if (session == null) {
return null;
}
let screen: Screen = null;
let index = 0;
let showingScreens = [];
let activeScreenId = session.activeScreenId.get();
let screens = GlobalModel.getSessionScreens(session.sessionId);
for (let screen of screens) {
if (!screen.archived.get() || activeScreenId == screen.screenId) {
showingScreens.push(screen);
}
}
showingScreens.sort((a, b) => {
let aidx = a.screenIdx.get();
let bidx = b.screenIdx.get();
if (aidx < bidx) {
return -1;
}
if (aidx > bidx) {
return 1;
}
return 0;
});
return (
<div className="screen-tabs-container">
<div
className={cn("screen-tabs", { scrolling: this.scrolling.get() })}
ref={this.tabsRef}
onScroll={this.handleScroll}
>
<For each="screen" index="index" of={showingScreens}>
{this.renderTab(screen, activeScreenId, index)}
</For>
<div key="new-screen" className="screen-tab new-screen" onClick={this.handleNewScreen}>
<AddIcon className="icon hoverEffect" />
</div>
</div>
{/**<div className="cmd-hints">
<div className="hint-item color-green">move left {renderCmdText("[")}</div>
<div className="hint-item color-green">move right {renderCmdText("]")}</div>
<div className="hint-item color-green">new tab {renderCmdText("T")}</div>
</div>*/}
</div>
);
}
}
export { ScreenTabs };

View File

@ -4,17 +4,18 @@ import * as mobx from "mobx";
import cn from "classnames"; import cn from "classnames";
import dayjs from "dayjs"; import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat"; import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel } from "../../model"; import { GlobalModel } from "../../model/model";
import { CmdInput } from "./CmdWindow/CmdInput"; import { CmdInput } from "./cmdinput/cmdinput";
import { ScreenView, ScreenTabs } from "./Screen/ScreenView"; import { ScreenView } from "./screen/screenview";
import "./sessionview.less"; import { ScreenTabs } from "./screen/tabs";
import "./workspace.less";
dayjs.extend(localizedFormat); dayjs.extend(localizedFormat);
type OV<V> = mobx.IObservableValue<V>; type OV<V> = mobx.IObservableValue<V>;
@mobxReact.observer @mobxReact.observer
class SessionView extends React.Component<{}, {}> { class WorkspaceView extends React.Component<{}, {}> {
render() { render() {
let model = GlobalModel; let model = GlobalModel;
let session = model.getActiveSession(); let session = model.getActiveSession();
@ -38,4 +39,4 @@ class SessionView extends React.Component<{}, {}> {
} }
} }
export { SessionView }; export { WorkspaceView };

View File

@ -4,7 +4,7 @@ import * as fs from "fs";
import fetch from "node-fetch"; import fetch from "node-fetch";
import * as child_process from "node:child_process"; import * as child_process from "node:child_process";
import { debounce } from "throttle-debounce"; import { debounce } from "throttle-debounce";
import { handleJsonFetchResponse } from "./util/util"; import { handleJsonFetchResponse } from "../util/util";
import * as winston from "winston"; import * as winston from "winston";
import * as util from "util"; import * as util from "util";
import { sprintf } from "sprintf-js"; import { sprintf } from "sprintf-js";
@ -204,14 +204,14 @@ function createMainWindow(clientData) {
titleBarStyle: "hiddenInset", titleBarStyle: "hiddenInset",
width: bounds.width, width: bounds.width,
height: bounds.height, height: bounds.height,
minWidth: 600, minWidth: 800,
minHeight: 400, minHeight: 600,
webPreferences: { webPreferences: {
preload: path.join(getAppBasePath(), DistDir, "preload.js"), preload: path.join(getAppBasePath(), DistDir, "preload.js"),
}, },
}); });
let indexHtml = isDev ? "index-dev.html" : "index.html"; let indexHtml = isDev ? "index-dev.html" : "index.html";
win.loadFile(path.join(getAppBasePath(), "static", indexHtml)); win.loadFile(path.join(getAppBasePath(), "public", indexHtml));
win.webContents.on("before-input-event", (e, input) => { win.webContents.on("before-input-event", (e, input) => {
if (win.isFocused()) { if (win.isFocused()) {
wasActive = true; wasActive = true;

View File

@ -43,7 +43,7 @@
@font-face { @font-face {
font-family: "Martian Mono"; font-family: "Martian Mono";
src: url("./fonts/MartianMono-VariableFont_wdth,wght.ttf") format("truetype"); src: url("./app/assets/fonts/MartianMono-VariableFont_wdth,wght.ttf") format("truetype");
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@ -184,106 +184,6 @@ a.a-block {
display: none; display: none;
} }
} }
.screen-view {
flex-grow: 1;
border-right: 1px solid #ccc;
position: relative;
}
.window-view {
display: flex;
flex-direction: column;
position: absolute;
width: 100%;
height: calc(100% - 3em);
overflow-x: hidden;
.rendermode-tag {
position: absolute;
top: 0;
right: 0;
background-color: rgba(78, 154, 6, 0.65);
color: @term-black;
padding: 2px 8px 2px 4px;
border-bottom-left-radius: 5px;
z-index: 10;
&.is-active {
color: @term-white;
}
.render-mode {
padding-top: 2px;
position: relative;
cursor: pointer;
color: @term-white;
&:hover {
color: @term-white;
}
}
}
.share-tag {
color: @term-white;
position: absolute;
top: 0;
left: 40%;
background-color: darken(rgb(0, 177, 10), 20%);
padding: 2px 8px 2px 4px;
z-index: 11;
/* border-radius: 0 0 5px 5px; */
opacity: 0.8;
display: flex;
flex-direction: column;
.share-tag-link {
margin-top: 10px;
display: none;
}
&:hover {
.share-tag-title {
font-weight: bold;
}
opacity: 1;
padding: 20px;
width: 250px;
border: 1px solid #ccc;
border-top: 0;
.share-tag-link {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
}
}
}
.window-empty {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 10px;
height: 100%;
color: @term-white;
code {
background-color: @term-black;
color: #4e9a06;
}
&.should-fade {
opacity: 1;
animation: fade-in 2.5s;
}
}
}
} }
} }

View File

@ -2,8 +2,7 @@ import * as mobx from "mobx";
import * as React from "react"; import * as React from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { sprintf } from "sprintf-js"; import { sprintf } from "sprintf-js";
import { Main } from "./main/Main"; import { App } from "./app/app";
import { loadFonts } from "./util/util";
import * as DOMPurify from "dompurify"; import * as DOMPurify from "dompurify";
// @ts-ignore // @ts-ignore
@ -11,17 +10,11 @@ let VERSION = __PROMPT_VERSION__;
// @ts-ignore // @ts-ignore
let BUILD = __PROMPT_BUILD__; let BUILD = __PROMPT_BUILD__;
//loadFonts();
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
let reactElem = React.createElement(Main, null, null); let reactElem = React.createElement(App, null, null);
let elem = document.getElementById("app"); let elem = document.getElementById("app");
let root = createRoot(elem); let root = createRoot(elem);
// @check:font root.render(reactElem);
// let isFontLoaded = document.fonts.check("12px 'JetBrains Mono'");
document.fonts.ready.then(() => {
root.render(reactElem);
});
}); });
(window as any).mobx = mobx; (window as any).mobx = mobx;

View File

@ -10,8 +10,8 @@ import {
genMergeSimpleData, genMergeSimpleData,
boundInt, boundInt,
isModKeyPress, isModKeyPress,
} from "./util/util"; } from "../util/util";
import { TermWrap } from "./common/terminal/term"; import { TermWrap } from "../plugins/terminal/term";
import type { import type {
SessionDataType, SessionDataType,
LineType, LineType,
@ -57,8 +57,8 @@ import type {
CommandRtnType, CommandRtnType,
WebCmd, WebCmd,
WebRemote, WebRemote,
} from "./types/types"; } from "../types/types";
import * as T from "./types/types"; import * as T from "../types/types";
import { WSControl } from "./ws"; import { WSControl } from "./ws";
import { import {
measureText, measureText,
@ -67,11 +67,11 @@ import {
windowHeightToRows, windowHeightToRows,
termWidthFromCols, termWidthFromCols,
termHeightFromRows, termHeightFromRows,
} from "./util/textmeasure"; } from "../util/textmeasure";
import dayjs from "dayjs"; import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat"; import localizedFormat from "dayjs/plugin/localizedFormat";
import customParseFormat from "dayjs/plugin/customParseFormat"; import customParseFormat from "dayjs/plugin/customParseFormat";
import { getRendererContext, cmdStatusIsRunning } from "./main/line/lineutil"; import { getRendererContext, cmdStatusIsRunning } from "../app/line/lineutil";
dayjs.extend(customParseFormat); dayjs.extend(customParseFormat);
dayjs.extend(localizedFormat); dayjs.extend(localizedFormat);

View File

@ -1,7 +1,7 @@
import * as mobx from "mobx"; import * as mobx from "mobx";
import { sprintf } from "sprintf-js"; import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator"; import { boundMethod } from "autobind-decorator";
import { WatchScreenPacketType } from "./types/types"; import { WatchScreenPacketType } from "../types/types";
import dayjs from "dayjs"; import dayjs from "dayjs";
class WSControl { class WSControl {

View File

@ -1,8 +1,8 @@
import * as React from "react"; import * as React from "react";
import { RendererContext, RendererOpts, LineStateType, RendererModelContainerApi } from "../../types/types"; import { RendererContext, RendererOpts, LineStateType, RendererModelContainerApi } from "../../types/types";
import Editor from "@monaco-editor/react"; import Editor from "@monaco-editor/react";
import { Markdown } from "../../common/common"; import { Markdown } from "../../app/common/common";
import { GlobalModel, GlobalCommandRunner } from "../../model"; import { GlobalModel, GlobalCommandRunner } from "../../model/model";
import Split from "react-split-it"; import Split from "react-split-it";
import loader from "@monaco-editor/loader"; import loader from "@monaco-editor/loader";
loader.config({ paths: { vs: "./node_modules/monaco-editor/min/vs" } }); loader.config({ paths: { vs: "./node_modules/monaco-editor/min/vs" } });

View File

@ -5,7 +5,7 @@ import cn from "classnames";
import { If, For, When, Otherwise, Choose } from "tsx-control-statements/components"; import { If, For, When, Otherwise, Choose } from "tsx-control-statements/components";
import { WindowSize, RendererContext, TermOptsType, LineType, RendererOpts } from "../types/types"; import { WindowSize, RendererContext, TermOptsType, LineType, RendererOpts } from "../types/types";
import { sprintf } from "sprintf-js"; import { sprintf } from "sprintf-js";
import { Markdown } from "../common/common"; import { Markdown } from "../app/common/common";
import "./plugins.less"; import "./plugins.less";

View File

@ -9,7 +9,7 @@ import { sprintf } from "sprintf-js";
import { isBlank } from "../util/util"; import { isBlank } from "../util/util";
import mustache from "mustache"; import mustache from "mustache";
import * as DOMPurify from "dompurify"; import * as DOMPurify from "dompurify";
import { GlobalModel } from "../model"; import { GlobalModel } from "../model/model";
import "./plugins.less"; import "./plugins.less";

View File

@ -4,8 +4,8 @@ import * as mobxReact from "mobx-react";
import * as T from "../types/types"; import * as T from "../types/types";
import { debounce } from "throttle-debounce"; import { debounce } from "throttle-debounce";
import { boundMethod } from "autobind-decorator"; import { boundMethod } from "autobind-decorator";
import { PacketDataBuffer } from "../common/terminal/ptydata"; import { PacketDataBuffer } from "./util/ptydata";
import { Markdown } from "../common/common"; import { Markdown } from "../app/common/common";
import "./plugins.less"; import "./plugins.less";

View File

@ -0,0 +1,296 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import { If, For, When, Otherwise, Choose } from "tsx-control-statements/components";
import type {
RendererModelInitializeParams,
TermOptsType,
RendererContext,
RendererOpts,
SimpleBlobRendererComponent,
RendererModelContainerApi,
RendererPluginType,
PtyDataType,
RendererModel,
RendererOptsUpdate,
LineStateType,
LineType,
TermContextUnion,
RendererContainerType,
} from "../../types/types";
import * as T from "../../types/types";
import { PacketDataBuffer } from "../../common/prompt/ptydata";
import { debounce, throttle } from "throttle-debounce";
import * as util from "../../util/util";
import { GlobalModel } from "../../model/model";
type OV<V> = mobx.IObservableValue<V>;
type CV<V> = mobx.IComputedValue<V>;
class SimpleBlobRendererModel {
context: RendererContext;
opts: RendererOpts;
isDone: OV<boolean>;
api: RendererModelContainerApi;
savedHeight: number;
loading: OV<boolean>;
loadError: OV<string> = mobx.observable.box(null, {
name: "renderer-loadError",
});
lineState: LineStateType;
ptyData: PtyDataType;
ptyDataSource: (termContext: TermContextUnion) => Promise<PtyDataType>;
dataBlob: Blob;
readOnly: boolean;
notFound: boolean;
initialize(params: RendererModelInitializeParams): void {
this.loading = mobx.observable.box(true, { name: "renderer-loading" });
this.isDone = mobx.observable.box(params.isDone, {
name: "renderer-isDone",
});
this.context = params.context;
this.opts = params.opts;
this.api = params.api;
this.lineState = params.lineState;
this.savedHeight = params.savedHeight;
this.ptyDataSource = params.ptyDataSource;
if (this.isDone.get()) {
setTimeout(() => this.reload(0), 10);
}
}
dispose(): void {
return;
}
giveFocus(): void {
return;
}
updateOpts(update: RendererOptsUpdate): void {
Object.assign(this.opts, update);
}
updateHeight(newHeight: number): void {
if (this.savedHeight != newHeight) {
this.savedHeight = newHeight;
this.api.saveHeight(newHeight);
}
}
setIsDone(): void {
if (this.isDone.get()) {
return;
}
mobx.action(() => {
this.isDone.set(true);
})();
this.reload(0);
}
reload(delayMs: number): void {
mobx.action(() => {
this.loading.set(true);
})();
if (delayMs == 0) {
this.reload_noDelay();
} else {
setTimeout(() => {
this.reload_noDelay();
}, delayMs);
}
}
reload_noDelay(): void {
let source = this.lineState["prompt:source"] || "pty";
if (source == "pty") {
this.reloadPtyData();
} else if (source == "file") {
this.reloadFileData();
} else {
mobx.action(() => {
this.loadError.set("error: invalid load source: " + source);
})();
}
}
reloadFileData(): void {
// todo add file methods to API, so we don't have a GlobalModel dependency here!
let path = this.lineState["prompt:file"];
if (util.isBlank(path)) {
mobx.action(() => {
this.loadError.set("renderer has file source, but no prompt:file specified");
})();
return;
}
let rtnp = GlobalModel.readRemoteFile(this.context.screenId, this.context.lineId, path);
rtnp.then((file) => {
this.notFound = (file as any).notFound;
this.readOnly = (file as any).readOnly;
this.dataBlob = file;
mobx.action(() => {
this.loading.set(false);
this.loadError.set(null);
})();
}).catch((e) => {
mobx.action(() => {
this.loadError.set("error loading file data: " + e);
})();
});
}
reloadPtyData(): void {
this.readOnly = true;
let rtnp = this.ptyDataSource(this.context);
if (rtnp == null) {
console.log("no promise returned from ptyDataSource (simplerenderer)", this.context);
return;
}
rtnp.then((ptydata) => {
this.ptyData = ptydata;
this.dataBlob = new Blob([this.ptyData.data]);
mobx.action(() => {
this.loading.set(false);
this.loadError.set(null);
})();
}).catch((e) => {
mobx.action(() => {
this.loadError.set("error loading data: " + e);
})();
});
}
receiveData(pos: number, data: Uint8Array, reason?: string): void {
// this.dataBuf.receiveData(pos, data, reason);
}
}
@mobxReact.observer
class SimpleBlobRenderer extends React.Component<
{
rendererContainer: RendererContainerType;
lineId: string;
plugin: RendererPluginType;
onHeightChange: () => void;
initParams: RendererModelInitializeParams;
scrollToBringIntoViewport: () => void;
isSelected: boolean;
shouldFocus: boolean;
},
{}
> {
model: SimpleBlobRendererModel;
wrapperDivRef: React.RefObject<any> = React.createRef();
rszObs: ResizeObserver;
updateHeight_debounced: (newHeight: number) => void;
constructor(props: any) {
super(props);
let { rendererContainer, lineId, plugin, initParams } = this.props;
this.model = new SimpleBlobRendererModel();
this.model.initialize(initParams);
rendererContainer.registerRenderer(lineId, this.model);
this.updateHeight_debounced = debounce(1000, this.updateHeight.bind(this));
}
updateHeight(newHeight: number): void {
this.model.updateHeight(newHeight);
}
handleResize(entries: ResizeObserverEntry[]): void {
if (this.model.loading.get()) {
return;
}
if (this.props.onHeightChange) {
this.props.onHeightChange();
}
if (!this.model.loading.get() && this.wrapperDivRef.current != null) {
let height = this.wrapperDivRef.current.offsetHeight;
this.updateHeight_debounced(height);
}
}
checkRszObs() {
if (this.rszObs != null) {
return;
}
if (this.wrapperDivRef.current == null) {
return;
}
this.rszObs = new ResizeObserver(this.handleResize.bind(this));
this.rszObs.observe(this.wrapperDivRef.current);
}
componentDidMount() {
this.checkRszObs();
}
componentWillUnmount() {
let { rendererContainer, lineId } = this.props;
rendererContainer.unloadRenderer(lineId);
if (this.rszObs != null) {
this.rszObs.disconnect();
this.rszObs = null;
}
}
componentDidUpdate() {
this.checkRszObs();
}
render() {
let { plugin } = this.props;
let model = this.model;
if (model.loadError.get() != null) {
let errorText = model.loadError.get();
let height = this.model.savedHeight;
return (
<div ref={this.wrapperDivRef} style={{ minHeight: height, fontSize: model.opts.termFontSize }}>
<div className="load-error-text">ERROR: {errorText}</div>
</div>
);
}
if (model.loading.get()) {
let height = this.model.savedHeight;
return (
<div
ref={this.wrapperDivRef}
className="renderer-loading"
style={{ minHeight: height, fontSize: model.opts.termFontSize }}
>
loading content <i className="fa fa-ellipsis fa-fade" />
</div>
);
}
let Comp = plugin.simpleComponent;
if (Comp == null) {
<div ref={this.wrapperDivRef}>(no component found in plugin)</div>;
}
let { festate, cmdstr, exitcode } = this.props.initParams.rawCmd;
return (
<div ref={this.wrapperDivRef} className="sr-wrapper">
<Comp
cwd={festate.cwd}
cmdstr={cmdstr}
exitcode={exitcode}
data={model.dataBlob}
readOnly={model.readOnly}
notFound={model.notFound}
lineState={model.lineState}
context={model.context}
opts={model.opts}
savedHeight={model.savedHeight}
scrollToBringIntoViewport={this.props.scrollToBringIntoViewport}
isSelected={this.props.isSelected}
shouldFocus={this.props.shouldFocus}
rendererApi={model.api}
/>
</div>
);
}
}
export { SimpleBlobRendererModel, SimpleBlobRenderer };

View File

@ -6,14 +6,14 @@ import type {
RendererPluginType, RendererPluginType,
RendererModel, RendererModel,
RendererContainerType, RendererContainerType,
} from "../../../types/types"; } from "../../types/types";
import { debounce, throttle } from "throttle-debounce"; import { debounce, throttle } from "throttle-debounce";
type OV<V> = mobx.IObservableValue<V>; type OV<V> = mobx.IObservableValue<V>;
type CV<V> = mobx.IComputedValue<V>; type CV<V> = mobx.IComputedValue<V>;
@mobxReact.observer @mobxReact.observer
class FullRenderer extends React.Component< class IncrementalRenderer extends React.Component<
{ {
rendererContainer: RendererContainerType; rendererContainer: RendererContainerType;
lineId: string; lineId: string;
@ -94,4 +94,4 @@ class FullRenderer extends React.Component<
} }
} }
export { FullRenderer }; export { IncrementalRenderer };

View File

@ -5,12 +5,11 @@ import { boundMethod } from "autobind-decorator";
import dayjs from "dayjs"; import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat"; import localizedFormat from "dayjs/plugin/localizedFormat";
import { If } from "tsx-control-statements/components"; import { If } from "tsx-control-statements/components";
import { GlobalModel } from "../../model"; import { GlobalModel, LineContainerModel } from "../../model/model";
import { termHeightFromRows } from "../../util/textmeasure"; import { termHeightFromRows } from "../../util/textmeasure";
import type { LineType } from "../../types/types"; import type { LineType } from "../../types/types";
import cn from "classnames"; import cn from "classnames";
import type { LineContainerModel } from "../../model"; import * as lineutil from "../../app/line/lineutil";
import * as lineutil from "../../main/line/lineutil";
import "./terminal.less"; import "./terminal.less";

View File

@ -12,7 +12,7 @@ import type {
WindowSize, WindowSize,
PtyDataType, PtyDataType,
} from "../../types/types"; } from "../../types/types";
import { getTheme } from "../../themes"; import { getTheme } from "../../app/common/themes";
type DataUpdate = { type DataUpdate = {
data: Uint8Array; data: Uint8Array;

View File

@ -0,0 +1,63 @@
@import "../../index.less";
@import "./xterm.less";
.terminal-wrapper {
position: relative;
margin-top: 1em;
.term-block {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: transparent;
z-index: 10;
}
.xterm-screen {
&::-webkit-scrollbar {
display: none;
}
}
&.focus .xterm {
.xterm-screen {
overflow-y: scroll;
overscroll-behavior: contain;
}
.xterm-viewport {
overscroll-behavior: contain;
}
}
&.focus .xterm-viewport {
&::-webkit-scrollbar {
background-color: #777;
width: 5px;
height: 5px;
}
&::-webkit-scrollbar-thumb {
background: white;
}
}
.xterm-viewport {
&::-webkit-scrollbar {
background-color: #222;
width: 5px;
height: 5px;
}
&::-webkit-scrollbar-thumb {
background: #555;
}
}
}
body .xterm .xterm-viewport {
overflow-y: auto;
width: calc(100% + 5px);
}

View File

@ -1,5 +1,3 @@
@import "../../index.less";
/** /**
* Adapted from xterm.css * Adapted from xterm.css
* Copyright (c) 2014 The xterm.js authors. All rights reserved. * Copyright (c) 2014 The xterm.js authors. All rights reserved.

View File

@ -1,364 +0,0 @@
import * as mobx from "mobx";
import { Terminal } from "xterm";
import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import { v4 as uuidv4 } from "uuid";
import { termHeightFromRows, windowWidthToCols, windowHeightToRows } from "../util/textmeasure";
import { boundInt } from "../util/util";
import type {
TermContextUnion,
TermOptsType,
TermWinSize,
RendererContext,
WindowSize,
PtyDataType,
} from "../types/types";
type DataUpdate = {
data: Uint8Array;
pos: number;
};
const MinTermCols = 10;
const MaxTermCols = 1024;
type TermWrapOpts = {
termContext: TermContextUnion;
usedRows?: number;
termOpts: TermOptsType;
winSize: WindowSize;
keyHandler?: (event: any, termWrap: TermWrap) => void;
focusHandler?: (focus: boolean) => void;
dataHandler?: (data: string, termWrap: TermWrap) => void;
isRunning: boolean;
customKeyHandler?: (event: any, termWrap: TermWrap) => boolean;
fontSize: number;
ptyDataSource: (termContext: TermContextUnion) => Promise<PtyDataType>;
onUpdateContentHeight: (termContext: RendererContext, height: number) => void;
};
// cmd-instance
class TermWrap {
terminal: any;
termContext: TermContextUnion;
atRowMax: boolean;
usedRows: mobx.IObservableValue<number>;
flexRows: boolean;
connectedElem: Element;
ptyPos: number = 0;
reloading: boolean = false;
dataUpdates: DataUpdate[] = [];
loadError: mobx.IObservableValue<boolean> = mobx.observable.box(false, { name: "term-loaderror" });
winSize: WindowSize;
numParseErrors: number = 0;
termSize: TermWinSize;
focusHandler: (focus: boolean) => void;
isRunning: boolean;
fontSize: number;
onUpdateContentHeight: (termContext: RendererContext, height: number) => void;
ptyDataSource: (termContext: TermContextUnion) => Promise<PtyDataType>;
initializing: boolean;
constructor(elem: Element, opts: TermWrapOpts) {
opts = opts ?? ({} as any);
this.termContext = opts.termContext;
this.connectedElem = elem;
this.flexRows = opts.termOpts.flexrows ?? false;
this.winSize = opts.winSize;
this.focusHandler = opts.focusHandler;
this.isRunning = opts.isRunning;
this.fontSize = opts.fontSize;
this.ptyDataSource = opts.ptyDataSource;
this.onUpdateContentHeight = opts.onUpdateContentHeight;
this.initializing = true;
if (this.flexRows) {
this.atRowMax = false;
this.usedRows = mobx.observable.box(opts.usedRows ?? (opts.isRunning ? 1 : 0), { name: "term-usedrows" });
} else {
this.atRowMax = true;
this.usedRows = mobx.observable.box(opts.termOpts.rows, { name: "term-usedrows" });
}
if (opts.winSize == null) {
this.termSize = { rows: opts.termOpts.rows, cols: opts.termOpts.cols };
} else {
let cols = windowWidthToCols(opts.winSize.width, opts.fontSize);
this.termSize = { rows: opts.termOpts.rows, cols: cols };
}
this.terminal = new Terminal({
rows: this.termSize.rows,
cols: this.termSize.cols,
fontSize: opts.fontSize,
});
this.terminal._core._inputHandler._parser.setErrorHandler((state) => {
this.numParseErrors++;
return state;
});
this.terminal.open(elem);
if (opts.keyHandler != null) {
this.terminal.onKey((e) => opts.keyHandler(e, this));
}
if (opts.dataHandler != null) {
this.terminal.onData((e) => opts.dataHandler(e, this));
}
this.terminal.textarea.addEventListener("focus", () => {
if (this.focusHandler != null) {
this.focusHandler(true);
}
});
this.terminal.textarea.addEventListener("blur", (e: any) => {
if (document.activeElement == this.terminal.textarea) {
return;
}
if (this.focusHandler != null) {
this.focusHandler(false);
}
});
elem.addEventListener("scroll", this.elemScrollHandler);
if (opts.customKeyHandler != null) {
this.terminal.attachCustomKeyEventHandler((e) => opts.customKeyHandler(e, this));
}
setTimeout(() => this.reload(0), 10);
}
getUsedRows(): number {
return this.usedRows.get();
}
@boundMethod
elemScrollHandler(e: any) {
// this stops a weird behavior in the terminal
// xterm.js renders a textarea that handles focus. when it focuses and a space is typed the browser
// will scroll to make it visible (even though our terminal element has overflow hidden)
// this will undo that scroll.
if (this.atRowMax || e.target.scrollTop == 0) {
return;
}
e.target.scrollTop = 0;
}
getContextRemoteId(): string {
if ("remoteId" in this.termContext) {
return this.termContext.remoteId;
}
return null;
}
getRendererContext(): RendererContext {
if ("remoteId" in this.termContext) {
return null;
}
return this.termContext;
}
getFontHeight(): number {
return this.terminal._core.viewport._currentRowHeight;
}
dispose() {
if (this.terminal != null) {
this.terminal.dispose();
this.terminal = null;
}
}
giveFocus() {
if (this.terminal == null) {
return;
}
this.terminal.focus();
setTimeout(() => this.terminal._core.viewport.syncScrollArea(true), 0);
}
disconnectElem() {
this.connectedElem = null;
}
getTermUsedRows(): number {
let term = this.terminal;
if (term == null) {
return 0;
}
let termBuf = term._core.buffer;
let termNumLines = termBuf.lines.length;
let termYPos = termBuf.y;
if (termNumLines > term.rows) {
return term.rows;
}
let usedRows = this.isRunning ? 1 : 0;
if (this.isRunning && termYPos >= usedRows) {
usedRows = termYPos + 1;
}
for (let i = term.rows - 1; i >= usedRows; i--) {
let line = termBuf.translateBufferLineToString(i, true);
if (line != null && line.trim() != "") {
usedRows = i + 1;
break;
}
}
return usedRows;
}
updateUsedRows(forceFull: boolean, reason: string) {
if (this.terminal == null) {
return;
}
if (!this.flexRows) {
return;
}
let termContext = this.getRendererContext();
if ("remoteId" in termContext) {
return;
}
if (forceFull) {
this.atRowMax = false;
}
if (this.atRowMax) {
return;
}
let tur = this.getTermUsedRows();
if (tur >= this.terminal.rows) {
this.atRowMax = true;
}
mobx.action(() => {
let oldUsedRows = this.usedRows.get();
if (!forceFull && tur <= oldUsedRows) {
return;
}
if (tur == oldUsedRows) {
return;
}
this.usedRows.set(tur);
if (this.onUpdateContentHeight != null) {
this.onUpdateContentHeight(termContext, tur);
}
})();
}
resizeCols(cols: number): void {
this.resize({ rows: this.termSize.rows, cols: cols });
}
resize(size: TermWinSize): void {
if (this.terminal == null) {
return;
}
let newSize = { rows: size.rows, cols: size.cols };
newSize.cols = boundInt(newSize.cols, MinTermCols, MaxTermCols);
if (newSize.rows == this.termSize.rows && newSize.cols == this.termSize.cols) {
return;
}
this.termSize = newSize;
this.terminal.resize(newSize.cols, newSize.rows);
this.updateUsedRows(true, "resize");
}
resizeWindow(size: WindowSize): void {
let cols = windowWidthToCols(size.width, this.fontSize);
let rows = windowHeightToRows(size.height, this.fontSize);
this.resize({ rows, cols });
}
_reloadThenHandler(ptydata: PtyDataType) {
this.reloading = false;
this.ptyPos = ptydata.pos;
this.receiveData(ptydata.pos, ptydata.data, "reload-main");
for (let i = 0; i < this.dataUpdates.length; i++) {
this.receiveData(this.dataUpdates[i].pos, this.dataUpdates[i].data, "reload-update-" + i);
}
this.dataUpdates = [];
if (this.terminal != null) {
this.terminal.write(new Uint8Array(), () => {
this.updateUsedRows(true, "reload");
});
}
}
getLineNum(): number {
let context = this.getRendererContext();
if (context == null) {
return 0;
}
return context.lineNum;
}
hardResetTerminal(): void {
if (this.terminal == null) {
return;
}
this.terminal.reset();
this.ptyPos = 0;
this.updateUsedRows(true, "term-reset");
this.dataUpdates = [];
this.numParseErrors = 0;
}
reload(delayMs: number) {
if (this.terminal == null) {
return;
}
// console.log("reload-term", this.getLineNum());
if (!this.initializing) {
this.hardResetTerminal();
}
this.reloading = true;
this.initializing = false;
let rtnp = this.ptyDataSource(this.termContext);
if (rtnp == null) {
console.log("no promise returned from ptyDataSource (termwrap)", this.termContext);
return;
}
rtnp.then((ptydata) => {
setTimeout(() => {
this._reloadThenHandler(ptydata);
}, delayMs);
}).catch((e) => {
mobx.action(() => {
this.loadError.set(true);
})();
this.dataUpdates = [];
this.reloading = false;
console.log("error reloading terminal", this.termContext, e);
});
}
receiveData(pos: number, data: Uint8Array, reason?: string) {
// console.log("update-pty-data", reason, "line:" + this.getLineNum(), pos, data.length, "=>", pos + data.length);
if (this.initializing) {
return;
}
if (this.terminal == null) {
return;
}
if (this.loadError.get()) {
return;
}
if (this.reloading) {
this.dataUpdates.push({ data: data, pos: pos });
return;
}
if (pos > this.ptyPos) {
console.log(sprintf("pty-jump term[%s] %d => %d", JSON.stringify(this.termContext), this.ptyPos, pos));
this.ptyPos = pos;
}
if (pos < this.ptyPos) {
let diff = this.ptyPos - pos;
if (diff >= data.length) {
// already contains all the data
return;
}
data = data.slice(diff);
pos += diff;
}
this.ptyPos += data.length;
this.terminal.write(data, () => {
this.updateUsedRows(false, "updatePtyData");
});
}
cmdDone(): void {
this.isRunning = false;
this.updateUsedRows(true, "cmd-done");
}
}
export { TermWrap };

View File

@ -1,195 +0,0 @@
@import "../index.less";
.term-prompt {
font-weight: 300;
.icon {
margin: 0 4px 0 2px;
vertical-align: middle;
width: 1.2em;
height: 1.2em;
fill: @prompt-green;
}
.term-prompt-branch {
color: @term-white;
}
.term-prompt-python {
color: @term-bright-magenta;
}
.term-prompt-remote {
i {
margin-right: 0;
}
}
.term-prompt-remote {
color: @term-bright-green;
&.color-green {
color: @term-bright-green;
}
&.color-red {
color: @term-bright-red;
}
&.color-blue {
color: @term-bright-blue;
}
&.color-yellow {
color: @term-bright-yellow;
}
&.color-magenta {
color: @term-bright-magenta;
}
&.color-cyan {
color: @term-bright-cyan;
}
&.color-white {
color: @term-bright-white;
}
&.color-orange {
color: @tab-orange;
}
}
.term-prompt-cwd {
color: @term-bright-green;
}
.term-prompt-end {
color: @term-bright-green;
}
}
.terminal-wrapper {
position: relative;
margin-top: 1em;
.term-block {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: transparent;
z-index: 10;
}
.xterm-screen {
&::-webkit-scrollbar {
display: none;
}
}
&.focus .xterm {
.xterm-screen {
overflow-y: scroll;
overscroll-behavior: contain;
}
.xterm-viewport {
overscroll-behavior: contain;
}
}
&.focus .xterm-viewport {
&::-webkit-scrollbar {
background-color: #777;
width: 5px;
height: 5px;
}
&::-webkit-scrollbar-thumb {
background: white;
}
}
.xterm-viewport {
&::-webkit-scrollbar {
background-color: #222;
width: 5px;
height: 5px;
}
&::-webkit-scrollbar-thumb {
background: #555;
}
}
}
body .xterm .xterm-viewport {
overflow-y: auto;
width: calc(100% + 5px);
}
#main .term-prompt {
i {
margin-right: 3px;
}
.term-prompt-branch {
color: @term-white;
}
.term-prompt-python {
color: @term-bright-magenta;
}
.term-prompt-remote {
i {
margin-right: 0;
}
}
.term-prompt-remote {
color: @term-bright-green;
&.color-green {
color: @term-bright-green;
}
&.color-red {
color: @term-bright-red;
}
&.color-blue {
color: @term-bright-blue;
}
&.color-yellow {
color: @term-bright-yellow;
}
&.color-magenta {
color: @term-bright-magenta;
}
&.color-cyan {
color: @term-bright-cyan;
}
&.color-white {
color: @term-bright-white;
}
&.color-orange {
color: @tab-orange;
}
}
.term-prompt-cwd {
color: @term-bright-green;
}
.term-prompt-end {
color: @term-bright-green;
}
}

View File

@ -1,178 +0,0 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import { v4 as uuidv4 } from "uuid";
import dayjs from "dayjs";
import { If, For, When, Otherwise, Choose } from "tsx-control-statements/components";
import cn from "classnames";
import { GlobalModel, GlobalCommandRunner, Screen } from "../model";
import { WebStopShareConfirmMarkdown } from "../main/modals/settings";
import * as util from "../util/util";
import "./webshare.less";
type OV<V> = mobx.IObservableValue<V>;
type OArr<V> = mobx.IObservableArray<V>;
type OMap<K, V> = mobx.ObservableMap<K, V>;
@mobxReact.observer
class WebShareView extends React.Component<{}, {}> {
shareCopied: OV<string> = mobx.observable.box(null, { name: "sw-shareCopied" });
@boundMethod
closeView(): void {
GlobalModel.showSessionView();
}
@boundMethod
viewInContext(screen: Screen) {
GlobalModel.historyViewModel.closeView();
GlobalCommandRunner.lineView(screen.sessionId, screen.screenId, screen.selectedLine.get());
}
getSSName(screen: Screen, snames: Record<string, string>): string {
let sessionName = snames[screen.sessionId] ?? "unknown";
return sprintf("#%s[%s]", sessionName, screen.name.get());
}
@boundMethod
copyShareLink(screen: Screen): void {
let shareLink = screen.getWebShareUrl();
if (shareLink == null) {
return;
}
navigator.clipboard.writeText(shareLink);
mobx.action(() => {
this.shareCopied.set(screen.screenId);
})();
setTimeout(() => {
mobx.action(() => {
this.shareCopied.set(null);
})();
}, 600);
}
@boundMethod
openScreenSettings(screen: Screen): void {
mobx.action(() => {
GlobalModel.screenSettingsModal.set({ sessionId: screen.sessionId, screenId: screen.screenId });
})();
}
@boundMethod
stopSharing(screen: Screen): void {
let message = WebStopShareConfirmMarkdown;
let alertRtn = GlobalModel.showAlert({ message: message, confirm: true, markdown: true });
alertRtn.then((result) => {
if (!result) {
return;
}
let prtn = GlobalCommandRunner.screenWebShare(screen.screenId, false);
prtn.then((crtn) => {
if (crtn.success) {
return;
}
GlobalModel.showAlert({ message: crtn.error });
});
});
}
render() {
let isHidden = GlobalModel.activeMainView.get() != "webshare";
if (isHidden) {
return null;
}
let snames = GlobalModel.getSessionNames();
let screenList = GlobalModel.getWebSharedScreens();
let screen: Screen = null;
return (
<div className={cn("webshare-view", "alt-view")}>
<div className="close-button" onClick={this.closeView}>
<i className="fa-sharp fa-solid fa-xmark"></i>
</div>
<div className="alt-title">
<i className="fa-sharp fa-solid fa-share-nodes" style={{ marginRight: 10 }} />
WEB SHARING<If condition={screenList.length > 0}> ({screenList.length})</If>
</div>
<If condition={screenList.length == 0}>
<div className="no-content">
No Active Web Shares.
<br />
Share a screen using the "web share" toggle in screen/tab settings{" "}
<i className="fa-sharp fa-solid fa-gear" />.
</div>
</If>
<If condition={screenList.length > 0}>
<div className="alt-list">
<For each="screen" of={screenList}>
<div key={screen.screenId} className="webshare-item">
<If condition={this.shareCopied.get() == screen.screenId}>
<div className="copied-indicator" />
</If>
<div className="webshare-vic">
<span className="webshare-vic-link" onClick={() => this.viewInContext(screen)}>
{this.getSSName(screen, snames)}
</span>
</div>
<div className="actions">
<a
href={util.makeExternLink(screen.getWebShareUrl())}
target="_blank"
className="button is-prompt-green is-outlined is-small a-block"
>
<span>open in browser</span>
<span className="icon">
<i className="fa-sharp fa-solid fa-up-right-from-square" />
</span>
</a>
<div
className="button is-prompt-green is-outlined is-small"
onClick={() => this.copyShareLink(screen)}
>
<span>copy link</span>
<span className="icon">
<i className="fa-sharp fa-solid fa-copy" />
</span>
</div>
<div
className="button is-prompt-green is-outlined is-small"
onClick={() => this.openScreenSettings(screen)}
>
<span>open settings</span>
<span className="icon">
<i className="fa-sharp fa-solid fa-cog" />
</span>
</div>
<div
className="button is-prompt-danger is-outlined is-small ml-4"
onClick={() => this.stopSharing(screen)}
>
<span>stop sharing</span>
<span className="icon">
<i className="fa-sharp fa-solid fa-trash" />
</span>
</div>
</div>
</div>
</For>
</div>
</If>
<div className="alt-help">
<div className="help-entry">
Currently limited to a maximum of 3 screens, each with up to 50 commands.
<br />
Contact us on{" "}
<a target="_blank" href="https://discord.gg/XfvZ334gwU">
<i className="fa-brands fa-discord" /> Discord
</a>{" "}
to get a higher limit.
</div>
</div>
</div>
);
}
}
export { WebShareView };

View File

@ -1,859 +0,0 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import { If, For, When, Otherwise, Choose } from "tsx-control-statements/components";
import cn from "classnames";
import { WebShareModel, getTermPtyData } from "./webshare-model";
import * as T from "./types";
import { isBlank } from "./util";
import { PluginModel } from "./plugins";
import * as lineutil from "./lineutil";
import * as util from "./util";
import { windowWidthToCols, windowHeightToRows, termHeightFromRows, termWidthFromCols } from "./textmeasure";
import { debounce, throttle } from "throttle-debounce";
import { LinesView } from "./linesview";
import { Toggle } from "./elements";
import { SimpleBlobRendererModel, SimpleBlobRenderer } from "./simplerenderer";
type OV<V> = mobx.IObservableValue<V>;
type OArr<V> = mobx.IObservableArray<V>;
type OMap<K, V> = mobx.ObservableMap<K, V>;
let foo = LinesView;
// TODO reshare
// TODO document.visibility API to disconnect websocket: document.addEventListener("visibilitychange", () => { document.hidden });
function makeFullRemoteRef(ownerName: string, remoteRef: string, name: string): string {
if (isBlank(ownerName) && isBlank(name)) {
return remoteRef;
}
if (!isBlank(ownerName) && isBlank(name)) {
return ownerName + ":" + remoteRef;
}
if (isBlank(ownerName) && !isBlank(name)) {
return remoteRef + ":" + name;
}
return ownerName + ":" + remoteRef + ":" + name;
}
function getShortVEnv(venvDir: string): string {
if (isBlank(venvDir)) {
return "";
}
let lastSlash = venvDir.lastIndexOf("/");
if (lastSlash == -1) {
return venvDir;
}
return venvDir.substr(lastSlash + 1);
}
function replaceHomePath(path: string, homeDir: string): string {
if (path == homeDir) {
return "~";
}
if (path.startsWith(homeDir + "/")) {
return "~" + path.substr(homeDir.length);
}
return path;
}
function getCwdStr(remote: T.WebRemote, state: Record<string, string>): string {
if (state == null || isBlank(state.cwd)) {
return "~";
}
let cwd = state.cwd;
if (remote && remote.homedir) {
cwd = replaceHomePath(cwd, remote.homedir);
}
return cwd;
}
function getRemoteStr(remote: T.WebRemote): string {
if (remote == null) {
return "(invalid remote)";
}
let remoteRef = !isBlank(remote.alias) ? remote.alias : remote.canonicalname;
let fullRef = makeFullRemoteRef(null, remoteRef, remote.name);
return fullRef;
}
@mobxReact.observer
class Prompt extends React.Component<{ remote: T.WebRemote; festate: Record<string, string> }, {}> {
render() {
let { remote, festate } = this.props;
let remoteStr = getRemoteStr(remote);
let cwd = getCwdStr(remote, festate);
let isRoot = !!remote.isroot;
let remoteColorClass = isRoot ? "color-red" : "color-green";
let remoteTitle: string = null;
if (remote && remote.canonicalname) {
remoteTitle = remote.canonicalname;
}
let cwdElem = (
<span title="current directory" className="term-prompt-cwd">
<i className="fa-solid fa-sharp fa-folder-open" />
{cwd}
</span>
);
let remoteElem = (
<span title={remoteTitle} className={cn("term-prompt-remote", remoteColorClass)}>
[{remoteStr}]{" "}
</span>
);
let rootIndicatorElem = <span className="term-prompt-end">{isRoot ? "#" : "$"}</span>;
let branchElem = null;
let pythonElem = null;
if (!isBlank(festate["PROMPTVAR_GITBRANCH"])) {
let branchName = festate["PROMPTVAR_GITBRANCH"];
branchElem = (
<span title="current git branch" className="term-prompt-branch">
<i className="fa-sharp fa-solid fa-code-branch" />
{branchName}{" "}
</span>
);
}
if (!isBlank(festate["VIRTUAL_ENV"])) {
let venvDir = festate["VIRTUAL_ENV"];
let venv = getShortVEnv(venvDir);
pythonElem = (
<span title="python venv" className="term-prompt-python">
<i className="fa-brands fa-python" />
{venv}{" "}
</span>
);
}
return (
<span className="term-prompt">
{remoteElem} {pythonElem}
{branchElem}
{cwdElem} {rootIndicatorElem}
</span>
);
}
}
@mobxReact.observer
class LineAvatar extends React.Component<{ line: T.WebLine; cmd: T.WebCmd }, {}> {
render() {
let { line, cmd } = this.props;
let lineNumStr = String(line.linenum);
let status = cmd != null ? cmd.status : "done";
let rtnstate = cmd != null ? cmd.rtnstate : false;
let isComment = line.linetype == "text";
return (
<div className={cn("avatar", "num-" + lineNumStr.length, "status-" + status, { rtnstate: rtnstate })}>
{lineNumStr}
<If condition={status == "hangup" || status == "error"}>
<i className="fa-sharp fa-solid fa-triangle-exclamation status-icon" />
</If>
<If condition={status == "detached"}>
<i className="fa-sharp fa-solid fa-rotate status-icon" />
</If>
<If condition={isComment}>
<i className="fa-sharp fa-solid fa-comment comment-icon" />
</If>
</div>
);
}
}
@mobxReact.observer
class WebLineCmdView extends React.Component<
{
line: T.WebLine;
cmd: T.WebCmd;
topBorder: boolean;
width: number;
onHeightChange: T.LineHeightChangeCallbackType;
staticRender: boolean;
visible: OV<boolean>;
},
{}
> {
lineRef: React.RefObject<any> = React.createRef();
isCmdExpanded: OV<boolean> = mobx.observable.box(false, { name: "cmd-expanded" });
isOverflow: OV<boolean> = mobx.observable.box(false, { name: "line-overflow" });
cmdTextRef: React.RefObject<any> = React.createRef();
copiedIndicator: OV<boolean> = mobx.observable.box(false, { name: "copiedIndicator" });
lastHeight: number;
componentDidMount(): void {
this.checkCmdText();
this.componentDidUpdate();
}
componentDidUpdate(): void {
this.handleHeightChange();
}
renderSimple() {
let { line, cmd, topBorder } = this.props;
let height: number = 0;
if (isBlank(line.renderer) || line.renderer == "terminal") {
height = this.getTerminalRendererHeight(cmd);
} else {
let { line, width } = this.props;
let usedRows = WebShareModel.getUsedRows(lineutil.getWebRendererContext(line), line, cmd, width);
height = 36 + usedRows;
}
let mainCn = cn("line", "line-cmd", { "top-border": topBorder });
return (
<div
ref={this.lineRef}
className={mainCn}
data-lineid={line.lineid}
data-linenum={line.linenum}
style={{ height: height }}
>
<LineAvatar line={line} cmd={null} />
</div>
);
}
getTerminalRendererHeight(cmd: T.WebCmd): number {
let { line, width } = this.props;
let height = 42; // height of zero height terminal
let usedRows = WebShareModel.getUsedRows(lineutil.getWebRendererContext(line), line, cmd, width);
if (usedRows > 0) {
height = 53 + termHeightFromRows(usedRows, WebShareModel.getTermFontSize());
}
return height;
}
@boundMethod
handleExpandCmd(): void {
mobx.action(() => {
this.isCmdExpanded.set(true);
})();
}
renderCmdText(cmd: T.WebCmd, remote: T.WebRemote): any {
if (cmd == null) {
return (
<div className="metapart-mono cmdtext">
<span className="term-bright-green">(cmd not found)</span>
</div>
);
}
if (this.isCmdExpanded.get()) {
return (
<React.Fragment>
<div key="meta2" className="meta meta-line2">
<div className="metapart-mono cmdtext">
<Prompt remote={cmd.remote} festate={cmd.festate} />
</div>
</div>
<div key="meta3" className="meta meta-line3 cmdtext-expanded-wrapper">
<div className="cmdtext-expanded">{lineutil.getFullCmdText(cmd.cmdstr)}</div>
</div>
</React.Fragment>
);
}
let isMultiLine = lineutil.isMultiLineCmdText(cmd.cmdstr);
return (
<div key="meta2" className="meta meta-line2" ref={this.cmdTextRef}>
<div className="metapart-mono cmdtext">
<Prompt remote={cmd.remote} festate={cmd.festate} />
<span> </span>
<span>{lineutil.getSingleLineCmdText(cmd.cmdstr)}</span>
</div>
<If condition={this.isOverflow.get() || isMultiLine}>
<div className="cmdtext-overflow" onClick={this.handleExpandCmd}>
...&#x25BC;
</div>
</If>
</div>
);
}
checkCmdText() {
let metaElem = this.cmdTextRef.current;
if (metaElem == null || metaElem.childNodes.length == 0) {
return;
}
let metaElemWidth = metaElem.offsetWidth;
let metaChild = metaElem.firstChild;
let children = metaChild.childNodes;
let childWidth = 0;
for (let i = 0; i < children.length; i++) {
let ch = children[i];
childWidth += ch.offsetWidth;
}
let isOverflow = childWidth > metaElemWidth;
if (isOverflow != this.isOverflow.get()) {
mobx.action(() => {
this.isOverflow.set(isOverflow);
})();
}
}
@boundMethod
handleHeightChange(): void {
let { line } = this.props;
let curHeight = 0;
let curWidth = 0;
let elem = this.lineRef.current;
if (elem != null) {
curHeight = elem.offsetHeight;
curWidth = elem.offsetWidth;
}
if (this.lastHeight == curHeight) {
return;
}
let lastHeight = this.lastHeight;
this.lastHeight = curHeight;
this.props.onHeightChange(line.linenum, curHeight, lastHeight);
// console.log("line height change: ", line.linenum, lastHeight, "=>", curHeight);
}
@boundMethod
handleClick(): void {
WebShareModel.setSelectedLine(this.props.line.linenum);
}
renderMetaWrap() {
let { line, cmd } = this.props;
let formattedTime = lineutil.getLineDateTimeStr(line.ts);
let termOpts = cmd.termopts;
let remote = cmd.remote;
let renderer = line.renderer;
return (
<div key="meta" className="meta-wrap">
<div key="meta1" className="meta meta-line1">
<div className="ts">{formattedTime}</div>
<div>&nbsp;</div>
<If condition={!isBlank(renderer) && renderer != "terminal"}>
<div className="renderer">
<i className="fa-sharp fa-solid fa-fill" />
{renderer}&nbsp;
</div>
</If>
<div className="termopts">
({termOpts.rows}x{termOpts.cols})
</div>
</div>
{this.renderCmdText(cmd, remote)}
</div>
);
}
copyAllowed(): boolean {
return navigator.clipboard != null;
}
@boundMethod
clickCopy(): void {
if (this.copyAllowed()) {
let { cmd } = this.props;
navigator.clipboard.writeText(cmd.cmdstr);
}
mobx.action(() => {
this.copiedIndicator.set(true);
})();
setTimeout(() => {
mobx.action(() => {
this.copiedIndicator.set(false);
})();
}, 600);
}
getRendererOpts(cmd: T.WebCmd): T.RendererOpts {
return {
maxSize: WebShareModel.getMaxContentSize(),
idealSize: WebShareModel.getIdealContentSize(),
termOpts: mobx.toJS(cmd.termopts),
termFontSize: WebShareModel.getTermFontSize(),
};
}
makeRendererModelInitializeParams(): T.RendererModelInitializeParams {
let { line, cmd } = this.props;
let context = lineutil.getWebRendererContext(line);
let savedHeight = WebShareModel.getContentHeight(context);
if (savedHeight == null) {
if (line.contentheight != null && line.contentheight != -1) {
savedHeight = line.contentheight;
} else {
savedHeight = 0;
}
}
let api = {
saveHeight: (height: number) => {
WebShareModel.setContentHeight(lineutil.getWebRendererContext(line), height);
},
onFocusChanged: (focus: boolean) => {
// nothing
},
dataHandler: (data: string, model: T.RendererModel) => {
// nothing
},
};
return {
context: context,
isDone: !lineutil.cmdStatusIsRunning(cmd.status),
savedHeight: savedHeight,
opts: this.getRendererOpts(cmd),
ptyDataSource: getTermPtyData,
api: api,
};
}
render() {
let { line, cmd, topBorder, staticRender, visible } = this.props;
let isVisible = visible.get();
if (staticRender || !isVisible) {
return this.renderSimple();
}
let model = WebShareModel;
let isSelected = mobx
.computed(() => model.getSelectedLine() == line.linenum, { name: "computed-isSelected" })
.get();
let isServerSelected = mobx
.computed(() => model.getServerSelectedLine() == line.linenum, { name: "computed-isServerSelected" })
.get();
let rendererPlugin: T.RendererPluginType = null;
let isNoneRenderer = line.renderer == "none";
if (!isBlank(line.renderer) && line.renderer != "terminal" && !isNoneRenderer) {
rendererPlugin = PluginModel.getRendererPluginByName(line.renderer);
}
let rendererType = lineutil.getRendererType(line);
let mainCn = cn("web-line line line-cmd", { "top-border": topBorder });
let visObs = mobx.observable.box(true, { name: "visObs" });
let width = this.props.width;
if (width == 0) {
width = 1024;
}
let isExpanded = this.isCmdExpanded.get();
return (
<div
ref={this.lineRef}
className={mainCn}
data-lineid={line.lineid}
data-linenum={line.linenum}
onClick={this.handleClick}
>
<If condition={this.copiedIndicator.get()}>
<div key="copied" className="copied-indicator">
<div>copied</div>
</div>
</If>
<div
key="focus"
className={cn(
"focus-indicator",
{ selected: isSelected || isServerSelected },
{ active: isSelected }
)}
/>
<div className={cn("line-header", { "is-expanded": isExpanded })}>
<LineAvatar line={line} cmd={cmd} />
{this.renderMetaWrap()}
<If condition={this.copyAllowed()}>
<div
key="copy"
title="Copy Command"
className={cn("line-icon copy-icon")}
onClick={this.clickCopy}
style={{ marginLeft: 5 }}
>
<i className="fa-sharp fa-solid fa-copy" />
</div>
</If>
</div>
<If condition={rendererPlugin == null && !isNoneRenderer}>
<TerminalRenderer
line={line}
cmd={cmd}
width={width}
staticRender={staticRender}
visible={visible}
onHeightChange={this.handleHeightChange}
/>
</If>
<If condition={rendererPlugin != null}>
<SimpleBlobRenderer
rendererContainer={WebShareModel}
lineId={line.lineid}
plugin={rendererPlugin}
onHeightChange={this.handleHeightChange}
initParams={this.makeRendererModelInitializeParams()}
/>
</If>
<If condition={cmd && cmd.rtnstate}>
<div
key="rtnstate"
className="cmd-rtnstate"
style={{ visibility: cmd.status == "done" ? "visible" : "hidden" }}
>
<If condition={isBlank(cmd.rtnstatestr)}>
<div className="cmd-rtnstate-label">state unchanged</div>
<div className="cmd-rtnstate-sep"></div>
</If>
<If condition={!isBlank(cmd.rtnstatestr)}>
<div className="cmd-rtnstate-label">new state</div>
<div className="cmd-rtnstate-sep"></div>
<div className="cmd-rtnstate-diff">{cmd.rtnstatestr}</div>
</If>
</div>
</If>
</div>
);
}
}
@mobxReact.observer
class WebLineTextView extends React.Component<{ line: T.WebLine; cmd: T.WebCmd; topBorder: boolean }, {}> {
render() {
let { line, topBorder } = this.props;
let model = WebShareModel;
let isSelected = mobx
.computed(() => model.getSelectedLine() == line.linenum, { name: "computed-isSelected" })
.get();
let mainCn = cn("web-line line line-text", { "top-border": topBorder });
return (
<div className={mainCn} data-lineid={line.lineid} data-linenum={line.linenum}>
<div key="focus" className={cn("focus-indicator", { "selected active": isSelected })} />
<div className="line-header">
<LineAvatar line={line} cmd={null} />
</div>
<div>
<div>{line.text}</div>
</div>
</div>
);
}
}
@mobxReact.observer
class TerminalRenderer extends React.Component<
{
line: T.WebLine;
cmd: T.WebCmd;
width: number;
staticRender: boolean;
visible: OV<boolean>;
onHeightChange: () => void;
},
{}
> {
termLoaded: mobx.IObservableValue<boolean> = mobx.observable.box(false, { name: "termrenderer-termLoaded" });
elemRef: React.RefObject<any> = React.createRef();
termRef: React.RefObject<any> = React.createRef();
constructor(props) {
super(props);
}
componentDidMount() {
this.componentDidUpdate(null, null, null);
}
componentWillUnmount() {
if (this.termLoaded.get()) {
this.unloadTerminal(true);
}
}
getSnapshotBeforeUpdate(prevProps, prevState): { height: number } {
let elem = this.elemRef.current;
if (elem == null) {
return { height: 0 };
}
return { height: elem.offsetHeight };
}
componentDidUpdate(prevProps, prevState, snapshot: { height: number }): void {
if (this.props.onHeightChange == null) {
return;
}
let { line } = this.props;
let curHeight = 0;
let elem = this.elemRef.current;
if (elem != null) {
curHeight = elem.offsetHeight;
}
if (snapshot == null) {
snapshot = { height: 0 };
}
if (snapshot.height != curHeight) {
this.props.onHeightChange();
// console.log("term-render height change: ", line.linenum, snapshot.height, "=>", curHeight);
}
this.checkLoad();
}
checkLoad(): void {
let { line, staticRender, visible } = this.props;
if (staticRender) {
return;
}
let vis = visible && visible.get();
let curVis = this.termLoaded.get();
if (vis && !curVis) {
this.loadTerminal();
} else if (!vis && curVis) {
this.unloadTerminal(false);
}
}
loadTerminal(): void {
let { line, cmd } = this.props;
if (cmd == null) {
return;
}
let termElem = this.termRef.current;
if (termElem == null) {
console.log("cannot load terminal, no term elem found", line);
return;
}
WebShareModel.loadTerminalRenderer(termElem, line, cmd, this.props.width);
mobx.action(() => this.termLoaded.set(true))();
}
unloadTerminal(unmount: boolean): void {
let { line } = this.props;
WebShareModel.unloadRenderer(line.lineid);
if (!unmount) {
mobx.action(() => this.termLoaded.set(false))();
let termElem = this.termRef.current;
if (termElem != null) {
termElem.replaceChildren();
}
}
}
@boundMethod
clickTermBlock(e: any) {
let { line } = this.props;
let termWrap = WebShareModel.getTermWrap(line.lineid);
if (termWrap != null) {
termWrap.giveFocus();
}
mobx.action(() => {
WebShareModel.setSelectedLine(line.linenum);
})();
}
render() {
let { cmd, line, width, staticRender, visible } = this.props;
let isVisible = visible.get(); // for reaction
let usedRows = WebShareModel.getUsedRows(lineutil.getWebRendererContext(line), line, cmd, width);
let termHeight = termHeightFromRows(usedRows, WebShareModel.getTermFontSize());
let termLoaded = this.termLoaded.get();
let isFocused = WebShareModel.getSelectedLine() == line.linenum;
return (
<div
ref={this.elemRef}
key="term-wrap"
className={cn(
"terminal-wrapper",
{ "cmd-done": !lineutil.cmdStatusIsRunning(cmd.status) },
{ "zero-height": termHeight == 0 }
)}
>
<If condition={!isFocused}>
<div key="term-block" className="term-block" onClick={this.clickTermBlock}></div>
</If>
<div
key="term-connectelem"
className="terminal-connectelem"
ref={this.termRef}
data-lineid={line.lineid}
style={{ height: termHeight }}
></div>
<If condition={!termLoaded}>
<div key="term-loading" className="terminal-loading-message">
...
</div>
</If>
</div>
);
}
}
@mobxReact.observer
class WebLineView extends React.Component<
{
line: T.WebLine;
cmd: T.WebCmd;
topBorder: boolean;
width: number;
onHeightChange: T.LineHeightChangeCallbackType;
staticRender: boolean;
visible: OV<boolean>;
},
{}
> {
render() {
let { line } = this.props;
if (line.linetype == "text") {
return <WebLineTextView {...this.props} />;
}
if (line.linetype == "cmd") {
return <WebLineCmdView {...this.props} />;
}
return <div className="web-line line">invalid linetype "{line.linetype}"</div>;
}
}
@mobxReact.observer
class WebScreenView extends React.Component<{}, {}> {
viewRef: React.RefObject<any> = React.createRef();
width: OV<number> = mobx.observable.box(0, { name: "WebScreenView-width" });
handleResize_debounced: () => void;
rszObs: ResizeObserver;
constructor(props: any) {
super(props);
this.handleResize_debounced = debounce(1000, this.handleResize.bind(this));
}
componentDidMount(): void {
if (this.viewRef.current != null) {
let viewElem = this.viewRef.current;
this.rszObs = new ResizeObserver(this.handleResize_debounced.bind(this));
this.rszObs.observe(viewElem);
let width = viewElem.offsetWidth;
if (width > 0) {
mobx.action(() => {
this.width.set(width);
this.handleResize();
})();
}
}
}
handleResize(): void {
let viewElem = this.viewRef.current;
if (viewElem == null) {
return;
}
let width = viewElem.offsetWidth;
let height = viewElem.offsetHeight;
WebShareModel.setLastScreenSize({ width, height });
if (width != this.width.get()) {
WebShareModel.resizeWindow({ width: width, height: height });
mobx.action(() => {
this.width.set(width);
})();
}
}
@boundMethod
buildLineComponent(lineProps: T.LineFactoryProps): JSX.Element {
let line: T.WebLine = lineProps.line as T.WebLine;
let cmd = WebShareModel.getCmdById(lineProps.line.lineid);
return (
<WebLineView
key={line.lineid}
line={line}
cmd={cmd}
topBorder={lineProps.topBorder}
width={lineProps.width}
onHeightChange={lineProps.onHeightChange}
staticRender={lineProps.staticRender}
visible={lineProps.visible}
/>
);
}
renderEmpty(): any {
return (
<div className="web-screen-view" ref={this.viewRef}>
<div className="web-lines lines">
<div key="spacer" className="lines-spacer"></div>
</div>
</div>
);
}
render() {
let fullScreen = WebShareModel.fullScreen.get();
if (fullScreen == null || fullScreen.lines.length == 0) {
return this.renderEmpty();
}
return (
<div className="web-screen-view" ref={this.viewRef}>
<LinesView
screen={WebShareModel}
width={this.width.get()}
lines={fullScreen.lines}
renderMode="normal"
lineFactory={this.buildLineComponent}
/>
</div>
);
}
}
@mobxReact.observer
class WebShareMain extends React.Component<{}, {}> {
renderCopy() {
return <div className="footer-copy">&copy; 2023 Dashborg Inc</div>;
}
render() {
let screen = WebShareModel.fullScreen.get();
let errMessage = WebShareModel.errMessage.get();
let shareName = "";
if (screen != null) {
shareName = isBlank(screen.screen.sharename) ? "(no name)" : screen.screen.sharename;
}
return (
<div id="main">
<div className="logo-header">
<div className="logo-text">
<a target="_blank" href="https://www.commandline.dev">
[prompt]
</a>
</div>
<div className="flex-spacer" />
<a
href="https://www.commandline.dev/download/"
target="_blank"
className="download-button button is-link"
>
<span>Download Prompt</span>
<span className="icon is-small">
<i className="fa-sharp fa-solid fa-cloud-arrow-down" />
</span>
</a>
</div>
<div className="webshare-controls">
<div className="screen-sharename">{shareName}</div>
<div className="flex-spacer" />
<div className="sync-control">
<div>Sync Selection</div>
<Toggle
checked={WebShareModel.syncSelectedLine.get()}
onChange={(val) => WebShareModel.setSyncSelectedLine(val)}
/>
</div>
</div>
<div className="prompt-content">
<If condition={screen != null}>
<WebScreenView />
</If>
<If condition={errMessage != null}>
<div className="err-message">{WebShareModel.errMessage.get()}</div>
</If>
</div>
<div className="prompt-footer">
{this.renderCopy()}
<div className="flex-spacer" />
<a target="_blank" href="https://discord.gg/XfvZ334gwU" className="button is-link is-small">
<span className="icon is-small">
<i className="fa-brands fa-discord" />
</span>
<span>Discord</span>
</a>
</div>
</div>
);
}
}
export { WebShareMain };

View File

@ -1,679 +0,0 @@
import * as mobx from "mobx";
import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import { handleJsonFetchResponse, isModKeyPress, base64ToArray } from "./util";
import * as T from "./types";
import { TermWrap } from "./term";
import * as lineutil from "./lineutil";
import * as util from "./util";
import { windowWidthToCols, windowHeightToRows, termWidthFromCols, termHeightFromRows } from "./textmeasure";
import { WebShareWSControl } from "./webshare-ws";
// @ts-ignore
let PROMPT_DEV = __PROMPT_DEV__;
// @ts-ignore
let PROMPT_VERSION = __PROMPT_VERSION__;
// @ts-ignore
let PROMPT_BULILD = __PROMPT_BUILD__;
// @ts-ignore
let PROMPT_API_ENDPOINT = __PROMPT_API_ENDPOINT__;
// @ts-ignore
let PROMPT_WSAPI_ENDPOINT = __PROMPT_WSAPI_ENDPOINT__;
type OV<V> = mobx.IObservableValue<V>;
type OArr<V> = mobx.IObservableArray<V>;
type OMap<K, V> = mobx.ObservableMap<K, V>;
type CV<V> = mobx.IComputedValue<V>;
type PtyListener = {
receiveData(ptyPos: number, data: Uint8Array, reason?: string);
};
function isBlank(s: string) {
return s == null || s == "";
}
function getBaseUrl() {
return PROMPT_API_ENDPOINT;
}
function getBaseWSUrl() {
return PROMPT_WSAPI_ENDPOINT;
}
class WebShareModelClass {
viewKey: string;
screenId: string;
errMessage: OV<string> = mobx.observable.box(null, { name: "errMessage" });
fullScreen: OV<T.WebFullScreen> = mobx.observable.box(null, { name: "webScreen" });
terminals: Record<string, TermWrap> = {}; // lineid => TermWrap
renderers: Record<string, T.RendererModel> = {}; // lineid => RendererModel
contentHeightCache: Record<string, number> = {}; // lineid => height
wsControl: WebShareWSControl;
anchor: { anchorLine: number; anchorOffset: number } = { anchorLine: 0, anchorOffset: 0 };
selectedLine: OV<number> = mobx.observable.box(0, { name: "selectedLine" });
syncSelectedLine: OV<boolean> = mobx.observable.box(true, { name: "syncSelectedLine" });
lastScreenSize: T.WindowSize = null;
activePtyFetch: Record<string, boolean> = {}; // lineid -> active
localPtyOffsetMap: Record<string, number> = {};
remotePtyOffsetMap: Record<string, number> = {};
activeUpdateFetch: boolean = false;
remoteScreenVts: number = 0;
isDev: boolean = PROMPT_DEV;
constructor() {
let pathName = window.location.pathname;
let screenMatch = pathName.match(/\/share\/([a-f0-9-]+)/);
if (screenMatch != null) {
this.screenId = screenMatch[1];
}
let urlParams = new URLSearchParams(window.location.search);
this.viewKey = urlParams.get("viewkey");
if (this.screenId == null) {
this.screenId = urlParams.get("screenid");
}
setTimeout(() => this.loadFullScreenData(false), 10);
this.wsControl = new WebShareWSControl(
getBaseWSUrl(),
this.screenId,
this.viewKey,
this.wsMessageCallback.bind(this)
);
document.addEventListener("keydown", this.docKeyDownHandler.bind(this));
}
setErrMessage(msg: string): void {
mobx.action(() => {
this.errMessage.set(msg);
})();
}
setSyncSelectedLine(val: boolean): void {
mobx.action(() => {
this.syncSelectedLine.set(val);
if (val) {
let fullScreen = this.fullScreen.get();
if (fullScreen != null) {
this.selectedLine.set(fullScreen.screen.selectedline);
}
}
})();
}
setLastScreenSize(winSize: T.WindowSize) {
if (winSize == null || winSize.height == 0 || winSize.width == 0) {
return;
}
this.lastScreenSize = winSize;
}
getMaxContentSize(): T.WindowSize {
if (this.lastScreenSize == null) {
let width = termWidthFromCols(80, WebShareModel.getTermFontSize());
let height = termHeightFromRows(25, WebShareModel.getTermFontSize());
return { width, height };
}
let winSize = this.lastScreenSize;
let width = util.boundInt(winSize.width - 50, 100, 5000);
let height = util.boundInt(winSize.height - 100, 100, 5000);
return { width, height };
}
getIdealContentSize(): T.WindowSize {
if (this.lastScreenSize == null) {
let width = termWidthFromCols(80, WebShareModel.getTermFontSize());
let height = termHeightFromRows(25, WebShareModel.getTermFontSize());
return { width, height };
}
let winSize = this.lastScreenSize;
let width = util.boundInt(Math.ceil((winSize.width - 50) * 0.7), 100, 5000);
let height = util.boundInt(Math.ceil((winSize.height - 100) * 0.5), 100, 5000);
return { width, height };
}
getSelectedLine(): number {
return this.selectedLine.get();
}
getServerSelectedLine(): number {
let fullScreen = this.fullScreen.get();
if (fullScreen != null) {
return fullScreen.screen.selectedline;
}
}
setSelectedLine(lineNum: number): void {
mobx.action(() => {
this.selectedLine.set(lineNum);
})();
}
updateSelectedLineIndex(delta: number): void {
let fullScreen = this.fullScreen.get();
if (fullScreen == null) {
return;
}
let lineIndex = this.getLineIndex(this.selectedLine.get());
if (lineIndex == -1) {
return;
}
lineIndex += delta;
let lines = fullScreen.lines;
if (lineIndex < 0 || lineIndex >= lines.length) {
return;
}
this.setSelectedLine(lines[lineIndex].linenum);
}
setAnchorFields(anchorLine: number, anchorOffset: number, reason: string): void {
this.anchor.anchorLine = anchorLine;
this.anchor.anchorOffset = anchorOffset;
}
getAnchor(): { anchorLine: number; anchorOffset: number } {
return this.anchor;
}
getTermFontSize(): number {
return 12;
}
resizeWindow(winSize: T.WindowSize): void {
let cols = windowWidthToCols(winSize.width, this.getTermFontSize());
for (let lineId in this.terminals) {
let termWrap = this.terminals[lineId];
termWrap.resizeCols(cols);
}
}
mergeLine(fullScreen: T.WebFullScreen, newLine: T.WebLine) {
for (let i = 0; i < fullScreen.lines.length; i++) {
let line = fullScreen.lines[i];
if (line.lineid == newLine.lineid) {
fullScreen.lines[i] = newLine;
return;
}
if (line.linenum > newLine.linenum) {
fullScreen.lines.splice(i, 0, newLine);
return;
}
}
fullScreen.lines.push(newLine);
}
removeLine(fullScreen: T.WebFullScreen, lineId: string) {
for (let i = 0; i < fullScreen.lines.length; i++) {
let line = fullScreen.lines[i];
if (line.lineid == lineId) {
fullScreen.lines.splice(i, 1);
break;
}
}
for (let i = 0; i < fullScreen.cmds.length; i++) {
let cmd = fullScreen.cmds[i];
if (cmd.lineid == lineId) {
fullScreen.cmds.splice(i, 1);
break;
}
}
this.unloadRenderer(lineId);
}
setCmdDone(lineId: string): void {
let termWrap = this.getTermWrap(lineId);
if (termWrap != null) {
termWrap.cmdDone();
}
}
mergeCmd(fullScreen: T.WebFullScreen, newCmd: T.WebCmd) {
for (let i = 0; i < fullScreen.cmds.length; i++) {
let cmd = fullScreen.cmds[i];
if (cmd.lineid == newCmd.lineid) {
let wasRunning = lineutil.cmdStatusIsRunning(cmd.status);
let isRunning = lineutil.cmdStatusIsRunning(newCmd.status);
if (wasRunning && !isRunning) {
setTimeout(() => this.setCmdDone(cmd.lineid), 300);
}
fullScreen.cmds[i] = newCmd;
return;
}
}
fullScreen.cmds.push(newCmd);
}
mergeUpdate(msg: T.WebFullScreen) {
if (msg.screenid != this.screenId) {
console.log("bad WebFullScreen update, wrong screenid", msg.screenid);
return;
}
// console.log("merge screen-update", "vts=" + msg.vts);
// console.log("merge", "vts=" + msg.vts, msg);
mobx.action(() => {
let fullScreen = this.fullScreen.get();
if (fullScreen.vts >= msg.vts) {
console.log("stale merge", "cur-vts=" + fullScreen.vts, "merge-vts=" + msg.vts);
return;
}
fullScreen.vts = msg.vts;
if (msg.screen) {
fullScreen.screen = msg.screen;
if (this.syncSelectedLine.get()) {
this.selectedLine.set(msg.screen.selectedline);
}
}
if (msg.lines != null && msg.lines.length > 0) {
for (let line of msg.lines) {
if (line.archived) {
this.removeLine(fullScreen, line.lineid);
continue;
}
this.mergeLine(fullScreen, line);
}
}
if (msg.cmds != null && msg.cmds.length > 0) {
for (let cmd of msg.cmds) {
this.mergeCmd(fullScreen, cmd);
}
}
this.handleCmdPtyMap(msg.cmdptymap);
})();
}
handleCmdPtyMap(ptyMap: Record<string, number>) {
if (ptyMap == null) {
return;
}
for (let lineId in ptyMap) {
let newOffset = ptyMap[lineId];
this.remotePtyOffsetMap[lineId] = newOffset;
let localOffset = this.localPtyOffsetMap[lineId];
if (localOffset != null && localOffset < newOffset) {
this.runPtyFetch(lineId);
}
}
}
runPtyFetch(lineId: string) {
let prtn = this.checkFetchPtyData(lineId, false);
let ptyListener = this.getPtyListener(lineId);
if (ptyListener != null) {
prtn.then((ptydata) => {
ptyListener.receiveData(ptydata.pos, ptydata.data, "model-fetch");
if (ptydata.data.length > 0) {
setTimeout(() => this.checkFetchPtyData(lineId, false), 100);
}
});
}
}
getPtyListener(lineId: string) {
let termWrap = this.getTermWrap(lineId);
if (termWrap != null) {
return termWrap;
}
let renderer = this.getRenderer(lineId);
if (renderer != null) {
return renderer;
}
return null;
}
receivePtyData(lineId: string, ptyPos: number, data: Uint8Array, reason?: string): void {
let termWrap = this.getTermWrap(lineId);
if (termWrap != null) {
termWrap.receiveData(ptyPos, data, reason);
}
let renderer = this.getRenderer(lineId);
if (renderer != null) {
renderer.receiveData(ptyPos, data, reason);
}
}
checkFetchPtyData(lineId: string, reload: boolean): Promise<T.PtyDataType> {
let lineNum = this.getLineNumFromId(lineId);
if (this.activePtyFetch[lineId]) {
// console.log("check-fetch", lineNum, "already running");
return;
}
if (reload) {
this.localPtyOffsetMap[lineId] = 0;
}
let ptyOffset = this.localPtyOffsetMap[lineId];
if (ptyOffset == null) {
// console.log("check-fetch", lineNum, "no local offset");
return;
}
let remotePtyOffset = this.remotePtyOffsetMap[lineId];
if (ptyOffset >= remotePtyOffset) {
// up to date
return Promise.resolve({ pos: ptyOffset, data: new Uint8Array(0) });
}
this.activePtyFetch[lineId] = true;
let viewKey = WebShareModel.viewKey;
// console.log("fetch pty", lineNum, "pos=" + ptyOffset);
let usp = new URLSearchParams({
screenid: this.screenId,
viewkey: viewKey,
lineid: lineId,
pos: String(ptyOffset),
});
let url = new URL(getBaseUrl() + "/webshare/ptydata?" + usp.toString());
return fetch(url, { method: "GET", mode: "cors", cache: "no-cache" })
.then((resp) => {
if (!resp.ok) {
throw new Error(
sprintf("Bad fetch response for /webshare/ptydata: %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) => {
let dataArr = new Uint8Array(buf);
let newOffset = ptyOffset + dataArr.length;
// console.log("fetch pty success", lineNum, "len=" + dataArr.length, "pos => " + newOffset);
this.localPtyOffsetMap[lineId] = newOffset;
return { pos: ptyOffset, data: dataArr };
})
.finally(() => {
this.activePtyFetch[lineId] = false;
});
}
wsMessageCallback(msg: any) {
if (msg.type == "webscreen:update") {
// console.log("[ws] update vts", msg.vts);
if (msg.vts > this.remoteScreenVts) {
this.remoteScreenVts = msg.vts;
setTimeout(() => this.checkUpdateScreenData(), 10);
}
return;
}
if (msg.type == "success:webshare") {
return;
}
console.log("[ws] unhandled message", msg);
}
setWebFullScreen(screen: T.WebFullScreen) {
// console.log("got initial screen", "vts=" + screen.vts);
mobx.action(() => {
if (screen.lines == null) {
screen.lines = [];
}
if (screen.cmds == null) {
screen.cmds = [];
}
this.handleCmdPtyMap(screen.cmdptymap);
screen.cmdptymap = null;
this.fullScreen.set(screen);
this.wsControl.reconnect(true);
if (this.syncSelectedLine.get()) {
this.selectedLine.set(screen.screen.selectedline);
}
})();
}
loadTerminalRenderer(elem: Element, line: T.WebLine, cmd: T.WebCmd, width: number): void {
let lineId = cmd.lineid;
let termWrap = this.getTermWrap(lineId);
if (termWrap != null) {
console.log("term-wrap already exists for", lineId);
return;
}
let cols = windowWidthToCols(width, this.getTermFontSize());
let usedRows = this.getContentHeight(lineutil.getWebRendererContext(line));
if (line.contentheight != null && line.contentheight != -1) {
usedRows = line.contentheight;
}
let termContext = lineutil.getWebRendererContext(line);
termWrap = new TermWrap(elem, {
termContext: termContext,
usedRows: usedRows,
termOpts: cmd.termopts,
winSize: { height: 0, width: width },
dataHandler: null,
focusHandler: (focus: boolean) => this.setTermFocus(line.linenum, focus),
isRunning: lineutil.cmdStatusIsRunning(cmd.status),
customKeyHandler: this.termCustomKeyHandler.bind(this),
fontSize: this.getTermFontSize(),
ptyDataSource: getTermPtyData,
onUpdateContentHeight: (termContext: T.RendererContext, height: number) => {
this.setContentHeight(termContext, height);
},
});
this.terminals[lineId] = termWrap;
if (this.localPtyOffsetMap[lineId] == null) {
this.localPtyOffsetMap[lineId] = 0;
}
this.localPtyOffsetMap[lineId] = 0;
if (this.getSelectedLine() == line.linenum) {
termWrap.giveFocus();
}
return;
}
termCustomKeyHandler(e: any, termWrap: TermWrap): boolean {
if (e.type != "keydown" || isModKeyPress(e)) {
return false;
}
e.stopPropagation();
e.preventDefault();
if (e.code == "ArrowUp") {
termWrap.terminal.scrollLines(-1);
return false;
}
if (e.code == "ArrowDown") {
termWrap.terminal.scrollLines(1);
return false;
}
if (e.code == "PageUp") {
termWrap.terminal.scrollPages(-1);
return false;
}
if (e.code == "PageDown") {
termWrap.terminal.scrollPages(1);
return false;
}
return false;
}
setTermFocus(lineNum: number, focus: boolean): void {}
getContentHeight(context: T.RendererContext): number {
let key = context.lineId;
return this.contentHeightCache[key];
}
setContentHeight(context: T.RendererContext, height: number): void {
let key = context.lineId;
this.contentHeightCache[key] = height;
}
unloadRenderer(lineId: string): void {
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];
}
delete this.localPtyOffsetMap[lineId];
}
getUsedRows(context: T.RendererContext, line: T.WebLine, cmd: T.WebCmd, width: number): number {
if (cmd == null) {
return 0;
}
let termOpts = cmd.termopts;
if (!termOpts.flexrows) {
return termOpts.rows;
}
let termWrap = this.getTermWrap(cmd.lineid);
if (termWrap == null) {
let cols = windowWidthToCols(width, this.getTermFontSize());
let usedRows = this.getContentHeight(context);
if (usedRows != null) {
return usedRows;
}
if (line.contentheight != null && line.contentheight != -1) {
return line.contentheight;
}
return lineutil.cmdStatusIsRunning(cmd.status) ? 1 : 0;
}
return termWrap.getUsedRows();
}
getTermWrap(lineId: string): TermWrap {
return this.terminals[lineId];
}
getRenderer(lineId: string): T.RendererModel {
return this.renderers[lineId];
}
registerRenderer(lineId: string, renderer: T.RendererModel) {
this.renderers[lineId] = renderer;
if (this.localPtyOffsetMap[lineId] == null) {
this.localPtyOffsetMap[lineId] = 0;
}
}
checkUpdateScreenData(): void {
let fullScreen = this.fullScreen.get();
if (fullScreen == null) {
return;
}
// console.log("check-update", "vts=" + fullScreen.vts, "remote-vts=" + this.remoteScreenVts);
if (fullScreen.vts >= this.remoteScreenVts) {
return;
}
this.loadFullScreenData(true);
}
loadFullScreenData(update: boolean): void {
if (isBlank(this.screenId)) {
this.setErrMessage("No ScreenId Specified, Cannot Load.");
return;
}
if (isBlank(this.viewKey)) {
this.setErrMessage("No ViewKey Specified, Cannot Load.");
return;
}
if (this.activeUpdateFetch) {
// console.log("there is already an active update fetch");
return;
}
// console.log("running screen-data update");
this.activeUpdateFetch = true;
let urlParams: Record<string, string> = { screenid: this.screenId, viewkey: this.viewKey };
if (update) {
let fullScreen = this.fullScreen.get();
if (fullScreen != null) {
urlParams.vts = String(fullScreen.vts);
}
}
let usp = new URLSearchParams(urlParams);
let url = new URL(getBaseUrl() + "/webshare/screen?" + usp.toString());
fetch(url, { method: "GET", mode: "cors", cache: "no-cache" })
.then((resp) => handleJsonFetchResponse(url, resp))
.then((data) => {
let screen: T.WebFullScreen = data;
if (update) {
this.mergeUpdate(screen);
} else {
this.setWebFullScreen(screen);
}
setTimeout(() => this.checkUpdateScreenData(), 300);
})
.catch((err) => {
this.errMessage.set("Cannot get screen: " + err.message);
})
.finally(() => {
this.activeUpdateFetch = false;
});
}
getLineNumFromId(lineId: string): number {
let fullScreen = this.fullScreen.get();
if (fullScreen == null) {
return -1;
}
for (let i = 0; i < fullScreen.lines.length; i++) {
let line = fullScreen.lines[i];
if (line.lineid == lineId) {
return line.linenum;
}
}
return -1;
}
getLineIndex(lineNum: number): number {
let fullScreen = this.fullScreen.get();
if (fullScreen == null) {
return -1;
}
for (let i = 0; i < fullScreen.lines.length; i++) {
let line = fullScreen.lines[i];
if (line.linenum == lineNum) {
return i;
}
}
return -1;
}
getNumLines(): number {
let fullScreen = this.fullScreen.get();
if (fullScreen == null) {
return 0;
}
return fullScreen.lines.length;
}
getCmdById(lineId: string): T.WebCmd {
let fullScreen = this.fullScreen.get();
if (fullScreen == null) {
return null;
}
for (let cmd of fullScreen.cmds) {
if (cmd.lineid == lineId) {
return cmd;
}
}
return null;
}
docKeyDownHandler(e: any): void {
if (isModKeyPress(e)) {
return;
}
if (e.code == "PageUp" && e.getModifierState("Meta")) {
this.updateSelectedLineIndex(-1);
}
if (e.code == "PageDown" && e.getModifierState("Meta")) {
this.updateSelectedLineIndex(1);
}
}
}
function getTermPtyData(termContext: T.TermContextUnion): Promise<T.PtyDataType> {
if ("remoteId" in termContext) {
throw new Error("remote term ptydata is not supported in webshare");
}
return WebShareModel.checkFetchPtyData(termContext.lineId, true);
}
let WebShareModel: WebShareModelClass = null;
if ((window as any).WebShareModel == null) {
WebShareModel = new WebShareModelClass();
(window as any).WebShareModel = WebShareModel;
}
export { WebShareModel, getTermPtyData };

View File

@ -1,188 +0,0 @@
import * as mobx from "mobx";
import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import { WebShareWSMessage } from "./types";
import dayjs from "dayjs";
class WebShareWSControl {
wsConn: any;
open: mobx.IObservableValue<boolean>;
opening: boolean = false;
reconnectTimes: number = 0;
msgQueue: any[] = [];
messageCallback: (any) => void = null;
screenId: string = null;
viewKey: string = null;
wsUrl: string;
closed: boolean;
constructor(wsUrl: string, screenId: string, viewKey: string, messageCallback: (any) => void) {
this.wsUrl = wsUrl;
this.messageCallback = messageCallback;
this.screenId = screenId;
this.viewKey = viewKey;
this.open = mobx.observable.box(false, { name: "WSOpen" });
this.closed = true;
setInterval(this.sendPing, 20000);
}
close(): void {
this.closed = true;
if (this.wsConn != null) {
this.wsConn.close();
}
}
log(str: string) {
console.log("[wscontrol]", str);
}
@mobx.action
setOpen(val: boolean) {
mobx.action(() => {
this.open.set(val);
})();
}
connectNow(desc: string) {
this.closed = false;
if (this.open.get()) {
return;
}
this.log(sprintf("try reconnect (%s)", desc));
this.opening = true;
this.wsConn = new WebSocket(this.wsUrl);
this.wsConn.onopen = this.onopen;
this.wsConn.onmessage = this.onmessage;
this.wsConn.onclose = this.onclose;
// turns out onerror is not necessary (onclose always follows onerror)
// this.wsConn.onerror = this.onerror;
}
reconnect(forceClose?: boolean) {
this.closed = false;
if (this.open.get()) {
if (forceClose) {
this.wsConn.close(); // this will force a reconnect
}
return;
}
this.reconnectTimes++;
if (this.reconnectTimes > 20) {
this.log("cannot connect, giving up");
return;
}
let timeoutArr = [0, 0, 5, 5, 15, 30, 60, 300, 3600];
let timeout = timeoutArr[timeoutArr.length - 1];
if (this.reconnectTimes < timeoutArr.length) {
timeout = timeoutArr[this.reconnectTimes];
}
if (timeout > 0) {
this.log(sprintf("sleeping %ds", timeout));
}
setTimeout(() => {
this.connectNow(String(this.reconnectTimes));
}, timeout * 1000);
}
@boundMethod
onclose(event: any) {
// console.log("close", event);
if (event.wasClean) {
this.log("connection closed");
} else {
this.log("connection error/disconnected");
}
if (this.open.get() || this.opening) {
this.setOpen(false);
this.opening = false;
if (!this.closed) {
this.reconnect();
}
}
}
@boundMethod
onopen() {
this.log("connection open");
this.setOpen(true);
this.opening = false;
this.runMsgQueue();
this.sendWebShareInit();
// reconnectTimes is reset in onmessage:hello
}
runMsgQueue() {
if (!this.open.get()) {
return;
}
if (this.msgQueue.length == 0) {
return;
}
let msg = this.msgQueue.shift();
this.sendMessage(msg);
setTimeout(() => {
this.runMsgQueue();
}, 100);
}
@boundMethod
onmessage(event: any) {
let eventData = null;
if (event.data != null) {
eventData = JSON.parse(event.data);
}
if (eventData == null) {
return;
}
if (eventData.type == "ping") {
this.wsConn.send(JSON.stringify({ type: "pong", stime: Date.now() }));
return;
}
if (eventData.type == "pong") {
// nothing
return;
}
if (eventData.type == "hello") {
this.reconnectTimes = 0;
return;
}
if (this.messageCallback) {
try {
this.messageCallback(eventData);
} catch (e) {
this.log("[error] messageCallback " + e);
}
}
}
@boundMethod
sendPing() {
if (!this.open.get()) {
return;
}
this.wsConn.send(JSON.stringify({ type: "ping", stime: Date.now() }));
}
sendMessage(data: any) {
if (!this.open.get()) {
return;
}
this.wsConn.send(JSON.stringify(data));
}
pushMessage(data: any) {
if (!this.open.get()) {
this.msgQueue.push(data);
return;
}
this.sendMessage(data);
}
sendWebShareInit() {
let pk: WebShareWSMessage = { type: "webshare", screenid: this.screenId, viewkey: this.viewKey };
this.pushMessage(pk);
}
}
export { WebShareWSControl };

View File

@ -1,164 +0,0 @@
@import "../index.less";
body.prompt-webshare #main {
display: flex;
flex-direction: column;
.logo-header {
display: flex;
flex-direction: row;
padding: 10px;
border-bottom: 1px solid #777;
align-items: center;
flex-shrink: 0;
.logo-text {
a {
color: @prompt-green;
}
}
.screen-name {
color: @term-white;
margin-left: 20px;
}
.download-button {
margin-right: 20px;
}
}
.webshare-controls {
display: flex;
flex-direction: row;
flex-shrink: 0;
height: 40px;
align-items: center;
padding-left: 20px;
padding-right: 20px;
background-color: darken(@prompt-green, 30%);
color: @term-white;
.screen-sharename {
font-weight: bold;
}
.sync-control {
display: flex;
flex-direction: row;
color: @term-white;
align-items: center;
div:first-child {
margin-right: 5px;
}
}
}
.prompt-content {
flex-grow: 1;
padding: 10px 0;
display: flex;
flex-direction: row;
overflow: hidden;
.web-screen-view {
height: 100%;
width: 100%;
flex-grow: 1;
display: flex;
flex-direction: column;
.lines {
height: 100%;
flex-grow: 1;
padding-top: 0;
}
}
}
.prompt-footer {
display: flex;
flex-direction: row;
align-items: center;
color: #aaa;
border-top: 1px solid #777;
height: 50px;
padding-left: 20px;
padding-right: 20px;
flex-shrink: 0;
.footer-copy {
}
a {
display: block;
}
}
color: @term-white;
#app {
color: @term-white;
}
.lines .line-sep {
margin-left: 0px;
margin-right: 10px;
}
.lines .line.line-cmd {
.line-icon.copy-icon {
color: @term-white;
&:hover {
color: @term-bright-white;
}
}
.copied-indicator {
z-index: 10;
}
}
}
.webshare-view {
.webshare-item {
padding: 4px 5px 8px 15px;
margin-bottom: 4px;
border-top: 1px solid white;
display: flex;
flex-direction: row;
position: relative;
min-height: 55px;
align-items: center;
&:first-child {
border-top: 0;
}
.webshare-vic {
width: 200px;
.webshare-vic-link {
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
.actions {
display: flex;
flex-direction: row;
gap: 10px;
}
&:hover {
background-color: #222;
}
}
}

View File

@ -1,38 +0,0 @@
import * as mobx from "mobx";
import * as React from "react";
import { createRoot } from "react-dom/client";
import { sprintf } from "sprintf-js";
import { WebShareMain } from "./webshare-elems";
import { loadFonts } from "./util";
import { WebShareModel } from "./webshare-model";
import * as textmeasure from "./textmeasure";
// @ts-ignore
let PROMPT_DEV = __PROMPT_DEV__;
// @ts-ignore
let PROMPT_VERSION = __PROMPT_VERSION__;
// @ts-ignore
let PROMPT_BUILD = __PROMPT_BUILD__;
loadFonts();
document.addEventListener("DOMContentLoaded", () => {
let elem = document.getElementById("app");
let root = createRoot(elem);
let reactElem = React.createElement(WebShareMain, null, null);
// @check:font
let isFontLoaded = document.fonts.check("12px 'JetBrains Mono'");
if (isFontLoaded) {
root.render(reactElem);
} else {
document.fonts.ready.then(() => {
root.render(reactElem);
});
}
});
(window as any).textmeasure = textmeasure;
(window as any).mobx = mobx;
(window as any).sprintf = sprintf;
console.log("PROMPT webshare", PROMPT_VERSION, PROMPT_BUILD);

Binary file not shown.

View File

@ -4,7 +4,7 @@ const CopyPlugin = require("copy-webpack-plugin");
module.exports = { module.exports = {
mode: "development", mode: "development",
entry: { entry: {
emain: ["./src/emain.ts"], emain: ["./src/electron/emain.ts"],
}, },
target: "electron-main", target: "electron-main",
output: { output: {
@ -50,7 +50,7 @@ module.exports = {
}, },
plugins: [ plugins: [
new CopyPlugin({ new CopyPlugin({
patterns: [{ from: "src/preload.js", to: "preload.js" }], patterns: [{ from: "src/electron/preload.js", to: "preload.js" }],
}), }),
], ],
resolve: { resolve: {

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +0,0 @@
/*!
* Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license (Commercial License)
* Copyright 2023 Fonticons, Inc.
*/
:host,:root{--fa-style-family-sharp:"Font Awesome 6 Sharp";--fa-font-sharp-regular:normal 400 1em/1 "Font Awesome 6 Sharp"}@font-face{font-family:"Font Awesome 6 Sharp";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-sharp-regular-400.woff2) format("woff2"),url(../webfonts/fa-sharp-regular-400.ttf) format("truetype")}.fa-regular,.fasr{font-weight:400}

View File

@ -1,6 +0,0 @@
/*!
* Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license (Commercial License)
* Copyright 2023 Fonticons, Inc.
*/
:host,:root{--fa-style-family-sharp:"Font Awesome 6 Sharp";--fa-font-sharp-solid:normal 900 1em/1 "Font Awesome 6 Sharp"}@font-face{font-family:"Font Awesome 6 Sharp";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-sharp-solid-900.woff2) format("woff2"),url(../webfonts/fa-sharp-solid-900.ttf) format("truetype")}.fa-solid,.fass{font-weight:900}

View File

@ -1,6 +0,0 @@
/*!
* Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license (Commercial License)
* Copyright 2023 Fonticons, Inc.
*/
:host,:root{--fa-style-family-classic:"Font Awesome 6 Pro";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Pro"}@font-face{font-family:"Font Awesome 6 Pro";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-weight:900}

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