mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-22 16:48:23 +01:00
getting markdown/image simple renderers working
This commit is contained in:
parent
efb63a1466
commit
bc96a1bbed
@ -1,97 +0,0 @@
|
||||
import * as mobx from "mobx";
|
||||
import {WindowSize, RendererContext, TermOptsType} from "./types";
|
||||
import {getPtyData, termWidthFromCols, termHeightFromRows} from "./model";
|
||||
import {incObs} from "./util";
|
||||
import {PtyDataBuffer} from "./ptydata";
|
||||
|
||||
class ImageRendererModel {
|
||||
context : RendererContext;
|
||||
isDone : mobx.IObservableValue<boolean>;
|
||||
reloading : boolean = false;
|
||||
htmlImgDivElem : any;
|
||||
htmlImg : any;
|
||||
termOpts : TermOptsType;
|
||||
dataBuf : PtyDataBuffer;
|
||||
fontSize : number;
|
||||
|
||||
constructor(imgDivElem : any, context : RendererContext, termOpts : TermOptsType, isDone : boolean, fontSize : number) {
|
||||
this.dataBuf = new PtyDataBuffer();
|
||||
this.htmlImgDivElem = imgDivElem;
|
||||
this.termOpts = termOpts;
|
||||
this.context = context;
|
||||
this.isDone = mobx.observable.box(isDone, {name: "isDone"});
|
||||
this.fontSize = fontSize;
|
||||
this.reload(0);
|
||||
}
|
||||
|
||||
dispose() : void {
|
||||
this.dataBuf.reset();
|
||||
this.removeImage();
|
||||
}
|
||||
|
||||
removeImage() : void {
|
||||
this.htmlImg = null;
|
||||
this.htmlImgDivElem.replaceChildren();
|
||||
}
|
||||
|
||||
renderImage() : void {
|
||||
if (!this.isDone.get()) {
|
||||
return;
|
||||
}
|
||||
let blob = new Blob([this.dataBuf.getData()], {type: "image/jpeg"});
|
||||
this.htmlImg = new Image();
|
||||
this.htmlImg.src = URL.createObjectURL(blob);
|
||||
this.htmlImg.style.maxHeight = termHeightFromRows(this.termOpts.rows, this.fontSize) + "px";
|
||||
this.htmlImg.style.maxWidth = termWidthFromCols(this.termOpts.cols, this.fontSize) + "px";
|
||||
this.htmlImgDivElem.replaceChildren(this.htmlImg);
|
||||
}
|
||||
|
||||
reload(delayMs : number) : void {
|
||||
if (this.reloading) {
|
||||
return;
|
||||
}
|
||||
this.dataBuf.reset();
|
||||
this.reloading = true;
|
||||
let rtnp = getPtyData(this.context.sessionId, this.context.cmdId);
|
||||
rtnp.then((ptydata) => {
|
||||
setTimeout(() => {
|
||||
this.reloading = false;
|
||||
this.dataBuf.receiveData(ptydata.pos, ptydata.data, "reload");
|
||||
this.renderImage();
|
||||
}, delayMs);
|
||||
}).catch((e) => {
|
||||
this.dataBuf.brokenData = true;
|
||||
this.reloading = false;
|
||||
console.log("error reloading image data", e);
|
||||
});
|
||||
}
|
||||
|
||||
receiveData(pos : number, data : Uint8Array, reason? : string) : void {
|
||||
this.dataBuf.receiveData(pos, data, reason);
|
||||
}
|
||||
|
||||
cmdDone() : void {
|
||||
mobx.action(() => {
|
||||
this.isDone.set(true);
|
||||
})();
|
||||
}
|
||||
|
||||
resizeWindow(size : WindowSize) : void {
|
||||
return;
|
||||
}
|
||||
|
||||
resizeCols(cols : number) : void {
|
||||
return;
|
||||
}
|
||||
|
||||
giveFocus() : void {
|
||||
return;
|
||||
}
|
||||
|
||||
getUsedRows() : number {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
export {ImageRendererModel};
|
||||
|
56
src/imagerenderer.tsx
Normal file
56
src/imagerenderer.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import * as React from "react";
|
||||
import * as mobx from "mobx";
|
||||
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 {incObs} from "./util";
|
||||
import {PtyDataBuffer} from "./ptydata";
|
||||
|
||||
type OV<V> = mobx.IObservableValue<V>;
|
||||
type CV<V> = mobx.IComputedValue<V>;
|
||||
|
||||
// ctor(RendererContext, RenderOpts, isDone);
|
||||
// type RendererModel = {
|
||||
// dispose : () => void,
|
||||
// reload : (delayMs : number) => void,
|
||||
// receiveData : (pos : number, data : Uint8Array, reason? : string) => void,
|
||||
// cmdDone : () => void,
|
||||
// resizeWindow : (size : WindowSize) => void,
|
||||
// resizeCols : (cols : number) => void,
|
||||
// giveFocus : () => void,
|
||||
// getUsedRows : () => number,
|
||||
// };
|
||||
|
||||
// two types of renderers
|
||||
// JSON
|
||||
// blob
|
||||
//
|
||||
|
||||
@mobxReact.observer
|
||||
class SimpleImageRenderer extends React.Component<{data : Blob, context : RendererContext, opts : RendererOpts}, {}> {
|
||||
objUrl : string = null;
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.objUrl != null) {
|
||||
URL.revokeObjectURL(this.objUrl);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.objUrl == null) {
|
||||
let dataBlob = this.props.data;
|
||||
this.objUrl = URL.createObjectURL(dataBlob);
|
||||
}
|
||||
let opts = this.props.opts;
|
||||
return (
|
||||
<div className="simple-image-renderer">
|
||||
<img style={{maxHeight: opts.idealSize.height, maxWidth: opts.idealSize.width}} src={this.objUrl}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export {SimpleImageRenderer};
|
||||
|
@ -5,14 +5,14 @@ import {sprintf} from "sprintf-js";
|
||||
import {boundMethod} from "autobind-decorator";
|
||||
import dayjs from "dayjs";
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
import {ImageRendererModel} from "./imagerenderer";
|
||||
import {If, For, When, Otherwise, Choose} from "tsx-control-statements/components";
|
||||
import {GlobalModel, GlobalCommandRunner, Session, Cmd, ScreenLines, Screen, windowWidthToCols, windowHeightToRows, termHeightFromRows, termWidthFromCols} from "./model";
|
||||
import type {LineType, CmdDataType, FeStateType, RemoteType, RemotePtrType, RenderModeType} from "./types";
|
||||
import {GlobalModel, GlobalCommandRunner, Session, Cmd, ScreenLines, Screen, windowWidthToCols, windowHeightToRows, termHeightFromRows, termWidthFromCols, getRendererContext, getRendererType} from "./model";
|
||||
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";
|
||||
|
||||
dayjs.extend(localizedFormat)
|
||||
|
||||
@ -377,16 +377,9 @@ class LineCmd extends React.Component<{screen : LineContainerModel, line : LineT
|
||||
return (renderMode == "collapsed" && !overrideCollapsed.get());
|
||||
}
|
||||
|
||||
renderSimple() {
|
||||
getTerminalRendererHeight(cmd : Cmd) : number {
|
||||
let {screen, line, width, topBorder, renderMode} = this.props;
|
||||
let cmd = screen.getCmd(line);
|
||||
let isCollapsed = this.isCollapsed();
|
||||
let mainDivCn = cn(
|
||||
"line",
|
||||
"line-cmd",
|
||||
{"top-border": topBorder},
|
||||
{"collapsed": isCollapsed},
|
||||
);
|
||||
// header is 36px tall, padding+border = 6px
|
||||
// collapsed header is 24px tall + 6px
|
||||
// zero-terminal is 0px
|
||||
@ -395,11 +388,39 @@ class LineCmd extends React.Component<{screen : LineContainerModel, line : LineT
|
||||
// else: 53+(lines*lineheight)
|
||||
let height = (isCollapsed ? 30 : 42); // height of zero height terminal
|
||||
if (!isCollapsed) {
|
||||
let usedRows = screen.getUsedRows(line, cmd, width);
|
||||
let usedRows = screen.getUsedRows(getRendererContext(line), line, cmd, width);
|
||||
if (usedRows > 0) {
|
||||
height = 53 + termHeightFromRows(usedRows, GlobalModel.termFontSize.get());
|
||||
}
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
renderSimple() {
|
||||
let {screen, line, topBorder} = this.props;
|
||||
let cmd = screen.getCmd(line);
|
||||
let isCollapsed = this.isCollapsed();
|
||||
let height : number = 0;
|
||||
if (isBlank(line.renderer) || line.renderer == "terminal") {
|
||||
height = this.getTerminalRendererHeight(cmd);
|
||||
}
|
||||
else {
|
||||
let isCollapsed = this.isCollapsed();
|
||||
if (isCollapsed) {
|
||||
height = 24;
|
||||
}
|
||||
else {
|
||||
let {screen, line, width} = this.props;
|
||||
let usedRows = screen.getUsedRows(getRendererContext(line), line, cmd, width);
|
||||
height = 36 + usedRows;
|
||||
}
|
||||
}
|
||||
let mainDivCn = cn(
|
||||
"line",
|
||||
"line-cmd",
|
||||
{"top-border": topBorder},
|
||||
{"collapsed": isCollapsed},
|
||||
);
|
||||
return (
|
||||
<div className={mainDivCn} id={this.getLineDomId()} ref={this.lineRef} data-lineid={line.lineid} data-linenum={line.linenum} data-screenid={line.screenid} style={{height: height}}>
|
||||
<LineAvatar line={line} cmd={cmd}/>
|
||||
@ -413,11 +434,15 @@ class LineCmd extends React.Component<{screen : LineContainerModel, line : LineT
|
||||
let formattedTime = getLineDateTimeStr(line.ts);
|
||||
let termOpts = cmd.getTermOpts();
|
||||
let remote = model.getRemote(cmd.remoteId);
|
||||
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>
|
||||
@ -471,10 +496,11 @@ class LineCmd extends React.Component<{screen : LineContainerModel, line : LineT
|
||||
{"collapsed": isCollapsed},
|
||||
{"top-border": topBorder},
|
||||
);
|
||||
let RendererComponent : RendererComponentType = TerminalRenderer;
|
||||
if (line.renderer == "image") {
|
||||
RendererComponent = ImageRenderer;
|
||||
}
|
||||
let rendererPlugin : RendererPluginType = null;
|
||||
if (!isBlank(line.renderer) && line.renderer != "terminal") {
|
||||
rendererPlugin = GlobalModel.getRendererPluginByName(line.renderer);
|
||||
}
|
||||
let rendererType = getRendererType(line);
|
||||
return (
|
||||
<div className={mainDivCn} id={"line-" + getLineId(line)}
|
||||
ref={this.lineRef} onClick={this.handleClick}
|
||||
@ -501,7 +527,12 @@ class LineCmd extends React.Component<{screen : LineContainerModel, line : LineT
|
||||
</If>
|
||||
</div>
|
||||
</div>
|
||||
<RendererComponent screen={screen} line={line} width={width} staticRender={staticRender} visible={visible} onHeightChange={this.handleHeightChange} collapsed={isCollapsed}/>
|
||||
<If condition={rendererPlugin == null}>
|
||||
<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}/>
|
||||
</If>
|
||||
<If condition={!isCollapsed && cmd.getRtnState()}>
|
||||
<div key="rtnstate" className="cmd-rtnstate" style={{visibility: ((cmd.getStatus() == "done") ? "visible" : "hidden")}}>
|
||||
<If condition={rsdiff == null || rsdiff == ""}>
|
||||
@ -515,7 +546,7 @@ class LineCmd extends React.Component<{screen : LineContainerModel, line : LineT
|
||||
</If>
|
||||
</div>
|
||||
</If>
|
||||
<If condition={isSelected && !isFocused}>
|
||||
<If condition={isSelected && !isFocused && rendererType == "terminal"}>
|
||||
<div className="cmd-hints">
|
||||
<div className="hint-item color-nohover-white">focus line ({renderCmdText("L")})</div>
|
||||
</div>
|
||||
@ -542,13 +573,6 @@ class Line extends React.Component<{screen : LineContainerModel, line : LineType
|
||||
}
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class MarkdownRenderer extends React.Component<{s : Screen, line : LineType, width : number, staticRender : boolean, visible : OV<boolean>, onHeightChange : HeightChangeCallbackType}, {}> {
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class Prompt extends React.Component<{rptr : RemotePtrType, festate : FeStateType}, {}> {
|
||||
render() {
|
||||
@ -620,115 +644,6 @@ class LineText extends React.Component<{screen : LineContainerModel, line : Line
|
||||
}
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class ImageRenderer extends React.Component<{screen : LineContainerModel, line : LineType, width : number, staticRender : boolean, visible : OV<boolean>, onHeightChange : () => void, collapsed : boolean}, {}> {
|
||||
elemRef : React.RefObject<any> = React.createRef();
|
||||
imageDivRef : React.RefObject<any> = React.createRef();
|
||||
imageLoaded : mobx.IObservableValue<boolean> = mobx.observable.box(false, {name: "imageLoaded"});
|
||||
imageModel : ImageRendererModel;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.componentDidUpdate(null, null, null);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.imageLoaded.get()) {
|
||||
this.unloadImage(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("image-render height change: ", line.linenum, snapshot.height, "=>", curHeight);
|
||||
}
|
||||
this.checkLoad();
|
||||
}
|
||||
|
||||
checkLoad() : void {
|
||||
let {line, staticRender, visible, collapsed} = this.props;
|
||||
if (staticRender) {
|
||||
return;
|
||||
}
|
||||
let vis = visible && visible.get() && !collapsed;
|
||||
let curVis = this.imageLoaded.get();
|
||||
if (vis && !curVis) {
|
||||
this.loadImage();
|
||||
}
|
||||
else if (!vis && curVis) {
|
||||
this.unloadImage(false);
|
||||
}
|
||||
}
|
||||
|
||||
loadImage() : void {
|
||||
let {screen, line} = this.props;
|
||||
let model = GlobalModel;
|
||||
let cmd = screen.getCmd(line);
|
||||
if (cmd == null) {
|
||||
return;
|
||||
}
|
||||
let elem = this.imageDivRef.current;
|
||||
if (elem == null) {
|
||||
console.log("cannot load image, no elem found");
|
||||
return;
|
||||
}
|
||||
this.imageModel = screen.loadImageRenderer(this.imageDivRef.current, line, cmd);
|
||||
mobx.action(() => this.imageLoaded.set(true))();
|
||||
}
|
||||
|
||||
unloadImage(unmount : boolean) : void {
|
||||
let {screen, line} = this.props;
|
||||
screen.unloadRenderer(line.cmdid);
|
||||
this.imageModel = null;
|
||||
if (!unmount) {
|
||||
mobx.action(() => this.imageLoaded.set(false))();
|
||||
if (this.imageDivRef.current != null) {
|
||||
this.imageDivRef.current.replaceChildren();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let imageModel = this.imageModel;
|
||||
let isLoaded = this.imageLoaded.get();
|
||||
let isDone = (imageModel != null && imageModel.isDone.get());
|
||||
if (imageModel != null) {
|
||||
let dataVersion = imageModel.dataBuf.dataVersion.get();
|
||||
}
|
||||
let collapsed = this.props.collapsed;
|
||||
return (
|
||||
<div ref={this.elemRef} className={cn("image-wrapper", {"collapsed": collapsed})}>
|
||||
<div key="imagediv" ref={this.imageDivRef}></div>
|
||||
<If condition={!isDone}><div className="loading-div">...</div></If>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class TerminalRenderer extends React.Component<{screen : LineContainerModel, line : LineType, width : number, staticRender : boolean, visible : OV<boolean>, onHeightChange : () => void, collapsed : boolean}, {}> {
|
||||
termLoaded : mobx.IObservableValue<boolean> = mobx.observable.box(false, {name: "linecmd-term-loaded"});
|
||||
@ -840,7 +755,7 @@ class TerminalRenderer extends React.Component<{screen : LineContainerModel, lin
|
||||
return isPhysicalFocused && (screenFocusType == "cmd" || screenFocusType == "cmd-fg")
|
||||
}, {name: "computed-isFocused"}).get();
|
||||
let cmd = screen.getCmd(line); // will not be null
|
||||
let usedRows = screen.getUsedRows(line, cmd, width);
|
||||
let usedRows = screen.getUsedRows(getRendererContext(line), line, cmd, width);
|
||||
let termHeight = termHeightFromRows(usedRows, GlobalModel.termFontSize.get());
|
||||
let termLoaded = this.termLoaded.get();
|
||||
return (
|
||||
|
11
src/main.tsx
11
src/main.tsx
@ -1990,16 +1990,13 @@ class ScreenWindowView extends React.Component<{screen : Screen}, {}> {
|
||||
if (screen == null) {
|
||||
return;
|
||||
}
|
||||
if (width == null || height == null || width == 0 || height == 0) {
|
||||
return;
|
||||
}
|
||||
mobx.action(() => {
|
||||
this.width.set(width);
|
||||
this.height.set(height);
|
||||
let cols = windowWidthToCols(width, GlobalModel.termFontSize.get());
|
||||
let rows = windowHeightToRows(height, GlobalModel.termFontSize.get());
|
||||
if (cols == 0 || rows == 0) {
|
||||
console.log("cannot set screen size", rows, cols);
|
||||
return;
|
||||
}
|
||||
screen.termSizeCallback(rows, cols);
|
||||
screen.screenSizeCallback({height: height, width: width});
|
||||
})();
|
||||
}
|
||||
|
||||
|
64
src/markdownrenderer.tsx
Normal file
64
src/markdownrenderer.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import * as React from "react";
|
||||
import * as mobx from "mobx";
|
||||
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 ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
|
||||
type OV<V> = mobx.IObservableValue<V>;
|
||||
|
||||
function LinkRenderer(props : any) : any {
|
||||
let newUrl = "https://extern?" + encodeURIComponent(props.href);
|
||||
return <a href={newUrl} target="_blank">{props.children}</a>
|
||||
}
|
||||
|
||||
function HeaderRenderer(props : any, hnum : number) : any {
|
||||
return (
|
||||
<div className={cn("title", "is-" + hnum)}>{props.children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CodeRenderer(props : any) : any {
|
||||
return (
|
||||
<code className={cn({"inline": props.inline})}>{props.children}</code>
|
||||
);
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class SimpleMarkdownRenderer extends React.Component<{data : Blob, context : RendererContext, opts : RendererOpts}, {}> {
|
||||
markdownText : OV<string> = mobx.observable.box(null, {name: "markdownText"});
|
||||
|
||||
componentDidMount() {
|
||||
let prtn = this.props.data.text()
|
||||
prtn.then((text) => {
|
||||
mobx.action(() => {
|
||||
this.markdownText.set(text);
|
||||
})();
|
||||
});
|
||||
}
|
||||
render() {
|
||||
if (this.markdownText.get() == null) {
|
||||
return null;
|
||||
}
|
||||
let markdownComponents = {
|
||||
a: LinkRenderer,
|
||||
h1: (props) => HeaderRenderer(props, 1),
|
||||
h2: (props) => HeaderRenderer(props, 2),
|
||||
h3: (props) => HeaderRenderer(props, 3),
|
||||
h4: (props) => HeaderRenderer(props, 4),
|
||||
h5: (props) => HeaderRenderer(props, 5),
|
||||
h6: (props) => HeaderRenderer(props, 6),
|
||||
code: CodeRenderer,
|
||||
};
|
||||
let markdownText = this.markdownText.get();
|
||||
return (
|
||||
<div className="markdown-renderer markdown content">
|
||||
<ReactMarkdown children={this.markdownText.get()} remarkPlugins={[remarkGfm]} components={markdownComponents}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export {SimpleMarkdownRenderer};
|
250
src/model.ts
250
src/model.ts
@ -5,9 +5,8 @@ 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} 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} from "./types";
|
||||
import {WSControl} from "./ws";
|
||||
import {ImageRendererModel} from "./imagerenderer";
|
||||
import {measureText, getMonoFontSize} from "./textmeasure";
|
||||
import dayjs from "dayjs";
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
@ -39,23 +38,45 @@ const BUILD = __PROMPT_BUILD__;
|
||||
|
||||
type LineContainerModel = {
|
||||
loadTerminalRenderer : (elem : Element, line : LineType, cmd : Cmd, width : number) => void,
|
||||
loadImageRenderer : (imageDivElem : any, line : LineType, cmd : Cmd) => ImageRendererModel,
|
||||
registerRenderer : (cmdId : string, renderer : RendererModel) => void,
|
||||
unloadRenderer : (cmdId : string) => void,
|
||||
getUsedRows : (line : LineType, cmd : Cmd, width : number) => number,
|
||||
getIsFocused : (lineNum : number) => boolean,
|
||||
getTermWrap : (cmdId : string) => TermWrap;
|
||||
getRenderer : (cmdId : string) => RendererModel,
|
||||
getFocusType : () => "input" | "cmd" | "cmd-fg",
|
||||
getSelectedLine : () => number,
|
||||
getCmd : (line : LineType) => Cmd,
|
||||
setTermFocus : (lineNum : number, focus : boolean) => void,
|
||||
getUsedRows : (context : RendererContext, line : LineType, cmd : Cmd, width : number) => number,
|
||||
getContentHeight : (context : RendererContext) => number,
|
||||
setContentHeight : (context : RendererContext, height : number) => void,
|
||||
getMaxContentSize() : WindowSize,
|
||||
getIdealContentSize() : WindowSize,
|
||||
}
|
||||
|
||||
|
||||
type SWLinePtr = {
|
||||
line : LineType,
|
||||
slines : ScreenLines,
|
||||
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 {
|
||||
sessionId: line.sessionid,
|
||||
screenId: line.screenid,
|
||||
cmdId: line.cmdid,
|
||||
lineId: line.lineid,
|
||||
lineNum: line.linenum,
|
||||
};
|
||||
}
|
||||
|
||||
function windowWidthToCols(width : number, fontSize : number) : number {
|
||||
let dr = getMonoFontSize(fontSize);
|
||||
let cols = Math.trunc((width - 50) / dr.width) - 1;
|
||||
@ -260,6 +281,17 @@ class Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
handleDataFromRenderer(data : string, renderer : RendererModel) : void {
|
||||
// console.log("handle data", {data: data});
|
||||
if (!this.isRunning()) {
|
||||
return;
|
||||
}
|
||||
for (let pos=0; pos<data.length; pos += InputChunkSize) {
|
||||
let dataChunk = data.slice(pos, pos+InputChunkSize);
|
||||
this.handleInputChunk(dataChunk);
|
||||
}
|
||||
}
|
||||
|
||||
handleInputChunk(data : string) : void {
|
||||
let inputPacket : FeInputPacketType = {
|
||||
type: "feinput",
|
||||
@ -279,6 +311,7 @@ class Screen {
|
||||
name : OV<string>;
|
||||
archived : OV<boolean>;
|
||||
curRemote : OV<RemotePtrType>;
|
||||
lastScreenSize : WindowSize;
|
||||
lastCols : number;
|
||||
lastRows : number;
|
||||
selectedLine : OV<number>;
|
||||
@ -287,7 +320,8 @@ class Screen {
|
||||
anchorOffset : number = 0;
|
||||
termLineNumFocus : OV<number>;
|
||||
setAnchor_debounced : (anchorLine : number, anchorOffset : number) => void;
|
||||
renderers : Record<string, RendererModel> = {}; // cmdid => TermWrap
|
||||
terminals : Record<string, TermWrap> = {}; // cmdid => TermWrap
|
||||
renderers : Record<string, RendererModel> = {}; // cmdid => RendererModel
|
||||
|
||||
constructor(sdata : ScreenDataType) {
|
||||
this.sessionId = sdata.sessionid;
|
||||
@ -327,6 +361,14 @@ class Screen {
|
||||
})();
|
||||
}
|
||||
|
||||
getContentHeight(context : RendererContext) : number {
|
||||
return GlobalModel.getContentHeight(context);
|
||||
}
|
||||
|
||||
setContentHeight(context : RendererContext, height : number) : void {
|
||||
GlobalModel.setContentHeight(context, height);
|
||||
}
|
||||
|
||||
getCmd(line : LineType) : Cmd {
|
||||
return GlobalModel.getCmd(line);
|
||||
}
|
||||
@ -483,11 +525,15 @@ class Screen {
|
||||
updatePtyData(ptyMsg : PtyDataUpdateType) {
|
||||
let cmdId = ptyMsg.cmdid;
|
||||
let renderer = this.renderers[cmdId];
|
||||
if (renderer == null) {
|
||||
return;
|
||||
if (renderer != null) {
|
||||
let data = base64ToArray(ptyMsg.ptydata64);
|
||||
renderer.receiveData(ptyMsg.ptypos, data, "from-sw");
|
||||
}
|
||||
let term = this.terminals[cmdId];
|
||||
if (term != null) {
|
||||
let data = base64ToArray(ptyMsg.ptydata64);
|
||||
term.receiveData(ptyMsg.ptypos, data, "from-sw");
|
||||
}
|
||||
let data = base64ToArray(ptyMsg.ptydata64);
|
||||
renderer.receiveData(ptyMsg.ptypos, data, "from-sw");
|
||||
}
|
||||
|
||||
isActive() : boolean {
|
||||
@ -498,7 +544,44 @@ class Screen {
|
||||
return (this.sessionId == activeScreen.sessionId) && (this.screenId == activeScreen.screenId);
|
||||
}
|
||||
|
||||
termSizeCallback(rows : number, cols : number) : void {
|
||||
screenSizeCallback(winSize : WindowSize) : void {
|
||||
if (winSize.height == 0 || winSize.width == 0) {
|
||||
return;
|
||||
}
|
||||
if (this.lastScreenSize != null && this.lastScreenSize.height == winSize.height && this.lastScreenSize.width == winSize.width) {
|
||||
return;
|
||||
}
|
||||
this.lastScreenSize = winSize;
|
||||
let cols = windowWidthToCols(winSize.width, GlobalModel.termFontSize.get());
|
||||
let rows = windowHeightToRows(winSize.height, GlobalModel.termFontSize.get());
|
||||
this._termSizeCallback(rows, cols);
|
||||
}
|
||||
|
||||
getMaxContentSize() : WindowSize {
|
||||
if (this.lastScreenSize == null) {
|
||||
let width = termWidthFromCols(80, GlobalModel.termFontSize.get());
|
||||
let height = termHeightFromRows(25, GlobalModel.termFontSize.get());
|
||||
return {width, height};
|
||||
}
|
||||
let winSize = this.lastScreenSize;
|
||||
let width = boundInt(winSize.width-50, 100, 5000);
|
||||
let height = boundInt(winSize.height-100, 100, 5000);
|
||||
return {width, height};
|
||||
}
|
||||
|
||||
getIdealContentSize() : WindowSize {
|
||||
if (this.lastScreenSize == null) {
|
||||
let width = termWidthFromCols(80, GlobalModel.termFontSize.get());
|
||||
let height = termHeightFromRows(25, GlobalModel.termFontSize.get());
|
||||
return {width, height};
|
||||
}
|
||||
let winSize = this.lastScreenSize;
|
||||
let width = boundInt(Math.ceil((winSize.width-50)*0.7), 100, 5000);
|
||||
let height = boundInt(Math.ceil((winSize.height-100)*0.5), 100, 5000);
|
||||
return {width, height};
|
||||
}
|
||||
|
||||
_termSizeCallback(rows : number, cols : number) : void {
|
||||
if (cols == 0 || rows == 0) {
|
||||
return;
|
||||
}
|
||||
@ -507,16 +590,24 @@ class Screen {
|
||||
}
|
||||
this.lastRows = rows;
|
||||
this.lastCols = cols;
|
||||
for (let cmdid in this.renderers) {
|
||||
this.renderers[cmdid].resizeCols(cols);
|
||||
for (let cmdid in this.terminals) {
|
||||
this.terminals[cmdid].resizeCols(cols);
|
||||
}
|
||||
GlobalCommandRunner.resizeScreen(this.screenId, rows, cols);
|
||||
}
|
||||
|
||||
getTermWrap(cmdId : string) : TermWrap {
|
||||
return this.terminals[cmdId];
|
||||
}
|
||||
|
||||
getRenderer(cmdId : string) : RendererModel {
|
||||
return this.renderers[cmdId];
|
||||
}
|
||||
|
||||
registerRenderer(cmdId : string, renderer : RendererModel) {
|
||||
this.renderers[cmdId] = renderer;
|
||||
}
|
||||
|
||||
setTermFocus(lineNum : number, focus : boolean) : void {
|
||||
// console.log("SW setTermFocus", lineNum, focus);
|
||||
mobx.action(() => this.termLineNumFocus.set(focus ? lineNum : 0))();
|
||||
@ -572,23 +663,15 @@ class Screen {
|
||||
return false;
|
||||
}
|
||||
|
||||
loadImageRenderer(imageDivElem : any, line : LineType, cmd : Cmd) : ImageRendererModel {
|
||||
let cmdId = cmd.cmdId;
|
||||
let context = {sessionId: this.sessionId, screenId: this.screenId, cmdId: cmdId, lineId : line.lineid, lineNum: line.linenum};
|
||||
let imageModel = new ImageRendererModel(imageDivElem, context, cmd.getTermOpts(), !cmd.isRunning(), GlobalModel.termFontSize.get());
|
||||
this.renderers[cmdId] = imageModel;
|
||||
return imageModel;
|
||||
}
|
||||
|
||||
loadTerminalRenderer(elem : Element, line : LineType, cmd : Cmd, width : number) {
|
||||
let cmdId = cmd.cmdId;
|
||||
let termWrap = this.getRenderer(cmdId);
|
||||
let termWrap = this.getTermWrap(cmdId);
|
||||
if (termWrap != null) {
|
||||
console.log("term-wrap already exists for", this.screenId, cmdId);
|
||||
return;
|
||||
}
|
||||
let cols = windowWidthToCols(width, GlobalModel.termFontSize.get());
|
||||
let usedRows = GlobalModel.getTUR(this.sessionId, cmdId, cols);
|
||||
let usedRows = GlobalModel.getContentHeight(getRendererContext(line));
|
||||
if (line.contentheight != null && line.contentheight != -1) {
|
||||
usedRows = line.contentheight;
|
||||
}
|
||||
@ -604,7 +687,7 @@ class Screen {
|
||||
customKeyHandler: this.termCustomKeyHandler.bind(this),
|
||||
fontSize: GlobalModel.termFontSize.get(),
|
||||
});
|
||||
this.renderers[cmdId] = termWrap;
|
||||
this.terminals[cmdId] = termWrap;
|
||||
if ((this.focusType.get() == "cmd" || this.focusType.get() == "cmd-fg") && this.selectedLine.get() == line.linenum) {
|
||||
termWrap.giveFocus();
|
||||
}
|
||||
@ -617,9 +700,14 @@ class Screen {
|
||||
rmodel.dispose();
|
||||
delete this.renderers[cmdId];
|
||||
}
|
||||
let term = this.terminals[cmdId];
|
||||
if (term != null) {
|
||||
term.dispose();
|
||||
delete this.terminals[cmdId];
|
||||
}
|
||||
}
|
||||
|
||||
getUsedRows(line : LineType, cmd : Cmd, width : number) : number {
|
||||
getUsedRows(context : RendererContext, line : LineType, cmd : Cmd, width : number) : number {
|
||||
if (cmd == null) {
|
||||
return 0;
|
||||
}
|
||||
@ -627,10 +715,10 @@ class Screen {
|
||||
if (!termOpts.flexrows) {
|
||||
return termOpts.rows;
|
||||
}
|
||||
let termWrap = this.getRenderer(cmd.cmdId);
|
||||
let termWrap = this.getTermWrap(cmd.cmdId);
|
||||
if (termWrap == null) {
|
||||
let cols = windowWidthToCols(width, GlobalModel.termFontSize.get());
|
||||
let usedRows = GlobalModel.getTUR(this.sessionId, cmd.cmdId, cols);
|
||||
let usedRows = GlobalModel.getContentHeight(context);
|
||||
if (usedRows != null) {
|
||||
return usedRows;
|
||||
}
|
||||
@ -1601,6 +1689,7 @@ type LineFocusType = {
|
||||
|
||||
class SpecialHistoryViewLineContainer {
|
||||
historyItem : HistoryItem;
|
||||
terminal : TermWrap;
|
||||
renderer : RendererModel;
|
||||
cmd : Cmd;
|
||||
|
||||
@ -1614,17 +1703,37 @@ class SpecialHistoryViewLineContainer {
|
||||
}
|
||||
return this.cmd;
|
||||
}
|
||||
|
||||
setTermFocus(lineNum : number, focus : boolean) : void {
|
||||
return;
|
||||
}
|
||||
|
||||
setContentHeight(context : RendererContext, height : number) : void {
|
||||
return;
|
||||
}
|
||||
|
||||
getMaxContentSize() : WindowSize {
|
||||
let width = termWidthFromCols(80, GlobalModel.termFontSize.get());
|
||||
let height = termHeightFromRows(25, GlobalModel.termFontSize.get());
|
||||
return {width, height};
|
||||
}
|
||||
|
||||
getIdealContentSize() : WindowSize {
|
||||
let width = termWidthFromCols(80, GlobalModel.termFontSize.get());
|
||||
let height = termHeightFromRows(25, GlobalModel.termFontSize.get());
|
||||
return {width, height};
|
||||
}
|
||||
|
||||
loadTerminalRenderer(elem : Element, line : LineType, cmd : Cmd, width : number) : void {
|
||||
this.unloadRenderer(null);
|
||||
let cmdId = cmd.cmdId;
|
||||
let termWrap = this.getRenderer(cmdId);
|
||||
let termWrap = this.getTermWrap(cmdId);
|
||||
if (termWrap != null) {
|
||||
console.log("term-wrap already exists for", line.screenid, cmdId);
|
||||
return;
|
||||
}
|
||||
let cols = windowWidthToCols(width, GlobalModel.termFontSize.get());
|
||||
let usedRows = GlobalModel.getTUR(line.sessionid, cmdId, cols);
|
||||
let usedRows = GlobalModel.getContentHeight(getRendererContext(line));
|
||||
if (line.contentheight != null && line.contentheight != -1) {
|
||||
usedRows = line.contentheight;
|
||||
}
|
||||
@ -1641,23 +1750,12 @@ class SpecialHistoryViewLineContainer {
|
||||
fontSize: GlobalModel.termFontSize.get(),
|
||||
noSetTUR: true,
|
||||
});
|
||||
this.renderer = termWrap;
|
||||
this.terminal = termWrap;
|
||||
return;
|
||||
}
|
||||
|
||||
loadImageRenderer(imageDivElem : any, line : LineType, cmd : Cmd) : ImageRendererModel {
|
||||
this.unloadRenderer(null);
|
||||
let cmdId = cmd.cmdId;
|
||||
let context = {
|
||||
sessionId: this.historyItem.sessionid,
|
||||
screenId: this.historyItem.screenid,
|
||||
cmdId: cmdId,
|
||||
lineId : line.lineid,
|
||||
lineNum: line.linenum
|
||||
};
|
||||
let imageModel = new ImageRendererModel(imageDivElem, context, cmd.getTermOpts(), !cmd.isRunning(), GlobalModel.termFontSize.get());
|
||||
this.renderer = imageModel;
|
||||
return imageModel;
|
||||
|
||||
registerRenderer(cmdId : string, renderer : RendererModel) : void {
|
||||
this.renderer = renderer;
|
||||
}
|
||||
|
||||
unloadRenderer(cmdId : string) : void {
|
||||
@ -1665,9 +1763,17 @@ class SpecialHistoryViewLineContainer {
|
||||
this.renderer.dispose();
|
||||
this.renderer = null;
|
||||
}
|
||||
if (this.terminal != null) {
|
||||
this.terminal.dispose();
|
||||
this.terminal = null;
|
||||
}
|
||||
}
|
||||
|
||||
getContentHeight(context : RendererContext) : number {
|
||||
return GlobalModel.getContentHeight(context);
|
||||
}
|
||||
|
||||
getUsedRows(line : LineType, cmd : Cmd, width : number) : number {
|
||||
getUsedRows(context : RendererContext, line : LineType, cmd : Cmd, width : number) : number {
|
||||
if (cmd == null) {
|
||||
return 0;
|
||||
}
|
||||
@ -1675,10 +1781,10 @@ class SpecialHistoryViewLineContainer {
|
||||
if (!termOpts.flexrows) {
|
||||
return termOpts.rows;
|
||||
}
|
||||
let termWrap = this.getRenderer(cmd.cmdId);
|
||||
let termWrap = this.getTermWrap(cmd.cmdId);
|
||||
if (termWrap == null) {
|
||||
let cols = windowWidthToCols(width, GlobalModel.termFontSize.get());
|
||||
let usedRows = GlobalModel.getTUR(this.historyItem.sessionid, cmd.cmdId, cols);
|
||||
let usedRows = GlobalModel.getContentHeight(context);
|
||||
if (usedRows != null) {
|
||||
return usedRows;
|
||||
}
|
||||
@ -1697,6 +1803,10 @@ class SpecialHistoryViewLineContainer {
|
||||
getRenderer(cmdId : string) : RendererModel {
|
||||
return this.renderer;
|
||||
}
|
||||
|
||||
getTermWrap(cmdId : string) : TermWrap {
|
||||
return this.terminal;
|
||||
}
|
||||
|
||||
getFocusType() : "input" | "cmd" | "cmd-fg" {
|
||||
return "input";
|
||||
@ -2246,6 +2356,7 @@ class Model {
|
||||
welcomeModalOpen : OV<boolean> = mobx.observable.box(false, {name: "welcomeModalOpen"});
|
||||
screenSettingsModal : OV<{sessionId : string, screenId : string}> = mobx.observable.box(null, {name: "screenSettingsModal"});
|
||||
sessionSettingsModal : OV<string> = mobx.observable.box(null, {name: "sessionSettingsModal"});
|
||||
rendererPlugins : RendererPluginType[] = [];
|
||||
|
||||
inputModel : InputModel;
|
||||
bookmarksModel : BookmarksModel;
|
||||
@ -2293,6 +2404,27 @@ class Model {
|
||||
setTimeout(() => this.getClientData(), 10);
|
||||
}
|
||||
|
||||
registerRendererPlugin(plugin : RendererPluginType) {
|
||||
if (isBlank(plugin.name)) {
|
||||
throw new Error("invalid plugin, no 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;
|
||||
}
|
||||
|
||||
showAlert(alertMessage : AlertMessageType) : Promise<boolean> {
|
||||
mobx.action(() => {
|
||||
this.alertMessage.set(alertMessage);
|
||||
@ -2415,15 +2547,15 @@ class Model {
|
||||
})();
|
||||
}
|
||||
|
||||
getTUR(sessionId : string, cmdId : string, cols : number) : number {
|
||||
let key = sessionId + "/" + cmdId + "/" + cols;
|
||||
getContentHeight(context : RendererContext) : number {
|
||||
let key = context.sessionId + "/" + context.cmdId;
|
||||
return this.termUsedRowsCache[key];
|
||||
}
|
||||
|
||||
setTUR(termContext : RendererContext, size : TermWinSize, usedRows : number) : void {
|
||||
let key = termContext.sessionId + "/" + termContext.cmdId + "/" + size.cols;
|
||||
this.termUsedRowsCache[key] = usedRows;
|
||||
GlobalCommandRunner.setTermUsedRows(termContext, usedRows);
|
||||
setContentHeight(context : RendererContext, height : number) : void {
|
||||
let key = context.sessionId + "/" + context.cmdId;
|
||||
this.termUsedRowsCache[key] = height;
|
||||
GlobalCommandRunner.setTermUsedRows(context, height);
|
||||
}
|
||||
|
||||
contextScreen(e : any, screenId : string) {
|
||||
@ -2504,7 +2636,11 @@ class Model {
|
||||
let screen = ptr.screen;
|
||||
let renderer = screen.getRenderer(cmdId);
|
||||
if (renderer != null) {
|
||||
renderer.cmdDone();
|
||||
renderer.setIsDone();
|
||||
}
|
||||
let term = screen.getTermWrap(cmdId);
|
||||
if (term != null) {
|
||||
term.cmdDone();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3284,12 +3420,12 @@ function _getPtyDataFromUrl(url : string) : Promise<PtyDataType> {
|
||||
});
|
||||
}
|
||||
|
||||
function getPtyData(sessionId : string, cmdId : string) {
|
||||
function getPtyData(sessionId : string, cmdId : string) : Promise<PtyDataType> {
|
||||
let url = sprintf(GlobalModel.getBaseHostPort() + "/api/ptyout?sessionid=%s&cmdid=%s", sessionId, cmdId);
|
||||
return _getPtyDataFromUrl(url);
|
||||
}
|
||||
|
||||
function getRemotePtyData(remoteId : string) {
|
||||
function getRemotePtyData(remoteId : string) : Promise<PtyDataType> {
|
||||
let url = sprintf(GlobalModel.getBaseHostPort() + "/api/remote-pty?remoteid=%s", remoteId);
|
||||
return _getPtyDataFromUrl(url);
|
||||
}
|
||||
@ -3304,7 +3440,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};
|
||||
export {Model, Session, ScreenLines, GlobalModel, GlobalCommandRunner, Cmd, Screen, riToRPtr, windowWidthToCols, windowHeightToRows, termWidthFromCols, termHeightFromRows, getPtyData, getRemotePtyData, TabColors, RemoteColors, getRendererType, getRendererContext};
|
||||
export type {LineContainerModel};
|
||||
|
||||
|
||||
|
140
src/sh2.less
140
src/sh2.less
@ -764,54 +764,6 @@ body::-webkit-scrollbar {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown {
|
||||
color: @term-white;
|
||||
margin-bottom: 10px;
|
||||
|
||||
code {
|
||||
background-color: black;
|
||||
color: white;
|
||||
.mono-font();
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
code.inline {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: white;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: white;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #32afff;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
list-style-position: outside;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-position: outside;
|
||||
margin-left: 19px;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 4px 10px 4px 10px;
|
||||
border-radius: 3px;
|
||||
background-color: #444;
|
||||
padding: 2px 4px 2px 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.screen-tabs-container {
|
||||
@ -1609,13 +1561,26 @@ body::-webkit-scrollbar {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.ts, .termopts {
|
||||
.ts, .termopts, .renderer {
|
||||
display: flex;
|
||||
color: #aaa;
|
||||
margin-top: 5px;
|
||||
.mono-font(11px);
|
||||
}
|
||||
|
||||
.renderer {
|
||||
color: #aaa;
|
||||
margin-left: 3px;
|
||||
margin-top: 5px;
|
||||
.mono-font(11px);
|
||||
i {
|
||||
display: inline-block;
|
||||
font-size: 9px;
|
||||
margin-right: 2px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.termopts {
|
||||
display: flex;
|
||||
color: #aaa;
|
||||
@ -2978,3 +2943,80 @@ input[type=checkbox] {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.simple-image-renderer {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.markdown {
|
||||
color: @term-white;
|
||||
margin-bottom: 10px;
|
||||
|
||||
code {
|
||||
background-color: black;
|
||||
color: white;
|
||||
.mono-font();
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
code.inline {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: white;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: white;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #32afff;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
list-style-position: outside;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-position: outside;
|
||||
margin-left: 19px;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 4px 10px 4px 10px;
|
||||
border-radius: 3px;
|
||||
background-color: #444;
|
||||
padding: 2px 4px 2px 6px;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: inherit;
|
||||
margin: 4px 10px 4px 10px;
|
||||
padding: 2px 4px 2px 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-renderer.markdown {
|
||||
padding: 5px;
|
||||
line-height: 1.5;
|
||||
|
||||
blockquote {
|
||||
background-color: #222;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #222;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #222;
|
||||
}
|
||||
}
|
||||
|
28
src/sh2.ts
28
src/sh2.ts
@ -6,6 +6,9 @@ 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";
|
||||
|
||||
// @ts-ignore
|
||||
let VERSION = __PROMPT_VERSION__;
|
||||
@ -26,6 +29,31 @@ 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);
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
let reactElem = React.createElement(Main, null, null);
|
||||
let elem = document.getElementById("app");
|
||||
|
194
src/simplerenderer.tsx
Normal file
194
src/simplerenderer.tsx
Normal file
@ -0,0 +1,194 @@
|
||||
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, LineType} from "./types";
|
||||
import {GlobalModel, LineContainerModel, getPtyData, Cmd} from "./model";
|
||||
import {PtyDataBuffer} from "./ptydata";
|
||||
|
||||
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"});
|
||||
ptyData : PtyDataType;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
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.savedHeight = params.savedHeight;
|
||||
if (this.isDone.get()) {
|
||||
this.reload(0);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
})();
|
||||
let rtnp = getPtyData(this.context.sessionId, this.context.cmdId);
|
||||
rtnp.then((ptydata) => {
|
||||
setTimeout(() => {
|
||||
this.ptyData = ptydata;
|
||||
mobx.action(() => {
|
||||
this.loading.set(false);
|
||||
this.loadError.set(null);
|
||||
})();
|
||||
}, delayMs);
|
||||
}).catch((e) => {
|
||||
console.log("error loading data", e);
|
||||
mobx.action(() => {
|
||||
this.loadError.set("error loading data: " + e);
|
||||
})();
|
||||
});
|
||||
}
|
||||
|
||||
receiveData(pos : number, data : Uint8Array, reason? : string) : void {
|
||||
// this.dataBuf.receiveData(pos, data, reason);
|
||||
}
|
||||
}
|
||||
|
||||
function contextFromLine(line : LineType) : RendererContext {
|
||||
return {
|
||||
sessionId: line.sessionid,
|
||||
screenId: line.screenid,
|
||||
cmdId: line.cmdid,
|
||||
lineId: line.lineid,
|
||||
lineNum: line.linenum,
|
||||
};
|
||||
}
|
||||
|
||||
function apiAdapter(lcm : LineContainerModel, line : LineType, cmd : Cmd) : RendererModelContainerApi {
|
||||
return {
|
||||
saveHeight: (height : number) => {
|
||||
lcm.setContentHeight(contextFromLine(line), height);
|
||||
},
|
||||
|
||||
onFocusChanged: (focus : boolean) => {
|
||||
lcm.setTermFocus(line.linenum, focus);
|
||||
},
|
||||
|
||||
dataHandler: (data : string, model : RendererModel) => {
|
||||
cmd.handleDataFromRenderer(data, model);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class SimpleBlobRenderer extends React.Component<{lcm : LineContainerModel, line : LineType, cmd : Cmd, plugin : RendererPluginType}, {}> {
|
||||
model : SimpleBlobRendererModel;
|
||||
wrapperDivRef : React.RefObject<any> = React.createRef();
|
||||
|
||||
constructor(props : any) {
|
||||
super(props);
|
||||
let {lcm, line, cmd} = this.props;
|
||||
let context = contextFromLine(line);
|
||||
let savedHeight = lcm.getContentHeight(context);
|
||||
if (savedHeight == null) {
|
||||
if (line.contentheight != null && line.contentheight != -1) {
|
||||
savedHeight = line.contentheight;
|
||||
}
|
||||
else {
|
||||
savedHeight = 0;
|
||||
}
|
||||
}
|
||||
let initOpts = {
|
||||
context: context,
|
||||
isDone: !cmd.isRunning(),
|
||||
savedHeight: savedHeight,
|
||||
opts: {
|
||||
maxSize: lcm.getMaxContentSize(),
|
||||
idealSize: lcm.getIdealContentSize(),
|
||||
termOpts: cmd.getTermOpts(),
|
||||
termFontSize: GlobalModel.termFontSize.get(),
|
||||
},
|
||||
api: apiAdapter(lcm, line, cmd),
|
||||
};
|
||||
this.model = new SimpleBlobRendererModel();
|
||||
this.model.initialize(initOpts);
|
||||
lcm.registerRenderer(line.cmdid, this.model);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.checkHeight();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
let {lcm, line} = this.props;
|
||||
lcm.unloadRenderer(line.cmdid);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.checkHeight();
|
||||
}
|
||||
|
||||
checkHeight() : void {
|
||||
// TODO: use resizeobserver instead of ref.
|
||||
if (this.wrapperDivRef.current == null) {
|
||||
return;
|
||||
}
|
||||
let height = this.wrapperDivRef.current.offsetHeight;
|
||||
this.model.updateHeight(height);
|
||||
}
|
||||
|
||||
render() {
|
||||
let {plugin} = this.props;
|
||||
let model = this.model;
|
||||
if (model.loading.get()) {
|
||||
return (<div style={{height: this.model.savedHeight}}>...</div>);
|
||||
}
|
||||
let Comp = plugin.component;
|
||||
let dataBlob = new Blob([model.ptyData.data]);
|
||||
return (
|
||||
<div ref={this.wrapperDivRef}>
|
||||
<Comp data={dataBlob} context={model.context} opts={model.opts}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export {SimpleBlobRendererModel, SimpleBlobRenderer};
|
||||
|
@ -222,7 +222,7 @@ class TermWrap {
|
||||
}
|
||||
this.usedRows.set(tur);
|
||||
if (!this.noSetTUR) {
|
||||
GlobalModel.setTUR(termContext, this.termSize, tur);
|
||||
GlobalModel.setContentHeight(termContext, tur);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
59
src/types.ts
59
src/types.ts
@ -1,9 +1,12 @@
|
||||
import * as React from "react";
|
||||
import * as mobx from "mobx";
|
||||
|
||||
type ShareModeType = "local" | "private" | "view" | "shared";
|
||||
type FocusTypeStrs = "input"|"cmd"|"cmd-fg";
|
||||
type HistoryTypeStrs = "global" | "session" | "screen";
|
||||
|
||||
type OV<V> = mobx.IObservableValue<V>;
|
||||
|
||||
type SessionDataType = {
|
||||
sessionid : string,
|
||||
name : string,
|
||||
@ -348,7 +351,61 @@ type RendererContext = {
|
||||
lineNum : number,
|
||||
};
|
||||
|
||||
type RendererOpts = {
|
||||
maxSize : WindowSize,
|
||||
idealSize : WindowSize,
|
||||
termOpts : TermOptsType,
|
||||
termFontSize : number,
|
||||
};
|
||||
|
||||
type RendererOptsUpdate = {
|
||||
maxSize? : WindowSize,
|
||||
idealSize? : WindowSize,
|
||||
termOpts? : TermOptsType,
|
||||
termFontSize? : number,
|
||||
};
|
||||
|
||||
type RendererPluginType = {
|
||||
name : string,
|
||||
rendererType : "simple" | "full",
|
||||
heightType : "rows" | "pixels",
|
||||
dataType : "json" | "blob",
|
||||
collapseType : "hide" | "remove",
|
||||
globalCss? : string,
|
||||
mimeTypes? : string[],
|
||||
modelCtor? : RendererModel,
|
||||
component : SimpleBlobRendererComponent,
|
||||
}
|
||||
|
||||
type RendererModelContainerApi = {
|
||||
onFocusChanged : (focus : boolean) => void,
|
||||
saveHeight : (height : number) => void,
|
||||
dataHandler : (data : string, model : RendererModel) => void,
|
||||
};
|
||||
|
||||
type RendererModelInitializeParams = {
|
||||
context : RendererContext,
|
||||
isDone : boolean,
|
||||
savedHeight : number,
|
||||
opts : RendererOpts,
|
||||
api : RendererModelContainerApi,
|
||||
};
|
||||
|
||||
type RendererModel = {
|
||||
initialize : (params : RendererModelInitializeParams) => void,
|
||||
dispose : () => void,
|
||||
reload : (delayMs : number) => void,
|
||||
giveFocus : () => void,
|
||||
updateOpts : (opts : RendererOptsUpdate) => void,
|
||||
setIsDone : () => void,
|
||||
receiveData : (pos : number, data : Uint8Array, reason? : string) => void,
|
||||
};
|
||||
|
||||
type SimpleBlobRendererComponent = React.ComponentType<{data : Blob, context : RendererContext, opts : RendererOpts}>;
|
||||
type SimpleJsonRendererComponent = React.ComponentType<{data : any, context : RendererContext, opts : RendererOpts}>;
|
||||
type FullRendererComponent = React.ComponentType<{model : any}>;
|
||||
|
||||
type OldRendererModel = {
|
||||
dispose : () => void,
|
||||
reload : (delayMs : number) => void,
|
||||
receiveData : (pos : number, data : Uint8Array, reason? : string) => void,
|
||||
@ -417,4 +474,4 @@ 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};
|
||||
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};
|
||||
|
Loading…
Reference in New Issue
Block a user