diff --git a/src/imagerenderer.ts b/src/imagerenderer.ts deleted file mode 100644 index 9863ae5c..00000000 --- a/src/imagerenderer.ts +++ /dev/null @@ -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; - 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}; - diff --git a/src/imagerenderer.tsx b/src/imagerenderer.tsx new file mode 100644 index 00000000..3cb4bdc1 --- /dev/null +++ b/src/imagerenderer.tsx @@ -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 = mobx.IObservableValue; +type CV = mobx.IComputedValue; + +// 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 ( +
+ +
+ ); + } +} + +export {SimpleImageRenderer}; + diff --git a/src/linecomps.tsx b/src/linecomps.tsx index 4245bc14..53248b19 100644 --- a/src/linecomps.tsx +++ b/src/linecomps.tsx @@ -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 (
@@ -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 (
{formattedTime}
 
+ +
{renderer} 
+
({termOpts.rows}x{termOpts.cols})
@@ -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 (
- + + + + + +
@@ -515,7 +546,7 @@ class LineCmd extends React.Component<{screen : LineContainerModel, line : LineT
- +
focus line ({renderCmdText("L")})
@@ -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, 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, onHeightChange : () => void, collapsed : boolean}, {}> { - elemRef : React.RefObject = React.createRef(); - imageDivRef : React.RefObject = React.createRef(); - imageLoaded : mobx.IObservableValue = 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 ( -
-
-
...
-
- ); - } -} - @mobxReact.observer class TerminalRenderer extends React.Component<{screen : LineContainerModel, line : LineType, width : number, staticRender : boolean, visible : OV, onHeightChange : () => void, collapsed : boolean}, {}> { termLoaded : mobx.IObservableValue = 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 ( diff --git a/src/main.tsx b/src/main.tsx index a169882b..65eb35e2 100644 --- a/src/main.tsx +++ b/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}); })(); } diff --git a/src/markdownrenderer.tsx b/src/markdownrenderer.tsx new file mode 100644 index 00000000..d5c57461 --- /dev/null +++ b/src/markdownrenderer.tsx @@ -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 = mobx.IObservableValue; + +function LinkRenderer(props : any) : any { + let newUrl = "https://extern?" + encodeURIComponent(props.href); + return {props.children} +} + +function HeaderRenderer(props : any, hnum : number) : any { + return ( +
{props.children}
+ ); +} + +function CodeRenderer(props : any) : any { + return ( + {props.children} + ); +} + +@mobxReact.observer +class SimpleMarkdownRenderer extends React.Component<{data : Blob, context : RendererContext, opts : RendererOpts}, {}> { + markdownText : OV = 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 ( +
+ +
+ ); + } +} + +export {SimpleMarkdownRenderer}; diff --git a/src/model.ts b/src/model.ts index dfdb55cb..177538db 100644 --- a/src/model.ts +++ b/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; archived : OV; curRemote : OV; + lastScreenSize : WindowSize; lastCols : number; lastRows : number; selectedLine : OV; @@ -287,7 +320,8 @@ class Screen { anchorOffset : number = 0; termLineNumFocus : OV; setAnchor_debounced : (anchorLine : number, anchorOffset : number) => void; - renderers : Record = {}; // cmdid => TermWrap + terminals : Record = {}; // cmdid => TermWrap + renderers : Record = {}; // 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 = mobx.observable.box(false, {name: "welcomeModalOpen"}); screenSettingsModal : OV<{sessionId : string, screenId : string}> = mobx.observable.box(null, {name: "screenSettingsModal"}); sessionSettingsModal : OV = 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 { 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 { }); } -function getPtyData(sessionId : string, cmdId : string) { +function getPtyData(sessionId : string, cmdId : string) : Promise { 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 { 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}; diff --git a/src/sh2.less b/src/sh2.less index 60281ec1..9f292d48 100644 --- a/src/sh2.less +++ b/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; + } +} diff --git a/src/sh2.ts b/src/sh2.ts index 91663d61..9c58f4e8 100644 --- a/src/sh2.ts +++ b/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"); diff --git a/src/simplerenderer.tsx b/src/simplerenderer.tsx new file mode 100644 index 00000000..7963e970 --- /dev/null +++ b/src/simplerenderer.tsx @@ -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 = mobx.IObservableValue; +type CV = mobx.IComputedValue; + +class SimpleBlobRendererModel { + context : RendererContext; + opts : RendererOpts; + isDone : OV; + api : RendererModelContainerApi; + savedHeight : number; + loading : OV; + loadError : OV = 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 = 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 (
...
); + } + let Comp = plugin.component; + let dataBlob = new Blob([model.ptyData.data]); + return ( +
+ +
+ ); + } +} + +export {SimpleBlobRendererModel, SimpleBlobRenderer}; + diff --git a/src/term.ts b/src/term.ts index fbb964e6..8d56a010 100644 --- a/src/term.ts +++ b/src/term.ts @@ -222,7 +222,7 @@ class TermWrap { } this.usedRows.set(tur); if (!this.noSetTUR) { - GlobalModel.setTUR(termContext, this.termSize, tur); + GlobalModel.setContentHeight(termContext, tur); } })(); } diff --git a/src/types.ts b/src/types.ts index a97341a3..e679c019 100644 --- a/src/types.ts +++ b/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 = mobx.IObservableValue; + 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};