big refactor for webshare, reduce GlobalModel dependencies

This commit is contained in:
sawka 2023-03-28 22:53:18 -07:00
parent 6a42dcf07f
commit e83abeaa5b
25 changed files with 705 additions and 233 deletions

View File

@ -547,7 +547,7 @@ class LineContainer extends React.Component<{historyId : string, width : number}
<If condition={session == null}>
<div className="no-line-context"/>
</If>
<Line screen={hvm.specialLineContainer} line={this.line} width={width} staticRender={false} visible={this.visible} onHeightChange={this.handleHeightChange} overrideCollapsed={this.overrideCollapsed} topBorder={false} renderMode="normal"/>
<Line screen={hvm.specialLineContainer} line={this.line} width={width} staticRender={false} visible={this.visible} onHeightChange={this.handleHeightChange} overrideCollapsed={this.overrideCollapsed} topBorder={false} renderMode="normal" noSelect={true}/>
</div>
);
}

View File

@ -4,7 +4,8 @@ import * as mobxReact from "mobx-react";
import cn from "classnames";
import {If, For, When, Otherwise, Choose} from "tsx-control-statements/components";
import {WindowSize, RendererContext, TermOptsType, LineType, RendererOpts} from "./types";
import {getPtyData, termWidthFromCols, termHeightFromRows, GlobalModel, LineContainerModel} from "./model";
import {LineContainerModel} from "./model";
import {termWidthFromCols, termHeightFromRows} from "./textmeasure";
import {incObs} from "./util";
import {PtyDataBuffer} from "./ptydata";

View File

@ -6,13 +6,17 @@ import {boundMethod} from "autobind-decorator";
import dayjs from "dayjs";
import localizedFormat from 'dayjs/plugin/localizedFormat';
import {If, For, When, Otherwise, Choose} from "tsx-control-statements/components";
import {GlobalModel, GlobalCommandRunner, Session, Cmd, ScreenLines, Screen, windowWidthToCols, windowHeightToRows, termHeightFromRows, termWidthFromCols, getRendererContext, getRendererType} from "./model";
import {GlobalModel, GlobalCommandRunner, Session, Cmd, ScreenLines, Screen, getRendererContext} from "./model";
import {windowWidthToCols, windowHeightToRows, termHeightFromRows, termWidthFromCols} from "./textmeasure";
import type {LineType, CmdDataType, FeStateType, RemoteType, RemotePtrType, RenderModeType, RendererContext, RendererOpts, SimpleBlobRendererComponent, RendererPluginType} from "./types";
import cn from "classnames";
import {TermWrap} from "./term";
import type {LineContainerModel} from "./model";
import {renderCmdText} from "./elements";
import {SimpleBlobRendererModel, SimpleBlobRenderer} from "./simplerenderer";
import {isBlank} from "./util";
import {PluginModel} from "./plugins";
import * as lineutil from "./lineutil";
dayjs.extend(localizedFormat)
@ -24,10 +28,6 @@ type HeightChangeCallbackType = (lineNum : number, newHeight : number, oldHeight
type RendererComponentProps = {screen : LineContainerModel, line : LineType, width : number, staticRender : boolean, visible : OV<boolean>, onHeightChange : HeightChangeCallbackType, collapsed : boolean};
type RendererComponentType = { new(props : RendererComponentProps) : React.Component<RendererComponentProps, {}> };
function isBlank(s : string) : boolean {
return (s == null || s == "");
}
function makeFullRemoteRef(ownerName : string, remoteRef : string, name : string) : string {
if (isBlank(ownerName) && isBlank(name)) {
return remoteRef;
@ -75,40 +75,8 @@ function getCwdStr(remote : RemoteType, state : FeStateType) : string {
return cwd;
}
function getLineDateTimeStr(ts : number) : string {
let lineDate = new Date(ts);
let nowDate = new Date();
if (nowDate.getFullYear() != lineDate.getFullYear()) {
return dayjs(lineDate).format("ddd L LTS");
}
else if (nowDate.getMonth() != lineDate.getMonth() || nowDate.getDate() != lineDate.getDate()) {
let yesterdayDate = (new Date());
yesterdayDate.setDate(yesterdayDate.getDate()-1);
if (yesterdayDate.getMonth() == lineDate.getMonth() && yesterdayDate.getDate() == lineDate.getDate()) {
return "Yesterday " + dayjs(lineDate).format("LTS");;
}
return dayjs(lineDate).format("ddd L LTS");
}
else {
return dayjs(lineDate).format("LTS");
}
}
@mobxReact.observer
class LineAvatar extends React.Component<{line : LineType, cmd : Cmd}, {}> {
@boundMethod
handleRightClick(e : any) {
e.preventDefault();
e.stopPropagation();
let {line} = this.props;
if (line != null) {
mobx.action(() => {
GlobalModel.lineSettingsModal.set(line);
})();
}
}
class LineAvatar extends React.Component<{line : LineType, cmd : Cmd, onRightClick? : (e : any) => void}, {}> {
render() {
let {line, cmd} = this.props;
let lineNumStr = (line.linenumtemp ? "~" : "") + String(line.linenum);
@ -116,7 +84,7 @@ class LineAvatar extends React.Component<{line : LineType, cmd : Cmd}, {}> {
let rtnstate = (cmd != null ? cmd.getRtnState() : false);
let isComment = (line.linetype == "text");
return (
<div onContextMenu={(e) => this.handleRightClick(e)} className={cn("avatar", "num-"+lineNumStr.length, "status-" + status, {"ephemeral": line.ephemeral}, {"rtnstate": rtnstate})}>
<div onContextMenu={this.props.onRightClick} className={cn("avatar", "num-"+lineNumStr.length, "status-" + status, {"ephemeral": line.ephemeral}, {"rtnstate": rtnstate})}>
{lineNumStr}
<If condition={status == "hangup" || status == "error"}>
<i className="fa-sharp fa-solid fa-triangle-exclamation status-icon"/>
@ -132,7 +100,6 @@ class LineAvatar extends React.Component<{line : LineType, cmd : Cmd}, {}> {
}
}
@mobxReact.observer
class LineCmd extends React.Component<{screen : LineContainerModel, line : LineType, width : number, staticRender : boolean, visible : OV<boolean>, onHeightChange : HeightChangeCallbackType, topBorder : boolean, renderMode : RenderModeType, overrideCollapsed : OV<boolean>, noSelect? : boolean, showHints? : boolean}, {}> {
lineRef : React.RefObject<any> = React.createRef();
@ -231,18 +198,18 @@ class LineCmd extends React.Component<{screen : LineContainerModel, line : LineT
</div>
</div>
<div key="meta3" className="meta meta-line3 cmdtext-expanded-wrapper">
<div className="cmdtext-expanded">{cmd.getFullCmdText()}</div>
<div className="cmdtext-expanded">{lineutil.getFullCmdText(cmd.getCmdStr())}</div>
</div>
</React.Fragment>
);
}
let isMultiLine = cmd.isMultiLineCmdText();
let isMultiLine = lineutil.isMultiLineCmdText(cmd.getCmdStr());
return (
<div key="meta2" className="meta meta-line2" ref={this.cmdTextRef}>
<div className="metapart-mono cmdtext">
<Prompt rptr={cmd.remote} festate={cmd.getRemoteFeState()}/>
<span> </span>
<span>{cmd.getSingleLineCmdText()}</span>
<span>{lineutil.getSingleLineCmdText(cmd.getCmdStr())}</span>
</div>
<If condition={this.isOverflow.get() || isMultiLine}>
<div className="cmdtext-overflow" onClick={this.handleExpandCmd}>...&#x25BC;</div>
@ -389,6 +356,21 @@ class LineCmd extends React.Component<{screen : LineContainerModel, line : LineT
return height;
}
@boundMethod
onAvatarRightClick(e : any) : void {
let {line, noSelect} = this.props;
if (noSelect) {
return;
}
e.preventDefault();
e.stopPropagation();
if (line != null) {
mobx.action(() => {
GlobalModel.lineSettingsModal.set(line);
})();
}
}
renderSimple() {
let {screen, line, topBorder} = this.props;
let cmd = screen.getCmd(line);
@ -436,7 +418,7 @@ class LineCmd extends React.Component<{screen : LineContainerModel, line : LineT
renderMetaWrap(cmd : Cmd) {
let {line} = this.props;
let model = GlobalModel;
let formattedTime = getLineDateTimeStr(line.ts);
let formattedTime = lineutil.getLineDateTimeStr(line.ts);
let termOpts = cmd.getTermOpts();
let remote = model.getRemote(cmd.remoteId);
let renderer = line.renderer;
@ -460,6 +442,16 @@ class LineCmd extends React.Component<{screen : LineContainerModel, line : LineT
);
}
getRendererOpts(cmd : Cmd) : RendererOpts {
let {screen} = this.props;
return {
maxSize: screen.getMaxContentSize(),
idealSize: screen.getIdealContentSize(),
termOpts: cmd.getTermOpts(),
termFontSize: GlobalModel.termFontSize.get(),
};
}
render() {
let {screen, line, width, staticRender, visible, topBorder, renderMode} = this.props;
let model = GlobalModel;
@ -468,7 +460,7 @@ class LineCmd extends React.Component<{screen : LineContainerModel, line : LineT
if (staticRender || !isVisible) {
return this.renderSimple();
}
let formattedTime = getLineDateTimeStr(line.ts);
let formattedTime = lineutil.getLineDateTimeStr(line.ts);
let cmd = screen.getCmd(line);
if (cmd == null) {
return (
@ -507,16 +499,16 @@ class LineCmd extends React.Component<{screen : LineContainerModel, line : LineT
let rendererPlugin : RendererPluginType = null;
let isNoneRenderer = (line.renderer == "none");
if (!isBlank(line.renderer) && line.renderer != "terminal" && !isNoneRenderer) {
rendererPlugin = GlobalModel.getRendererPluginByName(line.renderer);
rendererPlugin = PluginModel.getRendererPluginByName(line.renderer);
}
let rendererType = getRendererType(line);
let rendererType = lineutil.getRendererType(line);
return (
<div className={mainDivCn}
ref={this.lineRef} onClick={this.handleClick}
data-lineid={line.lineid} data-linenum={line.linenum} data-screenid={line.screenid} data-cmdid={line.cmdid}>
<div key="focus" className={cn("focus-indicator", {"selected": isSelected}, {"active": isSelected && isFocused}, {"fg-focus": isFgFocused})}/>
<div key="header" className={cn("line-header", {"is-expanded": isExpanded}, {"is-collapsed": isCollapsed})}>
<LineAvatar line={line} cmd={cmd}/>
<LineAvatar line={line} cmd={cmd} onRightClick={this.onAvatarRightClick}/>
<If condition={renderMode == "collapsed"}>
<div key="collapsed" className="collapsed-indicator" title={isCollapsed ? "output collapsed, click to show" : "click to hide output" } onClick={this.handleCollapsedClick}>
<If condition={isCollapsed}><i className="fa-sharp fa-solid fa-caret-right"/></If>
@ -535,7 +527,7 @@ class LineCmd extends React.Component<{screen : LineContainerModel, line : LineT
<TerminalRenderer screen={screen} line={line} width={width} staticRender={staticRender} visible={visible} onHeightChange={this.handleHeightChange} collapsed={isCollapsed}/>
</If>
<If condition={rendererPlugin != null}>
<SimpleBlobRenderer lcm={screen} line={line} cmd={cmd} plugin={rendererPlugin} onHeightChange={this.handleHeightChange}/>
<SimpleBlobRenderer lcm={screen} line={line} cmd={cmd} plugin={rendererPlugin} onHeightChange={this.handleHeightChange} rendererOpts={this.getRendererOpts(cmd)}/>
</If>
<If condition={!isCollapsed && cmd.getRtnState()}>
<div key="rtnstate" className="cmd-rtnstate" style={{visibility: ((cmd.getStatus() == "done") ? "visible" : "hidden")}}>
@ -622,9 +614,24 @@ class LineText extends React.Component<{screen : LineContainerModel, line : Line
GlobalCommandRunner.screenSelectLine(String(line.linenum));
}
@boundMethod
onAvatarRightClick(e : any) : void {
let {line, noSelect} = this.props;
if (noSelect) {
return;
}
e.preventDefault();
e.stopPropagation();
if (line != null) {
mobx.action(() => {
GlobalModel.lineSettingsModal.set(line);
})();
}
}
render() {
let {screen, line, topBorder, renderMode} = this.props;
let formattedTime = getLineDateTimeStr(line.ts);
let formattedTime = lineutil.getLineDateTimeStr(line.ts);
let isSelected = mobx.computed(() => (screen.getSelectedLine() == line.linenum), {name: "computed-isSelected"}).get();
let isFocused = mobx.computed(() => (screen.getFocusType() == "cmd"), {name: "computed-isFocused"}).get();
let isCollapsed = (renderMode == "collapsed");
@ -638,7 +645,7 @@ class LineText extends React.Component<{screen : LineContainerModel, line : Line
return (
<div className={mainClass} data-lineid={line.lineid} data-linenum={line.linenum} data-screenid={line.screenid} onClick={this.clickHandler}>
<div className={cn("focus-indicator", {"selected": isSelected}, {"active": isSelected && isFocused})}/>
<LineAvatar line={line} cmd={null}/>
<LineAvatar line={line} cmd={null} onRightClick={this.onAvatarRightClick}/>
<div className="line-content">
<div className="meta">
<div className="ts">{formattedTime}</div>

76
src/lineutil.ts Normal file
View File

@ -0,0 +1,76 @@
import dayjs from "dayjs";
import localizedFormat from 'dayjs/plugin/localizedFormat';
import {isBlank, getDateStr} from "./util";
import {LineType, WebLine} from "./types";
dayjs.extend(localizedFormat)
function getRendererType(line : LineType|WebLine) : "terminal" | "plugin" {
if (isBlank(line.renderer) || line.renderer == "terminal") {
return "terminal";
}
return "plugin";
}
function getLineDateStr(todayDate : string, yesterdayDate : string, ts : number) : string {
let lineDate = new Date(ts);
let dateStr = getDateStr(lineDate);
if (dateStr == todayDate) {
return "today";
}
if (dateStr == yesterdayDate) {
return "yesterday";
}
return dateStr;
}
function getLineDateTimeStr(ts : number) : string {
let lineDate = new Date(ts);
let nowDate = new Date();
if (nowDate.getFullYear() != lineDate.getFullYear()) {
return dayjs(lineDate).format("ddd L LTS");
}
else if (nowDate.getMonth() != lineDate.getMonth() || nowDate.getDate() != lineDate.getDate()) {
let yesterdayDate = (new Date());
yesterdayDate.setDate(yesterdayDate.getDate()-1);
if (yesterdayDate.getMonth() == lineDate.getMonth() && yesterdayDate.getDate() == lineDate.getDate()) {
return "Yesterday " + dayjs(lineDate).format("LTS");;
}
return dayjs(lineDate).format("ddd L LTS");
}
else {
return dayjs(lineDate).format("LTS");
}
}
function isMultiLineCmdText(cmdText : string) : boolean {
if (cmdText == null) {
return false;
}
cmdText = cmdText.trim();
let nlIdx = cmdText.indexOf("\n");
return (nlIdx != -1);
}
function getFullCmdText(cmdText : string) {
if (cmdText == null) {
return "(none)";
}
cmdText = cmdText.trim();
return cmdText;
}
function getSingleLineCmdText(cmdText : string) {
if (cmdText == null) {
return "(none)";
}
cmdText = cmdText.trim();
let nlIdx = cmdText.indexOf("\n");
if (nlIdx != -1) {
cmdText = cmdText.substr(0, nlIdx);
}
return cmdText;
}
export {getRendererType, getLineDateStr, getLineDateTimeStr, isMultiLineCmdText, getFullCmdText, getSingleLineCmdText};

View File

@ -11,7 +11,8 @@ import dayjs from "dayjs";
import type {SessionDataType, LineType, CmdDataType, RemoteType, RemoteStateType, RemoteInstanceType, RemotePtrType, HistoryItem, HistoryQueryOpts, RemoteEditType, FeStateType, ContextMenuOpts, BookmarkType, RenderModeType, ClientMigrationInfo} from "./types";
import type * as T from "./types";
import localizedFormat from 'dayjs/plugin/localizedFormat';
import {GlobalModel, GlobalCommandRunner, Session, Cmd, ScreenLines, Screen, riToRPtr, windowWidthToCols, windowHeightToRows, termHeightFromRows, termWidthFromCols, TabColors, RemoteColors} from "./model";
import {GlobalModel, GlobalCommandRunner, Session, Cmd, ScreenLines, Screen, riToRPtr, TabColors, RemoteColors} from "./model";
import {windowWidthToCols, windowHeightToRows, termHeightFromRows, termWidthFromCols} from "./textmeasure";
import {isModKeyPress, boundInt} from "./util";
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'

View File

@ -5,9 +5,9 @@ import {debounce} from "throttle-debounce";
import {handleJsonFetchResponse, base64ToArray, genMergeData, genMergeDataMap, genMergeSimpleData, boundInt, isModKeyPress} from "./util";
import {TermWrap} from "./term";
import {v4 as uuidv4} from "uuid";
import type {SessionDataType, LineType, RemoteType, HistoryItem, RemoteInstanceType, RemotePtrType, CmdDataType, FeCmdPacketType, TermOptsType, RemoteStateType, ScreenDataType, ScreenOptsType, PtyDataUpdateType, ModelUpdateType, UpdateMessage, InfoType, CmdLineUpdateType, UIContextType, HistoryInfoType, HistoryQueryOpts, FeInputPacketType, TermWinSize, RemoteInputPacketType, FeStateType, ContextMenuOpts, RendererContext, RendererModel, PtyDataType, BookmarkType, ClientDataType, HistoryViewDataType, AlertMessageType, HistorySearchParams, FocusTypeStrs, ScreenLinesType, HistoryTypeStrs, RendererPluginType, WindowSize, ClientMigrationInfo, WebShareOpts} from "./types";
import type {SessionDataType, LineType, RemoteType, HistoryItem, RemoteInstanceType, RemotePtrType, CmdDataType, FeCmdPacketType, TermOptsType, RemoteStateType, ScreenDataType, ScreenOptsType, PtyDataUpdateType, ModelUpdateType, UpdateMessage, InfoType, CmdLineUpdateType, UIContextType, HistoryInfoType, HistoryQueryOpts, FeInputPacketType, TermWinSize, RemoteInputPacketType, FeStateType, ContextMenuOpts, RendererContext, RendererModel, PtyDataType, BookmarkType, ClientDataType, HistoryViewDataType, AlertMessageType, HistorySearchParams, FocusTypeStrs, ScreenLinesType, HistoryTypeStrs, RendererPluginType, WindowSize, ClientMigrationInfo, WebShareOpts, TermContextUnion} from "./types";
import {WSControl} from "./ws";
import {measureText, getMonoFontSize} from "./textmeasure";
import {measureText, getMonoFontSize, windowWidthToCols, windowHeightToRows, termWidthFromCols, termHeightFromRows} from "./textmeasure";
import dayjs from "dayjs";
import localizedFormat from 'dayjs/plugin/localizedFormat';
import customParseFormat from 'dayjs/plugin/customParseFormat';
@ -18,8 +18,6 @@ dayjs.extend(localizedFormat)
var GlobalUser = "sawka";
const RemotePtyRows = 8; // also in main.tsx
const RemotePtyCols = 80;
const MinTermCols = 10;
const MaxTermCols = 1024;
const ProdServerEndpoint = "http://localhost:1619";
const ProdServerWsEndpoint = "ws://localhost:1623";
const DevServerEndpoint = "http://localhost:8090";
@ -60,13 +58,6 @@ type SWLinePtr = {
screen : Screen,
};
function getRendererType(line : LineType) : "terminal" | "plugin" {
if (isBlank(line.renderer) || line.renderer == "terminal") {
return "terminal";
}
return "plugin";
}
function getRendererContext(line : LineType) : RendererContext {
return {
screenId: line.screenid,
@ -76,32 +67,6 @@ function getRendererContext(line : LineType) : RendererContext {
};
}
function windowWidthToCols(width : number, fontSize : number) : number {
let dr = getMonoFontSize(fontSize);
let cols = Math.trunc((width - 50) / dr.width) - 1;
cols = boundInt(cols, MinTermCols, MaxTermCols);
return cols;
}
function windowHeightToRows(height : number, fontSize : number) : number {
let dr = getMonoFontSize(fontSize);
let rows = Math.floor((height - 80) / dr.height) - 1;
if (rows <= 0) {
rows = 1;
}
return rows;
}
function termWidthFromCols(cols : number, fontSize : number) : number {
let dr = getMonoFontSize(fontSize);
return Math.ceil(dr.width*cols) + 15;
}
function termHeightFromRows(rows : number, fontSize : number) : number {
let dr = getMonoFontSize(fontSize);
return Math.ceil(dr.height*rows);
}
function cmdStatusIsRunning(status : string) : boolean {
return status == "running" || status == "detached";
}
@ -227,38 +192,6 @@ class Cmd {
return this.data.get().festate;
}
isMultiLineCmdText() : boolean {
let cmdText = this.data.get().cmdstr;
if (cmdText == null) {
return false;
}
cmdText = cmdText.trim();
let nlIdx = cmdText.indexOf("\n");
return (nlIdx != -1);
}
getSingleLineCmdText() {
let cmdText = this.data.get().cmdstr;
if (cmdText == null) {
return "(none)";
}
cmdText = cmdText.trim();
let nlIdx = cmdText.indexOf("\n");
if (nlIdx != -1) {
cmdText = cmdText.substr(0, nlIdx);
}
return cmdText;
}
getFullCmdText() {
let cmdText = this.data.get().cmdstr;
if (cmdText == null) {
return "(none)";
}
cmdText = cmdText.trim();
return cmdText;
}
isRunning() : boolean {
let data = this.data.get();
return cmdStatusIsRunning(data.status);
@ -691,6 +624,8 @@ class Screen {
isRunning: cmd.isRunning(),
customKeyHandler: this.termCustomKeyHandler.bind(this),
fontSize: GlobalModel.termFontSize.get(),
ptyDataSource: getTermPtyData,
onUpdateContentHeight: (termContext : RendererContext, height : number) => { GlobalModel.setContentHeight(termContext, height); },
});
this.terminals[cmdId] = termWrap;
if ((this.focusType.get() == "cmd" || this.focusType.get() == "cmd-fg") && this.selectedLine.get() == line.linenum) {
@ -1639,6 +1574,8 @@ class InputModel {
focusHandler: this.setRemoteTermWrapFocus.bind(this),
isRunning: true,
fontSize: GlobalModel.termFontSize.get(),
ptyDataSource: getTermPtyData,
onUpdateContentHeight: null,
});
}
}
@ -1757,7 +1694,8 @@ class SpecialHistoryViewLineContainer {
isRunning: cmd.isRunning(),
customKeyHandler: null,
fontSize: GlobalModel.termFontSize.get(),
noSetTUR: true,
ptyDataSource: getTermPtyData,
onUpdateContentHeight: null,
});
this.terminal = termWrap;
return;
@ -2369,7 +2307,6 @@ class Model {
sessionSettingsModal : OV<string> = mobx.observable.box(null, {name: "sessionSettingsModal"});
clientSettingsModal : OV<boolean> = mobx.observable.box(false, {name: "clientSettingsModal"});
lineSettingsModal : OV<LineType> = mobx.observable.box(null, {name: "lineSettingsModal"});
rendererPlugins : RendererPluginType[] = [];
inputModel : InputModel;
bookmarksModel : BookmarksModel;
@ -2423,20 +2360,6 @@ class Model {
getApi().reloadWindow();
}
registerRendererPlugin(plugin : RendererPluginType) {
if (isBlank(plugin.name)) {
throw new Error("invalid plugin, no name");
}
if (plugin.name == "terminal" || plugin.name == "none") {
throw new Error(sprintf("invalid plugin, name '%s' is reserved", plugin.name));
}
let existingPlugin = this.getRendererPluginByName(plugin.name);
if (existingPlugin != null) {
throw new Error(sprintf("plugin with name %s already registered", plugin.name));
}
this.rendererPlugins.push(plugin);
}
getHasClientStop() : boolean {
if (this.clientData.get() == null) {
return true;
@ -2448,16 +2371,6 @@ class Model {
return false;
}
getRendererPluginByName(name : string) : RendererPluginType {
for (let i=0; i<this.rendererPlugins.length; i++) {
let plugin = this.rendererPlugins[i];
if (plugin.name == name) {
return plugin;
}
}
return null;
}
showAlert(alertMessage : AlertMessageType) : Promise<boolean> {
mobx.action(() => {
this.alertMessage.set(alertMessage);
@ -3494,6 +3407,13 @@ function _getPtyDataFromUrl(url : string) : Promise<PtyDataType> {
});
}
function getTermPtyData(termContext : TermContextUnion) : Promise<PtyDataType> {
if ("remoteId" in termContext) {
return getRemotePtyData(termContext.remoteId);
}
return getPtyData(termContext.screenId, termContext.cmdId, termContext.lineNum);
}
function getPtyData(screenId : string, cmdId : string, lineNum : number) : Promise<PtyDataType> {
let url = sprintf(GlobalModel.getBaseHostPort() + "/api/ptyout?linenum=%d&screenid=%s&cmdid=%s", lineNum, screenId, cmdId);
return _getPtyDataFromUrl(url);
@ -3514,7 +3434,7 @@ if ((window as any).GlobalModel == null) {
GlobalModel = (window as any).GlobalModel;
GlobalCommandRunner = (window as any).GlobalCommandRunner;
export {Model, Session, ScreenLines, GlobalModel, GlobalCommandRunner, Cmd, Screen, riToRPtr, windowWidthToCols, windowHeightToRows, termWidthFromCols, termHeightFromRows, getPtyData, getRemotePtyData, TabColors, RemoteColors, getRendererType, getRendererContext};
export {Model, Session, ScreenLines, GlobalModel, GlobalCommandRunner, Cmd, Screen, riToRPtr, TabColors, RemoteColors, getRendererContext, getTermPtyData};
export type {LineContainerModel};

67
src/plugins.ts Normal file
View File

@ -0,0 +1,67 @@
import {RendererPluginType} from "./types";
import {SimpleImageRenderer} from "./imagerenderer";
import {SimpleMarkdownRenderer} from "./markdownrenderer";
import {isBlank} from "./util";
import {sprintf} from "sprintf-js";
const ImagePlugin : RendererPluginType = {
name: "image",
rendererType: "simple",
heightType: "pixels",
dataType: "blob",
collapseType: "hide",
globalCss: null,
mimeTypes: ["image/*"],
component: SimpleImageRenderer,
};
const MarkdownPlugin : RendererPluginType = {
name: "markdown",
rendererType: "simple",
heightType: "pixels",
dataType: "blob",
collapseType: "hide",
globalCss: null,
mimeTypes: ["text/markdown"],
component: SimpleMarkdownRenderer,
};
let AllPlugins = [ImagePlugin, MarkdownPlugin];
class PluginModelClass {
rendererPlugins : RendererPluginType[] = [];
registerRendererPlugin(plugin : RendererPluginType) {
if (isBlank(plugin.name)) {
throw new Error("invalid plugin, no name");
}
if (plugin.name == "terminal" || plugin.name == "none") {
throw new Error(sprintf("invalid plugin, name '%s' is reserved", plugin.name));
}
let existingPlugin = this.getRendererPluginByName(plugin.name);
if (existingPlugin != null) {
throw new Error(sprintf("plugin with name %s already registered", plugin.name));
}
this.rendererPlugins.push(plugin);
}
getRendererPluginByName(name : string) : RendererPluginType {
for (let i=0; i<this.rendererPlugins.length; i++) {
let plugin = this.rendererPlugins[i];
if (plugin.name == name) {
return plugin;
}
}
return null;
}
}
let PluginModel : PluginModelClass = null;
if ((window as any).PluginModel == null) {
PluginModel = new PluginModelClass();
PluginModel.registerRendererPlugin(ImagePlugin);
PluginModel.registerRendererPlugin(MarkdownPlugin);
(window as any).PluginModel = PluginModel;
}
export {PluginModel};

View File

@ -8,6 +8,7 @@ import cn from "classnames";
import {GlobalModel, GlobalCommandRunner, TabColors} from "./model";
import {Toggle} from "./elements";
import {LineType, RendererPluginType, ClientDataType} from "./types";
import {PluginModel} from "./plugins";
type OV<V> = mobx.IObservableValue<V>;
type OArr<V> = mobx.IObservableArray<V>;
@ -307,7 +308,7 @@ class LineSettingsModal extends React.Component<{line : LineType}, {}> {
renderRendererDropdown() : any {
let {line} = this.props;
let renderer = this.tempRenderer.get() ?? "terminal";
let plugins = GlobalModel.rendererPlugins;
let plugins = PluginModel.rendererPlugins;
let plugin : RendererPluginType = null;
return (
<div className={cn("dropdown", "renderer-dropdown", {"is-active": this.rendererDropdownActive.get()})}>

View File

@ -3185,8 +3185,16 @@ body.prompt-webshare #main {
.logo-text {
.mono-font(32px);
a {
color: @prompt-green;
}
}
.screen-name {
color: white;
.mono-font(24px);
margin-left: 20px;
}
.download-button {
margin-right: 20px;
@ -3218,4 +3226,9 @@ body.prompt-webshare #main {
#app {
color: white;
}
.lines .line-sep {
margin-left: 0px;
margin-right: 10px;
}
}

View File

@ -6,53 +6,14 @@ import {Terminal} from 'xterm';
import {Main} from "./main";
import {GlobalModel} from "./model";
import {v4 as uuidv4} from "uuid";
import {RendererPluginType} from "./types";
import {SimpleImageRenderer} from "./imagerenderer";
import {SimpleMarkdownRenderer} from "./markdownrenderer";
import {loadFonts} from "./util";
// @ts-ignore
let VERSION = __PROMPT_VERSION__;
// @ts-ignore
let BUILD = __PROMPT_BUILD__;
let jbmFontNormal = new FontFace("JetBrains Mono", "url('static/fonts/jetbrains-mono-v13-latin-regular.woff2')", {style: "normal", weight: "400"});
let jbmFont200 = new FontFace("JetBrains Mono", "url('static/fonts/jetbrains-mono-v13-latin-200.woff2')", {style: "normal", weight: "200"});
let jbmFont700 = new FontFace("JetBrains Mono", "url('static/fonts/jetbrains-mono-v13-latin-700.woff2')", {style: "normal", weight: "700"});
let faFont = new FontFace("FontAwesome", "url(static/fonts/fontawesome-webfont-4.7.woff2)", {style: "normal", weight: "normal"});
let docFonts : any = document.fonts; // work around ts typing issue
docFonts.add(jbmFontNormal);
docFonts.add(jbmFont200);
docFonts.add(jbmFont700);
docFonts.add(faFont);
jbmFontNormal.load();
jbmFont200.load();
jbmFont700.load();
faFont.load();
const ImagePlugin : RendererPluginType = {
name: "image",
rendererType: "simple",
heightType: "pixels",
dataType: "blob",
collapseType: "hide",
globalCss: null,
mimeTypes: ["image/*"],
component: SimpleImageRenderer,
};
const MarkdownPlugin : RendererPluginType = {
name: "markdown",
rendererType: "simple",
heightType: "pixels",
dataType: "blob",
collapseType: "hide",
globalCss: null,
mimeTypes: ["text/markdown"],
component: SimpleMarkdownRenderer,
};
GlobalModel.registerRendererPlugin(ImagePlugin);
GlobalModel.registerRendererPlugin(MarkdownPlugin);
loadFonts();
document.addEventListener("DOMContentLoaded", () => {
let reactElem = React.createElement(Main, null, null);

View File

@ -5,7 +5,7 @@ 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, LineType} from "./types";
import {GlobalModel, LineContainerModel, getPtyData, Cmd} from "./model";
import {LineContainerModel, getTermPtyData, Cmd} from "./model";
import {PtyDataBuffer} from "./ptydata";
import {debounce, throttle} from "throttle-debounce";
@ -72,7 +72,7 @@ class SimpleBlobRendererModel {
mobx.action(() => {
this.loading.set(true);
})();
let rtnp = getPtyData(this.context.screenId, this.context.cmdId, this.context.lineNum);
let rtnp = getTermPtyData(this.context);
rtnp.then((ptydata) => {
setTimeout(() => {
this.ptyData = ptydata;
@ -120,14 +120,14 @@ function apiAdapter(lcm : LineContainerModel, line : LineType, cmd : Cmd) : Rend
}
@mobxReact.observer
class SimpleBlobRenderer extends React.Component<{lcm : LineContainerModel, line : LineType, cmd : Cmd, plugin : RendererPluginType, onHeightChange : () => void}, {}> {
class SimpleBlobRenderer extends React.Component<{lcm : LineContainerModel, line : LineType, cmd : Cmd, rendererOpts : RendererOpts, plugin : RendererPluginType, onHeightChange : () => void}, {}> {
model : SimpleBlobRendererModel;
wrapperDivRef : React.RefObject<any> = React.createRef();
rszObs : ResizeObserver;
constructor(props : any) {
super(props);
let {lcm, line, cmd} = this.props;
let {lcm, line, cmd, rendererOpts} = this.props;
let context = contextFromLine(line);
let savedHeight = lcm.getContentHeight(context);
if (savedHeight == null) {
@ -142,12 +142,7 @@ class SimpleBlobRenderer extends React.Component<{lcm : LineContainerModel, line
context: context,
isDone: !cmd.isRunning(),
savedHeight: savedHeight,
opts: {
maxSize: lcm.getMaxContentSize(),
idealSize: lcm.getIdealContentSize(),
termOpts: cmd.getTermOpts(),
termFontSize: GlobalModel.termFontSize.get(),
},
opts: rendererOpts,
api: apiAdapter(lcm, line, cmd),
};
this.model = new SimpleBlobRendererModel();

View File

@ -3,9 +3,9 @@ import {Terminal} from 'xterm';
import {sprintf} from "sprintf-js";
import {boundMethod} from "autobind-decorator";
import {v4 as uuidv4} from "uuid";
import {GlobalModel, GlobalCommandRunner, termHeightFromRows, windowWidthToCols, windowHeightToRows, getPtyData, getRemotePtyData} from "./model";
import {termHeightFromRows, windowWidthToCols, windowHeightToRows} from "./textmeasure";
import {boundInt} from "./util";
import type {TermOptsType, TermWinSize, RendererContext, WindowSize, PtyDataType} from "./types";
import type {TermContextUnion, TermOptsType, TermWinSize, RendererContext, WindowSize, PtyDataType} from "./types";
type DataUpdate = {
data : Uint8Array,
@ -15,12 +15,8 @@ type DataUpdate = {
const MinTermCols = 10;
const MaxTermCols = 1024;
type RemoteTermContext = {remoteId : string};
type TermContext = RendererContext | RemoteTermContext;
type TermWrapOpts = {
termContext : TermContext,
termContext : TermContextUnion,
usedRows? : number,
termOpts : TermOptsType,
winSize : WindowSize,
@ -30,13 +26,14 @@ type TermWrapOpts = {
isRunning : boolean,
customKeyHandler? : (event : any, termWrap : TermWrap) => boolean,
fontSize : number,
noSetTUR? : boolean,
ptyDataSource : (termContext : TermContextUnion) => Promise<PtyDataType>,
onUpdateContentHeight : (termContext : RendererContext, height : number) => void,
};
// cmd-instance
class TermWrap {
terminal : any;
termContext : TermContext;
termContext : TermContextUnion;
atRowMax : boolean;
usedRows : mobx.IObservableValue<number>;
flexRows : boolean;
@ -51,7 +48,8 @@ class TermWrap {
focusHandler : (focus : boolean) => void;
isRunning : boolean;
fontSize : number;
noSetTUR : boolean;
onUpdateContentHeight : (termContext : RendererContext, height : number) => void;
ptyDataSource : (termContext : TermContextUnion) => Promise<PtyDataType>;
constructor(elem : Element, opts : TermWrapOpts) {
opts = opts ?? ({} as any);
@ -62,7 +60,8 @@ class TermWrap {
this.focusHandler = opts.focusHandler;
this.isRunning = opts.isRunning;
this.fontSize = opts.fontSize;
this.noSetTUR = !!opts.noSetTUR;
this.ptyDataSource = opts.ptyDataSource;
this.onUpdateContentHeight = opts.onUpdateContentHeight;
if (this.flexRows) {
this.atRowMax = false;
this.usedRows = mobx.observable.box(opts.usedRows ?? (opts.isRunning ? 1 : 0), {name: "term-usedrows"});
@ -221,8 +220,8 @@ class TermWrap {
return;
}
this.usedRows.set(tur);
if (!this.noSetTUR) {
GlobalModel.setContentHeight(termContext, tur);
if (this.onUpdateContentHeight != null) {
this.onUpdateContentHeight(termContext, tur);
}
})();
}
@ -272,14 +271,7 @@ class TermWrap {
}
this.reloading = true;
this.terminal.reset();
let rtnp : Promise<PtyDataType> = null;
if (this.getContextRemoteId() != null) {
rtnp = getRemotePtyData(this.getContextRemoteId());
}
else {
let termContext = this.getRendererContext();
rtnp = getPtyData(termContext.screenId, termContext.cmdId, termContext.lineNum);
}
let rtnp = this.ptyDataSource(this.termContext);
rtnp.then((ptydata) => {
setTimeout(() => {
this._reloadThenHandler(ptydata);

View File

@ -1,3 +1,8 @@
import {boundInt} from "./util";
const MinTermCols = 10;
const MaxTermCols = 1024;
let MonoFontSizes : {height : number, width : number}[] = [];
MonoFontSizes[8] = {height: 11, width: 4.797};
@ -41,4 +46,30 @@ function measureText(text : string, textOpts? : {pre? : boolean, mono? : boolean
return measureDiv.getBoundingClientRect()
}
export {measureText, getMonoFontSize};
function windowWidthToCols(width : number, fontSize : number) : number {
let dr = getMonoFontSize(fontSize);
let cols = Math.trunc((width - 50) / dr.width) - 1;
cols = boundInt(cols, MinTermCols, MaxTermCols);
return cols;
}
function windowHeightToRows(height : number, fontSize : number) : number {
let dr = getMonoFontSize(fontSize);
let rows = Math.floor((height - 80) / dr.height) - 1;
if (rows <= 0) {
rows = 1;
}
return rows;
}
function termWidthFromCols(cols : number, fontSize : number) : number {
let dr = getMonoFontSize(fontSize);
return Math.ceil(dr.width*cols) + 15;
}
function termHeightFromRows(rows : number, fontSize : number) : number {
let dr = getMonoFontSize(fontSize);
return Math.ceil(dr.height*rows);
}
export {measureText, getMonoFontSize, windowWidthToCols, windowHeightToRows, termWidthFromCols, termHeightFromRows};

View File

@ -355,6 +355,10 @@ type RendererContext = {
lineNum : number,
};
type RemoteTermContext = {remoteId : string};
type TermContextUnion = RendererContext | RemoteTermContext;
type RendererOpts = {
maxSize : WindowSize,
idealSize : WindowSize,
@ -482,4 +486,52 @@ type HistorySearchParams = {
type RenderModeType = "normal" | "collapsed";
export type {SessionDataType, LineType, RemoteType, RemoteStateType, RemoteInstanceType, HistoryItem, CmdRemoteStateType, FeCmdPacketType, TermOptsType, CmdStartPacketType, CmdDataType, ScreenDataType, ScreenOptsType, PtyDataUpdateType, ModelUpdateType, UpdateMessage, InfoType, CmdLineUpdateType, RemotePtrType, UIContextType, HistoryInfoType, HistoryQueryOpts, WatchScreenPacketType, TermWinSize, FeInputPacketType, RemoteInputPacketType, RemoteEditType, FeStateType, ContextMenuOpts, RendererContext, WindowSize, RendererModel, PtyDataType, BookmarkType, ClientDataType, PlaybookType, PlaybookEntryType, HistoryViewDataType, RenderModeType, AlertMessageType, HistorySearchParams, ScreenLinesType, FocusTypeStrs, HistoryTypeStrs, RendererOpts, RendererPluginType, SimpleBlobRendererComponent, RendererModelContainerApi, RendererModelInitializeParams, RendererOptsUpdate, ClientMigrationInfo, WebShareOpts, RemoteStatusTypeStrs};
type WebScreen = {
screenid : string,
sharename : string,
vts : number,
};
type WebLine = {
screenid : string,
lineid : string,
ts : number,
linenum : number,
linetype : string,
text : string,
contentheight : number,
renderer : string,
archived : boolean,
vts : number,
};
type WebRemote = {
alias : string,
canonicalname : string,
name : string,
};
type WebCmd = {
screeid : string,
lineid : string,
remote : WebRemote,
cmdstr : string,
rawcmdstr : string,
festate : FeStateType,
termopts : TermOptsType,
status : string,
startpk : CmdStartPacketType,
doneinfo : CmdDoneInfoType,
rtnstate : boolean,
rtnstatestr : string,
vts : number,
};
type WebFullScreen = {
screen : WebScreen,
lines : WebLine[],
cmds : WebCmd[],
vts : number,
}
export type {SessionDataType, LineType, RemoteType, RemoteStateType, RemoteInstanceType, HistoryItem, CmdRemoteStateType, FeCmdPacketType, TermOptsType, CmdStartPacketType, CmdDataType, ScreenDataType, ScreenOptsType, PtyDataUpdateType, ModelUpdateType, UpdateMessage, InfoType, CmdLineUpdateType, RemotePtrType, UIContextType, HistoryInfoType, HistoryQueryOpts, WatchScreenPacketType, TermWinSize, FeInputPacketType, RemoteInputPacketType, RemoteEditType, FeStateType, ContextMenuOpts, RendererContext, WindowSize, RendererModel, PtyDataType, BookmarkType, ClientDataType, PlaybookType, PlaybookEntryType, HistoryViewDataType, RenderModeType, AlertMessageType, HistorySearchParams, ScreenLinesType, FocusTypeStrs, HistoryTypeStrs, RendererOpts, RendererPluginType, SimpleBlobRendererComponent, RendererModelContainerApi, RendererModelInitializeParams, RendererOptsUpdate, ClientMigrationInfo, WebShareOpts, RemoteStatusTypeStrs, WebFullScreen, WebScreen, WebLine, WebCmd, RemoteTermContext, TermContextUnion};

View File

@ -1,5 +1,13 @@
import * as mobx from "mobx";
import {sprintf} from "sprintf-js";
import dayjs from "dayjs";
import localizedFormat from 'dayjs/plugin/localizedFormat';
dayjs.extend(localizedFormat)
function isBlank(s : string) : boolean {
return (s == null || s == "");
}
function handleNotOkResp(resp : any, url : URL) : Promise<any> {
let errMsg = sprintf("Bad status code response from fetch '%s': code=%d %s", url.toString(), resp.status, resp.statusText);
@ -249,4 +257,46 @@ function incObs(inum : mobx.IObservableValue<number>) {
})();
}
export {handleJsonFetchResponse, base64ToArray, genMergeData, genMergeDataMap, genMergeSimpleData, parseEnv0, boundInt, isModKeyPress, incObs};
function loadFonts() {
let jbmFontNormal = new FontFace("JetBrains Mono", "url('static/fonts/jetbrains-mono-v13-latin-regular.woff2')", {style: "normal", weight: "400"});
let jbmFont200 = new FontFace("JetBrains Mono", "url('static/fonts/jetbrains-mono-v13-latin-200.woff2')", {style: "normal", weight: "200"});
let jbmFont700 = new FontFace("JetBrains Mono", "url('static/fonts/jetbrains-mono-v13-latin-700.woff2')", {style: "normal", weight: "700"});
let faFont = new FontFace("FontAwesome", "url(static/fonts/fontawesome-webfont-4.7.woff2)", {style: "normal", weight: "normal"});
let docFonts : any = document.fonts; // work around ts typing issue
docFonts.add(jbmFontNormal);
docFonts.add(jbmFont200);
docFonts.add(jbmFont700);
docFonts.add(faFont);
jbmFontNormal.load();
jbmFont200.load();
jbmFont700.load();
faFont.load();
}
const DOW_STRS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
function getTodayStr() : string {
return getDateStr(new Date());
}
function getYesterdayStr() : string {
let d = new Date();
d.setDate(d.getDate()-1);
return getDateStr(d);
}
function getDateStr(d : Date) : string {
let yearStr = String(d.getFullYear());
let monthStr = String(d.getMonth()+1);
if (monthStr.length == 1) {
monthStr = "0" + monthStr;
}
let dayStr = String(d.getDate());
if (dayStr.length == 1) {
dayStr = "0" + dayStr;
}
let dowStr = DOW_STRS[d.getDay()];
return dowStr + " " + yearStr + "-" + monthStr + "-" + dayStr;
}
export {handleJsonFetchResponse, base64ToArray, genMergeData, genMergeDataMap, genMergeSimpleData, parseEnv0, boundInt, isModKeyPress, incObs, isBlank, loadFonts, getTodayStr, getYesterdayStr, getDateStr};

View File

@ -6,6 +6,282 @@ import {boundMethod} from "autobind-decorator";
import {If, For, When, Otherwise, Choose} from "tsx-control-statements/components";
import cn from "classnames";
import {WebShareModel} 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";
// TODO selection
// TODO remotevars
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 replaceHomePath(path : string, homeDir : string) : string {
if (path == homeDir) {
return "~";
}
if (path.startsWith(homeDir + "/")) {
return "~" + path.substr(homeDir.length);
}
return path;
}
function getCwdStr(state : FeStateType) : string {
if ((state == null || state.cwd == null) && remote != null) {
return "~";
}
let cwd = "?";
if (state && state.cwd) {
cwd = state.cwd;
}
// if (remote && remote.remotevars.home) {
// cwd = replaceHomePath(cwd, remote.remotevars.cwd)
// }
return cwd;
}
function getRemoteStr(remote : 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 : T.FeStateType}, {}> {
render() {
let {remote, festate} = this.props;
let remoteStr = getRemoteStr(remote);
let cwd = getCwdStr(festate);
let isRoot = false;
// if (remote && remote.remotevars) {
// if (remote.remotevars["sudo"] || remote.remotevars["bestuser"] == "root") {
// isRoot = true;
// }
// }
let remoteColorClass = (isRoot ? "color-red" : "color-green");
if (remote && remote.remoteopts && remote.remoteopts.color) {
remoteColorClass = "color-" + remote.remoteopts.color;
}
let remoteTitle : string = null;
if (remote && remote.canonicalname) {
remoteTitle = remote.canonicalname;
}
return (
<span className="term-prompt"><span title={remoteTitle} className={cn("term-prompt-remote", remoteColorClass)}>[{remoteStr}]</span> <span className="term-prompt-cwd">{cwd}</span> <span className="term-prompt-end">{isRoot ? "#" : "$"}</span></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}, {}> {
isCmdExpanded : OV<boolean> = mobx.observable.box(false, {name: "cmd-expanded"});
isOverflow : OV<boolean> = mobx.observable.box(false, {name: "line-overflow"});
renderSimple() {
<div className={cn("web-line line", (line.linetype == "cmd" ? "line-cmd" : "line-text"))}>
<LineAvatar line={line}/>
</div>
}
renderCmdText(cmd : Cmd, remote : 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">{getFullCmdText(cmd.cmdstr)}</div>
</div>
</React.Fragment>
);
}
let isMultiLine = lineutil.isMultiLineCmdText(cmd.cmdstr);
return (
<div key="meta2" className="meta meta-line2" ref={this.cmdTextRef}>
<div className="metapart-mono cmdtext">
<Prompt remote={cmd.remote} festate={cmd.festate}/>
<span> </span>
<span>{lineutil.getSingleLineCmdText(cmd.cmdstr)}</span>
</div>
<If condition={this.isOverflow.get() || isMultiLine}>
<div className="cmdtext-overflow" onClick={this.handleExpandCmd}>...&#x25BC;</div>
</If>
</div>
);
}
renderMetaWrap() {
let {line, cmd} = this.props;
let formattedTime = lineutil.getLineDateTimeStr(line.ts);
let termOpts = cmd.termopts;
let remote = cmd.remote;
let renderer = line.renderer;
return (
<div key="meta" className="meta-wrap">
<div key="meta1" className="meta meta-line1">
<div className="ts">{formattedTime}</div>
<div>&nbsp;</div>
<If condition={!isBlank(renderer) && renderer != "terminal"}>
<div className="renderer"><i className="fa-sharp fa-solid fa-fill"/>{renderer}&nbsp;</div>
</If>
<div className="termopts">
({termOpts.rows}x{termOpts.cols})
</div>
</div>
{this.renderCmdText(cmd, remote)}
</div>
);
}
render() {
let {line, cmd} = this.props;
let model = WebShareModel;
let isSelected = mobx.computed(() => (model.getSelectedLine() == line.linenum), {name: "computed-isSelected"}).get();
let rendererPlugin : 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);
return (
<div className={cn("web-line line line-cmd")}>
<div key="focus" className={cn("focus-indicator", {"selected active": isSelected})}/>
<div className="line-header">
<LineAvatar line={line} cmd={cmd}/>
{this.renderMetaWrap()}
</div>
</div>
);
}
}
@mobxReact.observer
class WebLineTextView extends React.Component<{line : T.WebLine, cmd : T.WebCmd}, {}> {
render() {
let {line} = this.props;
let isSelected = mobx.computed(() => (model.getSelectedLine() == line.linenum), {name: "computed-isSelected"}).get();
return (
<div className={cn("web-line line line-text")}>
<div key="focus" className={cn("focus-indicator", {"selected active": isSelected})}/>
<div className="line-header">
<LineAvatar line={line}/>
</div>
<div>
<div>{line.text}</div>
</div>
</div>
);
}
}
@mobxReact.observer
class WebLineView extends React.Component<{line : T.WebLine, cmd : T.WebCmd}, {}> {
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<{screen : T.WebFullScreen}, {}> {
render() {
let {screen} = this.props;
let lines = screen.lines ?? [];
let cmds = screen.cmds ?? [];
let cmdMap : Record<string, WebCmd> = {};
for (let i=0; i<cmds.length; i++) {
let cmd = cmds[i];
cmdMap[cmd.lineid] = cmd;
}
let lineElements : any[] = [];
let todayStr = util.getTodayStr();
let yesterdayStr = util.getYesterdayStr();
let prevDateStr : string = null;
for (let idx=0; idx<lines.length; idx++) {
let line = lines[idx];
let lineNumStr = String(line.linenum);
let dateSepStr = null;
let curDateStr = lineutil.getLineDateStr(todayStr, yesterdayStr, line.ts);
if (curDateStr != prevDateStr) {
dateSepStr = curDateStr;
}
prevDateStr = curDateStr;
if (dateSepStr != null) {
let sepElem = <div key={"sep-" + line.lineid} className="line-sep">{dateSepStr}</div>
lineElements.push(sepElem);
}
let topBorder = (dateSepStr == null) && (idx != 0);
let lineElem = <WebLineView key={line.lineid} line={line} cmd={cmdMap[line.lineid]} topBorder={topBorder}/>;
lineElements.push(lineElem);
}
return (
<div className="web-screen-view">
<div className="web-lines lines">
<div className="lines-spacer"></div>
{lineElements}
</div>
</div>
);
}
}
@mobxReact.observer
class WebShareMain extends React.Component<{}, {}> {
@ -14,10 +290,17 @@ class WebShareMain extends React.Component<{}, {}> {
}
render() {
let screen = WebShareModel.screen.get();
let errMessage = WebShareModel.errMessage.get();
return (
<div id="main">
<div className="logo-header">
<div className="logo-text">[prompt]</div>
<div className="logo-text">
<a target="_blank" href="https://www.getprompt.dev">[prompt]</a>
</div>
<If condition={screen != null}>
<div className="screen-name">{screen.screen.sharename}</div>
</If>
<div className="flex-spacer"/>
<a href="https://getprompt.dev/download/" target="_blank" className="download-button button is-link">
<span>Download Prompt</span>
@ -27,8 +310,12 @@ class WebShareMain extends React.Component<{}, {}> {
</a>
</div>
<div className="prompt-content">
<div>screenid={WebShareModel.screenId}, viewkey={WebShareModel.viewKey}</div>
<div>{WebShareModel.errMessage.get()}</div>
<If condition={screen != null}>
<WebScreenView screen={screen}/>
</If>
<If condition={errMessage != null}>
<div className="err-message">{WebShareModel.errMessage.get()}</div>
</If>
</div>
<div className="prompt-footer">
{this.renderCopy()}

View File

@ -1,6 +1,7 @@
import * as mobx from "mobx";
import {boundMethod} from "autobind-decorator";
import {handleJsonFetchResponse} from "./util";
import * as T from "./types";
type OV<V> = mobx.IObservableValue<V>;
type OArr<V> = mobx.IObservableArray<V>;
@ -19,6 +20,7 @@ class WebShareModelClass {
viewKey : string;
screenId : string;
errMessage : OV<string> = mobx.observable.box(null, {name: "errMessage"});
screen : OV<T.WebFullScreen> = mobx.observable.box(null, {name: "webScreen"});
constructor() {
let urlParams = new URLSearchParams(window.location.search);
@ -34,6 +36,10 @@ class WebShareModelClass {
})();
}
getSelectedLine() : number {
return 10;
}
loadFullScreenData() : void {
if (isBlank(this.screenId)) {
this.setErrMessage("No ScreenId Specified, Cannot Load.");
@ -46,7 +52,7 @@ class WebShareModelClass {
let usp = new URLSearchParams({screenid: this.screenId, viewkey: this.viewKey});
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) => {
console.log("got data", data);
mobx.action(() => this.screen.set(data))();
}).catch((err) => {
this.errMessage.set("Cannot get screen: " + err.message);
});

View File

@ -3,12 +3,24 @@ 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";
loadFonts();
document.addEventListener("DOMContentLoaded", () => {
let elem = document.getElementById("app");
let root = createRoot(elem);
let reactElem = React.createElement(WebShareMain, null, null);
let isFontLoaded = document.fonts.check("12px 'JetBrains Mono'");
if (isFontLoaded) {
root.render(reactElem);
}
else {
document.fonts.ready.then(() => {
root.render(reactElem);
});
}
});
(window as any).mobx = mobx;

Binary file not shown.