Refactored now (#29)
* redy to recover Terminal dir * as Mike asked ... commit what you have :) * fir anyone to have a look - DONT RUN * works !!!
@ -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>
|
@ -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>
|
@ -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 };
|
Before Width: | Height: | Size: 471 B After Width: | Height: | Size: 471 B |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 448 B After Width: | Height: | Size: 448 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 305 B After Width: | Height: | Size: 305 B |
Before Width: | Height: | Size: 237 B After Width: | Height: | Size: 237 B |
Before Width: | Height: | Size: 327 B After Width: | Height: | Size: 327 B |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 627 B After Width: | Height: | Size: 627 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 638 B After Width: | Height: | Size: 638 B |
Before Width: | Height: | Size: 476 B After Width: | Height: | Size: 476 B |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
@ -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";
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
@import "../index.less";
|
@import "../../index.less";
|
||||||
|
|
||||||
.info-message {
|
.info-message {
|
||||||
position: relative;
|
position: relative;
|
@ -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";
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
@import "../../index.less";
|
@import "../../../index.less";
|
||||||
|
|
||||||
// modal css (also includes settings-field)
|
// modal css (also includes settings-field)
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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";
|
||||||
|
|
70
src/app/common/prompt/prompt.less
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
@import "../index.less";
|
@import "../../index.less";
|
||||||
|
|
||||||
.modal.prompt-modal.remotes-modal {
|
.modal.prompt-modal.remotes-modal {
|
||||||
.modal-content {
|
.modal-content {
|
@ -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>;
|
@ -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";
|
||||||
|
|
@ -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}
|
@ -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>;
|
@ -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";
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
@ -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);
|
@ -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);
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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) {
|
103
src/app/workspace/screen/screenview.less
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 };
|
|
193
src/app/workspace/screen/tabs.tsx
Normal 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 };
|
@ -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 };
|
@ -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;
|
102
src/index.less
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
13
src/index.ts
@ -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;
|
||||||
|
@ -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);
|
@ -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 {
|
@ -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" } });
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
296
src/plugins/renderer/basicrenderer.tsx
Normal 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 };
|
@ -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 };
|
@ -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";
|
||||||
|
|
@ -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;
|
63
src/plugins/terminal/terminal.less
Normal 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);
|
||||||
|
}
|
@ -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.
|
@ -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 };
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 };
|
|
@ -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}>
|
|
||||||
...▼
|
|
||||||
</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> </div>
|
|
||||||
<If condition={!isBlank(renderer) && renderer != "terminal"}>
|
|
||||||
<div className="renderer">
|
|
||||||
<i className="fa-sharp fa-solid fa-fill" />
|
|
||||||
{renderer}
|
|
||||||
</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">© 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 };
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
@ -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: {
|
||||||
|
1
webshare/static/bulma-0.9.4.min.css
vendored
Before Width: | Height: | Size: 15 KiB |
@ -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}
|
|
@ -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}
|
|
@ -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}
|
|