diff --git a/src/bookmarks.tsx b/src/bookmarks.tsx index e1e34280..67e70788 100644 --- a/src/bookmarks.tsx +++ b/src/bookmarks.tsx @@ -172,9 +172,9 @@ class BookmarksView extends React.Component<{}, {}> { let idx : number = 0; let bookmark : BookmarkType = null; return ( -
+
-
+
BOOKMARKS
@@ -190,7 +190,7 @@ class BookmarksView extends React.Component<{}, {}> {
0}> -
+
[Enter] to Use Bookmark
[Backspace/Delete]x2 or to Delete
diff --git a/src/history.tsx b/src/history.tsx new file mode 100644 index 00000000..c7040882 --- /dev/null +++ b/src/history.tsx @@ -0,0 +1,248 @@ +import * as React from "react"; +import * as mobxReact from "mobx-react"; +import * as mobx from "mobx"; +import {If, For, When, Otherwise, Choose} from "tsx-control-statements/components"; +import {sprintf} from "sprintf-js"; +import {boundMethod} from "autobind-decorator"; +import cn from "classnames"; +import {GlobalModel, GlobalCommandRunner} from "./model"; +import {HistoryItem, RemotePtrType} from "./types"; +import dayjs from "dayjs"; +import localizedFormat from 'dayjs/plugin/localizedFormat'; +import {Line} from "./linecomps"; + +dayjs.extend(localizedFormat) + +const PageSize = 50; + +function isBlank(s : string) { + return (s == null || s == ""); +} + +function getHistoryViewTs(nowDate : Date, ts : number) : string { + let itemDate = new Date(ts); + if (nowDate.getFullYear() != itemDate.getFullYear()) { + return dayjs(itemDate).format("M/D/YY"); + } + else if (nowDate.getMonth() != itemDate.getMonth() || nowDate.getDate() != itemDate.getDate()) { + return dayjs(itemDate).format("MMM D"); + } + else { + return dayjs(itemDate).format("h:mm A"); + } +} + +function formatRemoteName(rnames : Record, rptr : RemotePtrType) : string { + if (rptr == null || isBlank(rptr.remoteid)) { + return ""; + } + let rname = rnames[rptr.remoteid]; + if (rname == null) { + rname = rptr.remoteid.substr(0, 8); + } + if (!isBlank(rptr.name)) { + rname = rname + ":" + rptr.name; + } + return "[" + rname + "]"; +} + +function formatSSName(snames : Record, scrnames : Record, item : HistoryItem) : string { + if (isBlank(item.sessionid)) { + return ""; + } + let sessionName = "#" + (snames[item.sessionid] ?? item.sessionid.substr(0, 8)); + if (isBlank(item.screenid)) { + return sessionName; + } + // let screenName = "/" + (scrnames[item.screenid] ?? item.screenid.substr(0, 8)); + // return sessionName + screenName; + return sessionName; +} + +@mobxReact.observer +class HistoryView extends React.Component<{}, {}> { + @boundMethod + clickCloseHandler() : void { + GlobalModel.historyViewModel.closeView(); + } + + @boundMethod + handleNext() { + GlobalModel.historyViewModel.goNext(); + } + + @boundMethod + handlePrev() { + GlobalModel.historyViewModel.goPrev(); + } + + @boundMethod + changeSearchText(e : any) { + mobx.action(() => { + GlobalModel.historyViewModel.searchText.set(e.target.value); + })(); + } + + @boundMethod + searchKeyDown(e : any) { + if (e.code == "Enter") { + e.preventDefault(); + GlobalModel.historyViewModel.submitSearch(); + return; + } + } + + @boundMethod + handleSelect(historyId : string) { + let hvm = GlobalModel.historyViewModel; + mobx.action(() => { + if (hvm.selectedItems.get(historyId)) { + hvm.selectedItems.delete(historyId); + } + else { + hvm.selectedItems.set(historyId, true); + } + })(); + } + + @boundMethod + handleControlCheckbox() { + let hvm = GlobalModel.historyViewModel; + mobx.action(() => { + let numSelected = hvm.selectedItems.size; + if (numSelected > 0) { + hvm.selectedItems.clear(); + return; + } + else { + for (let i=0; i PageSize) { + items = items.slice(0, PageSize); + hasMore = true; + } + let offset = hvm.offset.get(); + let numSelected = hvm.selectedItems.size; + let controlCheckboxIcon = "fa-sharp fa-regular fa-square"; + if (numSelected > 0) { + controlCheckboxIcon = "fa-sharp fa-regular fa-square-minus"; + } + if (numSelected > 0 && numSelected == items.length) { + controlCheckboxIcon = "fa-sharp fa-regular fa-square-check"; + } + let activeItem = hvm.activeItem.get(); + return ( +
+
+
+
+ HISTORY +
+
+
+

+ + + + +

+
+
+
+
+
+ +
+
+ +
+
+
Showing {offset+1}-{offset+items.length}
+
+
+
+
+ + + + + + + + + + + + + + + + + + +
this.handleSelect(item.historyid)}> + + + + + + + + + + {getHistoryViewTs(nowDate, item.ts)} + + {formatSSName(snames, scrnames, item)} + + {formatRemoteName(rnames, item.remote)} + this.activateItem(item.historyid)}> + {item.cmdstr} +
+ +
+
+
+ [Esc] to Close
+
+
+
+ ); + } +} + + +export {HistoryView}; diff --git a/src/linecomps.tsx b/src/linecomps.tsx new file mode 100644 index 00000000..e40980d5 --- /dev/null +++ b/src/linecomps.tsx @@ -0,0 +1,847 @@ +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 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, Window, Screen, ScreenWindow, windowWidthToCols, windowHeightToRows, termHeightFromRows, termWidthFromCols} from "./model"; +import type {LineType, CmdDataType, FeStateType, RemoteType, RemotePtrType, RenderModeType} from "./types"; +import cn from "classnames"; +import {TermWrap} from "./term"; +import type {LineContainerModel} from "./model"; + +dayjs.extend(localizedFormat) + +type OV = mobx.IObservableValue; +type OArr = mobx.IObservableArray; +type OMap = mobx.ObservableMap; + +type HeightChangeCallbackType = (lineNum : number, newHeight : number, oldHeight : number) => void; +type RendererComponentProps = {sw : LineContainerModel, line : LineType, width : number, staticRender : boolean, visible : OV, onHeightChange : HeightChangeCallbackType, collapsed : boolean}; +type RendererComponentType = { new(props : RendererComponentProps) : React.Component }; + +function isBlank(s : string) : boolean { + return (s == null || s == ""); +} + +function getLineId(line : LineType) : string { + return sprintf("%s-%s-%s", line.sessionid, line.windowid, line.lineid); +} + +function makeFullRemoteRef(ownerName : string, remoteRef : string, name : string) : string { + if (isBlank(ownerName) && isBlank(name)) { + return remoteRef; + } + if (!isBlank(ownerName) && isBlank(name)) { + return ownerName + ":" + remoteRef; + } + if (isBlank(ownerName) && !isBlank(name)) { + return remoteRef + ":" + name; + } + return ownerName + ":" + remoteRef + ":" + name; +} + +function getRemoteStr(rptr : RemotePtrType) : string { + if (rptr == null || isBlank(rptr.remoteid)) { + return "(invalid remote)"; + } + let username = (isBlank(rptr.ownerid) ? null : GlobalModel.resolveUserIdToName(rptr.ownerid)); + let remoteRef = GlobalModel.resolveRemoteIdToRef(rptr.remoteid); + let fullRef = makeFullRemoteRef(username, remoteRef, rptr.name); + return fullRef; +} + +function replaceHomePath(path : string, homeDir : string) : string { + if (path == homeDir) { + return "~"; + } + if (path.startsWith(homeDir + "/")) { + return "~" + path.substr(homeDir.length); + } + return path; +} + +function getCwdStr(remote : RemoteType, state : FeStateType) : string { + if ((state == null || state.cwd == null) && remote != null) { + return "~"; + } + let cwd = "?"; + if (state && state.cwd) { + cwd = state.cwd; + } + if (remote && remote.remotevars.home) { + cwd = replaceHomePath(cwd, remote.remotevars.cwd) + } + return cwd; +} + +function getLineDateTimeStr(ts : number) : string { + let lineDate = new Date(ts); + let nowDate = new Date(); + + if (nowDate.getFullYear() != lineDate.getFullYear()) { + return dayjs(lineDate).format("ddd L LTS"); + } + else if (nowDate.getMonth() != lineDate.getMonth() || nowDate.getDate() != lineDate.getDate()) { + let yesterdayDate = (new Date()); + yesterdayDate.setDate(yesterdayDate.getDate()-1); + if (yesterdayDate.getMonth() == lineDate.getMonth() && yesterdayDate.getDate() == lineDate.getDate()) { + return "Yesterday " + dayjs(lineDate).format("LTS");; + } + return dayjs(lineDate).format("ddd L LTS"); + } + else { + return dayjs(lineDate).format("LTS"); + } +} + +@mobxReact.observer +class LineAvatar extends React.Component<{line : LineType, cmd : Cmd}, {}> { + render() { + let {line, cmd} = this.props; + let lineNumStr = (line.linenumtemp ? "~" : "") + String(line.linenum); + let status = (cmd != null ? cmd.getStatus() : "done"); + let rtnstate = (cmd != null ? cmd.getRtnState() : false); + let isComment = (line.linetype == "text"); + return ( +
+ {lineNumStr} + + + + + + + + + +
+ ); + } +} + + +@mobxReact.observer +class LineCmd extends React.Component<{sw : LineContainerModel, line : LineType, width : number, staticRender : boolean, visible : OV, onHeightChange : HeightChangeCallbackType, topBorder : boolean, renderMode : RenderModeType, overrideCollapsed : OV}, {}> { + lineRef : React.RefObject = React.createRef(); + cmdTextRef : React.RefObject = React.createRef(); + rtnStateDiff : mobx.IObservableValue = mobx.observable.box(null, {name: "linecmd-rtn-state-diff"}); + rtnStateDiffFetched : boolean = false; + lastHeight : number; + isOverflow : OV = mobx.observable.box(false, {name: "line-overflow"}); + isCmdExpanded : OV = mobx.observable.box(false, {name: "cmd-expanded"}); + + constructor(props) { + super(props); + } + + checkStateDiffLoad() : void { + let {line, staticRender, visible} = this.props; + if (staticRender || this.isCollapsed()) { + return; + } + if (!visible) { + if (this.rtnStateDiffFetched) { + this.rtnStateDiffFetched = false; + this.setRtnStateDiff(null); + } + return; + } + let cmd = GlobalModel.getCmd(line); + if (cmd == null || !cmd.getRtnState() || this.rtnStateDiffFetched) { + return; + } + if (cmd.getStatus() != "done") { + return; + } + this.fetchRtnStateDiff(); + } + + fetchRtnStateDiff() : void { + if (this.rtnStateDiffFetched) { + return; + } + let {line} = this.props; + this.rtnStateDiffFetched = true; + let usp = new URLSearchParams({sessionid: line.sessionid, cmdid: line.cmdid}); + let url = GlobalModel.getBaseHostPort() + "/api/rtnstate?" + usp.toString(); + let fetchHeaders = GlobalModel.getFetchHeaders(); + fetch(url, {headers: fetchHeaders}).then((resp) => { + if (!resp.ok) { + throw new Error(sprintf("Bad fetch response for /api/rtnstate: %d %s", resp.status, resp.statusText)); + } + return resp.text(); + }).then((text) => { + this.setRtnStateDiff(text ?? ""); + }).catch((err) => { + this.setRtnStateDiff("ERROR " + err.toString()) + }); + } + + setRtnStateDiff(val : string) : void { + mobx.action(() => { + this.rtnStateDiff.set(val); + })(); + } + + componentDidMount() { + this.componentDidUpdate(null, null, null); + this.checkCmdText(); + } + + // FIXME + scrollIntoView() { + let lineElem = document.getElementById("line-" + getLineId(this.props.line)); + lineElem.scrollIntoView({block: "end"}); + } + + @boundMethod + doRefresh() { + let {sw, line} = this.props; + let model = GlobalModel; + let termWrap = sw.getRenderer(line.cmdid); + if (termWrap != null) { + termWrap.reload(500); + } + } + + @boundMethod + handleExpandCmd() : void { + mobx.action(() => { + this.isCmdExpanded.set(true); + })(); + } + + renderCmdText(cmd : Cmd, remote : RemoteType) : any { + if (cmd == null) { + return ( +
+ (cmd not found) +
+ ); + } + if (this.isCmdExpanded.get()) { + return ( + +
+
+ +
+
+
+
{cmd.getFullCmdText()}
+
+
+ ); + } + let isMultiLine = cmd.isMultiLineCmdText(); + return ( +
+
+ + + {cmd.getSingleLineCmdText()} +
+ +
...▼
+
+
+ ); + } + + // TODO: this might not be necessary anymore because we're using this.lastHeight + getSnapshotBeforeUpdate(prevProps, prevState) : {height : number} { + let elem = this.lineRef.current; + if (elem == null) { + return {height: 0}; + } + return {height: elem.offsetHeight}; + } + + componentDidUpdate(prevProps, prevState, snapshot : {height : number}) : void { + this.handleHeightChange(); + this.checkStateDiffLoad(); + this.checkCmdText(); + } + + checkCmdText() { + let metaElem = this.cmdTextRef.current; + if (metaElem == null || metaElem.childNodes.length == 0) { + return; + } + let metaElemWidth = metaElem.offsetWidth; + let metaChild = metaElem.firstChild; + let children = metaChild.childNodes; + let childWidth = 0; + for (let i=0; i metaElemWidth); + if (isOverflow != this.isOverflow.get()) { + mobx.action(() => { + this.isOverflow.set(isOverflow); + })(); + } + } + + @boundMethod + handleHeightChange() { + if (this.props.onHeightChange == null) { + return; + } + let {line} = this.props; + let curHeight = 0; + let elem = this.lineRef.current; + if (elem != null) { + curHeight = elem.offsetHeight; + } + if (this.lastHeight == curHeight) { + return; + } + let lastHeight = this.lastHeight; + this.lastHeight = curHeight; + this.props.onHeightChange(line.linenum, curHeight, lastHeight); + // console.log("line height change: ", line.linenum, lastHeight, "=>", curHeight); + } + + @boundMethod + handleClick() { + let {line} = this.props; + let sel = window.getSelection(); + if (this.lineRef.current != null) { + let selText = sel.toString(); + if (sel.anchorNode != null && this.lineRef.current.contains(sel.anchorNode) && !isBlank(selText)) { + return; + } + } + GlobalCommandRunner.swSelectLine(String(line.linenum), "cmd"); + } + + @boundMethod + clickStar() { + let {line} = this.props; + if (!line.star || line.star == 0) { + GlobalCommandRunner.lineStar(line.lineid, 1); + } + else { + GlobalCommandRunner.lineStar(line.lineid, 0); + } + } + + @boundMethod + clickPin() { + let {line} = this.props; + if (!line.pinned) { + GlobalCommandRunner.linePin(line.lineid, true); + } + else { + GlobalCommandRunner.linePin(line.lineid, false); + } + } + + @boundMethod + clickBookmark() { + let {line} = this.props; + GlobalCommandRunner.lineBookmark(line.lineid); + } + + @boundMethod + handleResizeButton() { + console.log("resize button"); + } + + @boundMethod + handleCollapsedClick() { + let {overrideCollapsed} = this.props; + mobx.action(() => { + let isCollapsed = overrideCollapsed.get(); + overrideCollapsed.set(!isCollapsed); + })(); + } + + getLineDomId() : string { + let {line} = this.props; + return "line-" + getLineId(line); + } + + isCollapsed() : boolean { + let {renderMode, overrideCollapsed} = this.props; + return (renderMode == "collapsed" && !overrideCollapsed.get()); + } + + renderSimple() { + let {sw, line, width, topBorder, renderMode} = this.props; + let cmd = GlobalModel.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 + // terminal-wrapper overhead is 11px (margin/padding) + // inner-height, if zero-lines => 42 + // else: 53+(lines*lineheight) + let height = (isCollapsed ? 30 : 42); // height of zero height terminal + if (!isCollapsed) { + let usedRows = sw.getUsedRows(line, cmd, width); + if (usedRows > 0) { + height = 53 + termHeightFromRows(usedRows, GlobalModel.termFontSize.get()); + } + } + return ( +
+ +
+ ); + } + + renderMetaWrap(cmd : Cmd) { + let {line} = this.props; + let model = GlobalModel; + let formattedTime = getLineDateTimeStr(line.ts); + let termOpts = cmd.getTermOpts(); + let remote = model.getRemote(cmd.remoteId); + return ( +
+
+
{formattedTime}
+
 
+
+ ({termOpts.rows}x{termOpts.cols}) +
+
+ {this.renderCmdText(cmd, remote)} +
+ ); + } + + render() { + let {sw, line, width, staticRender, visible, topBorder, renderMode} = this.props; + let model = GlobalModel; + let lineid = line.lineid; + let isVisible = visible.get(); + if (staticRender || !isVisible) { + return this.renderSimple(); + } + let formattedTime = getLineDateTimeStr(line.ts); + let cmd = model.getCmd(line); + if (cmd == null) { + return ( +
+ [cmd not found '{line.cmdid}'] +
+ ); + } + let status = cmd.getStatus(); + let lineNumStr = (line.linenumtemp ? "~" : "") + String(line.linenum); + let isSelected = mobx.computed(() => (sw.getSelectedLine() == line.linenum), {name: "computed-isSelected"}).get(); + let isPhysicalFocused = mobx.computed(() => sw.getIsFocused(line.linenum), {name: "computed-getIsFocused"}).get(); + let isFocused = mobx.computed(() => { + let swFocusType = sw.getFocusType(); + return isPhysicalFocused && (swFocusType == "cmd" || swFocusType == "cmd-fg") + }, {name: "computed-isFocused"}).get(); + let isFgFocused = mobx.computed(() => { + let swFocusType = sw.getFocusType(); + return isPhysicalFocused && swFocusType == "cmd-fg" + }, {name: "computed-isFgFocused"}).get(); + let isStatic = staticRender; + let isRunning = cmd.isRunning() + let isCollapsed = this.isCollapsed(); + let isExpanded = this.isCmdExpanded.get(); + let rsdiff = this.rtnStateDiff.get(); + // console.log("render", "#" + line.linenum, termHeight, usedRows, cmd.getStatus(), (this.rtnStateDiff.get() != null), (!cmd.isRunning() ? "cmd-done" : "running")); + let mainDivCn = cn( + "line", + "line-cmd", + {"focus": isFocused}, + {"cmd-done": !isRunning}, + {"has-rtnstate": cmd.getRtnState()}, + {"collapsed": isCollapsed}, + {"top-border": topBorder}, + ); + let RendererComponent : RendererComponentType = TerminalRenderer; + if (line.renderer == "image") { + RendererComponent = ImageRenderer; + } + return ( +
+
+
+ + +
+ + +
+
+ {this.renderMetaWrap(cmd)} +
+ +
+
+ + + + + + +
+
+ + +
+ +
state unchanged
+
+
+ +
new state
+
+
{this.rtnStateDiff.get()}
+
+
+
+
+ ); + } +} + +@mobxReact.observer +class Line extends React.Component<{sw : LineContainerModel, line : LineType, width : number, staticRender : boolean, visible : OV, onHeightChange : HeightChangeCallbackType, overrideCollapsed : OV, topBorder : boolean, renderMode : RenderModeType}, {}> { + render() { + let line = this.props.line; + if (line.archived) { + return null; + } + if (line.linetype == "text") { + return ; + } + if (line.linetype == "cmd") { + return ; + } + return
[invalid line type '{line.linetype}']
; + } +} + +@mobxReact.observer +class MarkdownRenderer extends React.Component<{sw : ScreenWindow, 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() { + let rptr = this.props.rptr; + if (rptr == null || isBlank(rptr.remoteid)) { + return   + } + let remote = GlobalModel.getRemote(this.props.rptr.remoteid); + let remoteStr = getRemoteStr(rptr); + let cwd = getCwdStr(remote, this.props.festate); + let isRoot = false; + if (remote && remote.remotevars) { + if (remote.remotevars["sudo"] || remote.remotevars["bestuser"] == "root") { + isRoot = true; + } + } + let colorClass = (isRoot ? "color-red" : "color-green"); + if (remote && remote.remoteopts && remote.remoteopts.color) { + colorClass = "color-" + remote.remoteopts.color; + } + // TESTING cwd shortening with triple colon character + // if (cwd.startsWith("~/work/gopath/src/github.com/scripthaus-dev")) { + // cwd = cwd.replace("~/work/gopath/src/github.com/scripthaus-dev", "\u22EEscripthaus-dev"); + // } + return ( + [{remoteStr}] {cwd} {isRoot ? "#" : "$"} + ); + } +} + +@mobxReact.observer +class LineText extends React.Component<{sw : LineContainerModel, line : LineType, renderMode : RenderModeType, topBorder : boolean}, {}> { + @boundMethod + clickHandler() { + let {line} = this.props; + GlobalCommandRunner.swSelectLine(String(line.linenum)); + } + + render() { + let {sw, line, topBorder, renderMode} = this.props; + let formattedTime = getLineDateTimeStr(line.ts); + let isSelected = mobx.computed(() => (sw.getSelectedLine() == line.linenum), {name: "computed-isSelected"}).get(); + let isFocused = mobx.computed(() => (sw.getFocusType() == "cmd"), {name: "computed-isFocused"}).get(); + let isCollapsed = (renderMode == "collapsed"); + let mainClass = cn( + "line", + "line-text", + "focus-parent", + {"top-border": topBorder}, + {"collapsed": isCollapsed}, + ); + return ( +
+
+ +
+
+
{formattedTime}
+
+
+ {line.text} +
+
+
+ ); + } +} + +@mobxReact.observer +class ImageRenderer extends React.Component<{sw : 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 {sw, line} = this.props; + let model = GlobalModel; + let cmd = model.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 = sw.loadImageRenderer(this.imageDivRef.current, line, cmd); + mobx.action(() => this.imageLoaded.set(true))(); + } + + unloadImage(unmount : boolean) : void { + let {sw, line} = this.props; + sw.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<{sw : LineContainerModel, line : LineType, width : number, staticRender : boolean, visible : OV, onHeightChange : () => void, collapsed : boolean}, {}> { + termLoaded : mobx.IObservableValue = mobx.observable.box(false, {name: "linecmd-term-loaded"}); + elemRef : React.RefObject = React.createRef(); + + constructor(props) { + super(props); + } + + componentDidMount() { + this.componentDidUpdate(null, null, null); + } + + componentWillUnmount() { + if (this.termLoaded.get()) { + this.unloadTerminal(true); + } + } + + getSnapshotBeforeUpdate(prevProps, prevState) : {height : number} { + let elem = this.elemRef.current; + if (elem == null) { + return {height: 0}; + } + return {height: elem.offsetHeight}; + } + + componentDidUpdate(prevProps, prevState, snapshot : {height : number}) : void { + if (this.props.onHeightChange == null) { + return; + } + let {line} = this.props; + let curHeight = 0; + let elem = this.elemRef.current; + if (elem != null) { + curHeight = elem.offsetHeight; + } + if (snapshot == null) { + snapshot = {height: 0}; + } + if (snapshot.height != curHeight) { + this.props.onHeightChange(); + // console.log("term-render height change: ", line.linenum, snapshot.height, "=>", curHeight); + } + this.checkLoad(); + } + + checkLoad() : void { + let {line, staticRender, visible, collapsed} = this.props; + if (staticRender) { + return; + } + let vis = visible && visible.get() && !collapsed; + let curVis = this.termLoaded.get(); + if (vis && !curVis) { + this.loadTerminal(); + } + else if (!vis && curVis) { + this.unloadTerminal(false); + } + } + + loadTerminal() : void { + let {sw, line} = this.props; + let model = GlobalModel; + let cmd = model.getCmd(line); + if (cmd == null) { + return; + } + let termId = "term-" + getLineId(line); + let termElem = document.getElementById(termId); + if (termElem == null) { + console.log("cannot load terminal, no term elem found", termId); + return; + } + sw.loadTerminalRenderer(termElem, line, cmd, this.props.width); + mobx.action(() => this.termLoaded.set(true))(); + } + + unloadTerminal(unmount : boolean) : void { + let {sw, line} = this.props; + sw.unloadRenderer(line.cmdid); + if (!unmount) { + mobx.action(() => this.termLoaded.set(false))(); + let termId = "term-" + getLineId(line); + let termElem = document.getElementById(termId); + if (termElem != null) { + termElem.replaceChildren(); + } + } + } + + @boundMethod + clickTermBlock(e : any) { + let {sw, line} = this.props; + let model = GlobalModel; + let termWrap = sw.getRenderer(line.cmdid); + if (termWrap != null) { + termWrap.giveFocus(); + } + } + + render() { + let {sw, line, width, staticRender, visible, collapsed} = this.props; + let isVisible = visible.get(); // for reaction + let isPhysicalFocused = mobx.computed(() => sw.getIsFocused(line.linenum), {name: "computed-getIsFocused"}).get(); + let isFocused = mobx.computed(() => { + let swFocusType = sw.getFocusType(); + return isPhysicalFocused && (swFocusType == "cmd" || swFocusType == "cmd-fg") + }, {name: "computed-isFocused"}).get(); + let cmd = GlobalModel.getCmd(line); // will not be null + let usedRows = sw.getUsedRows(line, cmd, width); + let termHeight = termHeightFromRows(usedRows, GlobalModel.termFontSize.get()); + let termLoaded = this.termLoaded.get(); + return ( +
+ +
+
+
+
...
+ +
+ ); + } +} + +export {Line, Prompt}; diff --git a/src/main.tsx b/src/main.tsx index 83b18b5d..0b02843d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -8,15 +8,15 @@ import {v4 as uuidv4} from "uuid"; import dayjs from "dayjs"; import {If, For, When, Otherwise, Choose} from "tsx-control-statements/components"; import cn from "classnames"; -import {TermWrap} from "./term"; -import type {SessionDataType, LineType, CmdDataType, RemoteType, RemoteStateType, RemoteInstanceType, RemotePtrType, HistoryItem, HistoryQueryOpts, RemoteEditType, FeStateType, ContextMenuOpts, BookmarkType} from "./types"; +import type {SessionDataType, LineType, CmdDataType, RemoteType, RemoteStateType, RemoteInstanceType, RemotePtrType, HistoryItem, HistoryQueryOpts, RemoteEditType, FeStateType, ContextMenuOpts, BookmarkType, RenderModeType} from "./types"; import localizedFormat from 'dayjs/plugin/localizedFormat'; import {GlobalModel, GlobalCommandRunner, Session, Cmd, Window, Screen, ScreenWindow, riToRPtr, windowWidthToCols, windowHeightToRows, termHeightFromRows, termWidthFromCols} from "./model"; import {isModKeyPress} from "./util"; -import {ImageRendererModel} from "./imagerenderer"; import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' import {BookmarksView} from "./bookmarks"; +import {HistoryView} from "./history"; +import {Line, Prompt} from "./linecomps"; dayjs.extend(localizedFormat) @@ -32,12 +32,6 @@ type OArr = mobx.IObservableArray; type OMap = mobx.ObservableMap; type VisType = "visible" | ""; -type RenderModeType = "normal" | "collapsed"; - -type HeightChangeCallbackType = (lineNum : number, newHeight : number, oldHeight : number) => void; - -type RendererComponentProps = {sw : ScreenWindow, line : LineType, width : number, staticRender : boolean, visible : OV, onHeightChange : HeightChangeCallbackType, collapsed : boolean}; -type RendererComponentType = { new(props : RendererComponentProps) : React.Component }; type InterObsValue = { sessionid : string, @@ -52,6 +46,16 @@ function isBlank(s : string) : boolean { return (s == null || s == ""); } +function getTodayStr() : string { + return getDateStr(new Date()); +} + +function getYesterdayStr() : string { + let d = new Date(); + d.setDate(d.getDate()-1); + return getDateStr(d); +} + function scrollDiv(div : any, amt : number) { if (div == null) { return; @@ -76,861 +80,6 @@ function pageSize(div : any) : number { return size; } -function getLineId(line : LineType) : string { - return sprintf("%s-%s-%s", line.sessionid, line.windowid, line.lineid); -} - -function makeFullRemoteRef(ownerName : string, remoteRef : string, name : string) : string { - if (isBlank(ownerName) && isBlank(name)) { - return remoteRef; - } - if (!isBlank(ownerName) && isBlank(name)) { - return ownerName + ":" + remoteRef; - } - if (isBlank(ownerName) && !isBlank(name)) { - return remoteRef + ":" + name; - } - return ownerName + ":" + remoteRef + ":" + name; -} - -function getRemoteStr(rptr : RemotePtrType) : string { - if (rptr == null || isBlank(rptr.remoteid)) { - return "(invalid remote)"; - } - let username = (isBlank(rptr.ownerid) ? null : GlobalModel.resolveUserIdToName(rptr.ownerid)); - let remoteRef = GlobalModel.resolveRemoteIdToRef(rptr.remoteid); - let fullRef = makeFullRemoteRef(username, remoteRef, rptr.name); - return fullRef; -} - -function replaceHomePath(path : string, homeDir : string) : string { - if (path == homeDir) { - return "~"; - } - if (path.startsWith(homeDir + "/")) { - return "~" + path.substr(homeDir.length); - } - return path; -} - -function getCwdStr(remote : RemoteType, state : FeStateType) : string { - if ((state == null || state.cwd == null) && remote != null) { - return "~"; - } - let cwd = "?"; - if (state && state.cwd) { - cwd = state.cwd; - } - if (remote && remote.remotevars.home) { - cwd = replaceHomePath(cwd, remote.remotevars.cwd) - } - return cwd; -} - -function getLineDateTimeStr(ts : number) : string { - let lineDate = new Date(ts); - let nowDate = new Date(); - - - if (nowDate.getFullYear() != lineDate.getFullYear()) { - return dayjs(lineDate).format("ddd L LTS"); - } - else if (nowDate.getMonth() != lineDate.getMonth() || nowDate.getDate() != lineDate.getDate()) { - let yesterdayDate = (new Date()); - yesterdayDate.setDate(yesterdayDate.getDate()-1); - if (yesterdayDate.getMonth() == lineDate.getMonth() && yesterdayDate.getDate() == lineDate.getDate()) { - return "Yesterday " + dayjs(lineDate).format("LTS");; - } - return dayjs(lineDate).format("ddd L LTS"); - } - else { - return dayjs(ts).format("LTS"); - } -} - -function getTodayStr() : string { - return getDateStr(new Date()); -} - -function getYesterdayStr() : string { - let d = new Date(); - d.setDate(d.getDate()-1); - return getDateStr(d); -} - -const DOW_STRS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; - -function getDateStr(d : Date) : string { - let yearStr = String(d.getFullYear()); - let monthStr = String(d.getMonth()+1); - if (monthStr.length == 1) { - monthStr = "0" + monthStr; - } - let dayStr = String(d.getDate()); - if (dayStr.length == 1) { - dayStr = "0" + dayStr; - } - let dowStr = DOW_STRS[d.getDay()]; - return dowStr + " " + yearStr + "-" + monthStr + "-" + dayStr; -} - -function getLineDateStr(todayDate : string, yesterdayDate : string, ts : number) : string { - let lineDate = new Date(ts); - let dateStr = getDateStr(lineDate); - if (dateStr == todayDate) { - return "today"; - } - if (dateStr == yesterdayDate) { - return "yesterday"; - } - return dateStr; -} - -@mobxReact.observer -class LineAvatar extends React.Component<{line : LineType, cmd : Cmd}, {}> { - render() { - let {line, cmd} = this.props; - let lineNumStr = (line.linenumtemp ? "~" : "") + String(line.linenum); - let status = (cmd != null ? cmd.getStatus() : "done"); - let rtnstate = (cmd != null ? cmd.getRtnState() : false); - let isComment = (line.linetype == "text"); - return ( -
- {lineNumStr} - - - - - - - - - -
- ); - } -} - -@mobxReact.observer -class LineText extends React.Component<{sw : ScreenWindow, line : LineType, renderMode : RenderModeType, topBorder : boolean}, {}> { - @boundMethod - clickHandler() { - let {line} = this.props; - GlobalCommandRunner.swSelectLine(String(line.linenum)); - } - - render() { - let {sw, line, topBorder, renderMode} = this.props; - let formattedTime = getLineDateTimeStr(line.ts); - let isSelected = mobx.computed(() => (sw.selectedLine.get() == line.linenum), {name: "computed-isSelected"}).get(); - let isFocused = mobx.computed(() => (sw.focusType.get() == "cmd"), {name: "computed-isFocused"}).get(); - let isCollapsed = (renderMode == "collapsed"); - let mainClass = cn( - "line", - "line-text", - "focus-parent", - {"top-border": topBorder}, - {"collapsed": isCollapsed}, - ); - return ( -
-
- -
-
-
{formattedTime}
-
-
- {line.text} -
-
-
- ); - } -} - -@mobxReact.observer -class Prompt extends React.Component<{rptr : RemotePtrType, festate : FeStateType}, {}> { - render() { - let rptr = this.props.rptr; - if (rptr == null || isBlank(rptr.remoteid)) { - return   - } - let remote = GlobalModel.getRemote(this.props.rptr.remoteid); - let remoteStr = getRemoteStr(rptr); - let cwd = getCwdStr(remote, this.props.festate); - let isRoot = false; - if (remote && remote.remotevars) { - if (remote.remotevars["sudo"] || remote.remotevars["bestuser"] == "root") { - isRoot = true; - } - } - let colorClass = (isRoot ? "color-red" : "color-green"); - if (remote && remote.remoteopts && remote.remoteopts.color) { - colorClass = "color-" + remote.remoteopts.color; - } - // TESTING cwd shortening with triple colon character - // if (cwd.startsWith("~/work/gopath/src/github.com/scripthaus-dev")) { - // cwd = cwd.replace("~/work/gopath/src/github.com/scripthaus-dev", "\u22EEscripthaus-dev"); - // } - return ( - [{remoteStr}] {cwd} {isRoot ? "#" : "$"} - ); - } -} - -@mobxReact.observer -class ImageRenderer extends React.Component<{sw : ScreenWindow, 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 {sw, line} = this.props; - let model = GlobalModel; - let cmd = model.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 = sw.loadImageRenderer(this.imageDivRef.current, line, cmd); - mobx.action(() => this.imageLoaded.set(true))(); - } - - unloadImage(unmount : boolean) : void { - let {sw, line} = this.props; - sw.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<{sw : ScreenWindow, line : LineType, width : number, staticRender : boolean, visible : OV, onHeightChange : () => void, collapsed : boolean}, {}> { - termLoaded : mobx.IObservableValue = mobx.observable.box(false, {name: "linecmd-term-loaded"}); - elemRef : React.RefObject = React.createRef(); - - constructor(props) { - super(props); - } - - componentDidMount() { - this.componentDidUpdate(null, null, null); - } - - componentWillUnmount() { - if (this.termLoaded.get()) { - this.unloadTerminal(true); - } - } - - getSnapshotBeforeUpdate(prevProps, prevState) : {height : number} { - let elem = this.elemRef.current; - if (elem == null) { - return {height: 0}; - } - return {height: elem.offsetHeight}; - } - - componentDidUpdate(prevProps, prevState, snapshot : {height : number}) : void { - if (this.props.onHeightChange == null) { - return; - } - let {line} = this.props; - let curHeight = 0; - let elem = this.elemRef.current; - if (elem != null) { - curHeight = elem.offsetHeight; - } - if (snapshot == null) { - snapshot = {height: 0}; - } - if (snapshot.height != curHeight) { - this.props.onHeightChange(); - // console.log("term-render height change: ", line.linenum, snapshot.height, "=>", curHeight); - } - this.checkLoad(); - } - - checkLoad() : void { - let {line, staticRender, visible, collapsed} = this.props; - if (staticRender) { - return; - } - let vis = visible && visible.get() && !collapsed; - let curVis = this.termLoaded.get(); - if (vis && !curVis) { - this.loadTerminal(); - } - else if (!vis && curVis) { - this.unloadTerminal(false); - } - } - - loadTerminal() : void { - let {sw, line} = this.props; - let model = GlobalModel; - let cmd = model.getCmd(line); - if (cmd == null) { - return; - } - let termId = "term-" + getLineId(line); - let termElem = document.getElementById(termId); - if (termElem == null) { - console.log("cannot load terminal, no term elem found", termId); - return; - } - sw.loadTerminalRenderer(termElem, line, cmd, this.props.width); - mobx.action(() => this.termLoaded.set(true))(); - } - - unloadTerminal(unmount : boolean) : void { - let {sw, line} = this.props; - sw.unloadRenderer(line.cmdid); - if (!unmount) { - mobx.action(() => this.termLoaded.set(false))(); - let termId = "term-" + getLineId(line); - let termElem = document.getElementById(termId); - if (termElem != null) { - termElem.replaceChildren(); - } - } - } - - @boundMethod - clickTermBlock(e : any) { - let {sw, line} = this.props; - let model = GlobalModel; - let termWrap = sw.getRenderer(line.cmdid); - if (termWrap != null) { - termWrap.giveFocus(); - } - } - - render() { - let {sw, line, width, staticRender, visible, collapsed} = this.props; - let isVisible = visible.get(); // for reaction - let isPhysicalFocused = mobx.computed(() => sw.getIsFocused(line.linenum), {name: "computed-getIsFocused"}).get(); - let isFocused = mobx.computed(() => { - let swFocusType = sw.focusType.get(); - return isPhysicalFocused && (swFocusType == "cmd" || swFocusType == "cmd-fg") - }, {name: "computed-isFocused"}).get(); - let cmd = GlobalModel.getCmd(line); // will not be null - let usedRows = sw.getUsedRows(line, cmd, width); - let termHeight = termHeightFromRows(usedRows, GlobalModel.termFontSize.get()); - let termLoaded = this.termLoaded.get(); - return ( -
- -
-
-
-
...
- -
- ); - } -} - -@mobxReact.observer -class MarkdownRenderer extends React.Component<{sw : ScreenWindow, line : LineType, width : number, staticRender : boolean, visible : OV, onHeightChange : HeightChangeCallbackType}, {}> { - render() { - return null; - } -} - -@mobxReact.observer -class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType, width : number, staticRender : boolean, visible : OV, onHeightChange : HeightChangeCallbackType, topBorder : boolean, renderMode : RenderModeType, overrideCollapsed : OV}, {}> { - lineRef : React.RefObject = React.createRef(); - cmdTextRef : React.RefObject = React.createRef(); - rtnStateDiff : mobx.IObservableValue = mobx.observable.box(null, {name: "linecmd-rtn-state-diff"}); - rtnStateDiffFetched : boolean = false; - lastHeight : number; - isOverflow : OV = mobx.observable.box(false, {name: "line-overflow"}); - isCmdExpanded : OV = mobx.observable.box(false, {name: "cmd-expanded"}); - - constructor(props) { - super(props); - } - - checkStateDiffLoad() : void { - let {line, staticRender, visible} = this.props; - if (staticRender || this.isCollapsed()) { - return; - } - if (!visible) { - if (this.rtnStateDiffFetched) { - this.rtnStateDiffFetched = false; - this.setRtnStateDiff(null); - } - return; - } - let cmd = GlobalModel.getCmd(line); - if (cmd == null || !cmd.getRtnState() || this.rtnStateDiffFetched) { - return; - } - if (cmd.getStatus() != "done") { - return; - } - this.fetchRtnStateDiff(); - } - - fetchRtnStateDiff() : void { - if (this.rtnStateDiffFetched) { - return; - } - let {line} = this.props; - this.rtnStateDiffFetched = true; - let usp = new URLSearchParams({sessionid: line.sessionid, cmdid: line.cmdid}); - let url = GlobalModel.getBaseHostPort() + "/api/rtnstate?" + usp.toString(); - let fetchHeaders = GlobalModel.getFetchHeaders(); - fetch(url, {headers: fetchHeaders}).then((resp) => { - if (!resp.ok) { - throw new Error(sprintf("Bad fetch response for /api/rtnstate: %d %s", resp.status, resp.statusText)); - } - return resp.text(); - }).then((text) => { - this.setRtnStateDiff(text ?? ""); - }).catch((err) => { - this.setRtnStateDiff("ERROR " + err.toString()) - }); - } - - setRtnStateDiff(val : string) : void { - mobx.action(() => { - this.rtnStateDiff.set(val); - })(); - } - - componentDidMount() { - this.componentDidUpdate(null, null, null); - this.checkCmdText(); - } - - // FIXME - scrollIntoView() { - let lineElem = document.getElementById("line-" + getLineId(this.props.line)); - lineElem.scrollIntoView({block: "end"}); - } - - @boundMethod - doRefresh() { - let {sw, line} = this.props; - let model = GlobalModel; - let termWrap = sw.getRenderer(line.cmdid); - if (termWrap != null) { - termWrap.reload(500); - } - } - - @boundMethod - handleExpandCmd() : void { - mobx.action(() => { - this.isCmdExpanded.set(true); - })(); - } - - renderCmdText(cmd : Cmd, remote : RemoteType) : any { - if (cmd == null) { - return ( -
- (cmd not found) -
- ); - } - if (this.isCmdExpanded.get()) { - return ( - -
-
- -
-
-
-
{cmd.getFullCmdText()}
-
-
- ); - } - let isMultiLine = cmd.isMultiLineCmdText(); - return ( -
-
- - - {cmd.getSingleLineCmdText()} -
- -
...▼
-
-
- ); - } - - // TODO: this might not be necessary anymore because we're using this.lastHeight - getSnapshotBeforeUpdate(prevProps, prevState) : {height : number} { - let elem = this.lineRef.current; - if (elem == null) { - return {height: 0}; - } - return {height: elem.offsetHeight}; - } - - componentDidUpdate(prevProps, prevState, snapshot : {height : number}) : void { - this.handleHeightChange(); - this.checkStateDiffLoad(); - this.checkCmdText(); - } - - checkCmdText() { - let metaElem = this.cmdTextRef.current; - if (metaElem == null || metaElem.childNodes.length == 0) { - return; - } - let metaElemWidth = metaElem.offsetWidth; - let metaChild = metaElem.firstChild; - let children = metaChild.childNodes; - let childWidth = 0; - for (let i=0; i metaElemWidth); - if (isOverflow != this.isOverflow.get()) { - mobx.action(() => { - this.isOverflow.set(isOverflow); - })(); - } - } - - @boundMethod - handleHeightChange() { - if (this.props.onHeightChange == null) { - return; - } - let {line} = this.props; - let curHeight = 0; - let elem = this.lineRef.current; - if (elem != null) { - curHeight = elem.offsetHeight; - } - if (this.lastHeight == curHeight) { - return; - } - let lastHeight = this.lastHeight; - this.lastHeight = curHeight; - this.props.onHeightChange(line.linenum, curHeight, lastHeight); - // console.log("line height change: ", line.linenum, lastHeight, "=>", curHeight); - } - - @boundMethod - handleClick() { - let {line} = this.props; - let sel = window.getSelection(); - if (this.lineRef.current != null) { - let selText = sel.toString(); - if (sel.anchorNode != null && this.lineRef.current.contains(sel.anchorNode) && !isBlank(selText)) { - return; - } - } - GlobalCommandRunner.swSelectLine(String(line.linenum), "cmd"); - } - - @boundMethod - clickStar() { - let {line} = this.props; - if (!line.star || line.star == 0) { - GlobalCommandRunner.lineStar(line.lineid, 1); - } - else { - GlobalCommandRunner.lineStar(line.lineid, 0); - } - } - - @boundMethod - clickPin() { - let {line} = this.props; - if (!line.pinned) { - GlobalCommandRunner.linePin(line.lineid, true); - } - else { - GlobalCommandRunner.linePin(line.lineid, false); - } - } - - @boundMethod - clickBookmark() { - let {line} = this.props; - GlobalCommandRunner.lineBookmark(line.lineid); - } - - @boundMethod - handleResizeButton() { - console.log("resize button"); - } - - @boundMethod - handleCollapsedClick() { - let {overrideCollapsed} = this.props; - mobx.action(() => { - let isCollapsed = overrideCollapsed.get(); - overrideCollapsed.set(!isCollapsed); - })(); - } - - getLineDomId() : string { - let {line} = this.props; - return "line-" + getLineId(line); - } - - isCollapsed() : boolean { - let {renderMode, overrideCollapsed} = this.props; - return (renderMode == "collapsed" && !overrideCollapsed.get()); - } - - renderSimple() { - let {sw, line, width, topBorder, renderMode} = this.props; - let cmd = GlobalModel.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 - // terminal-wrapper overhead is 11px (margin/padding) - // inner-height, if zero-lines => 42 - // else: 53+(lines*lineheight) - let height = (isCollapsed ? 30 : 42); // height of zero height terminal - if (!isCollapsed) { - let usedRows = sw.getUsedRows(line, cmd, width); - if (usedRows > 0) { - height = 53 + termHeightFromRows(usedRows, GlobalModel.termFontSize.get()); - } - } - return ( -
- -
- ); - } - - renderMetaWrap(cmd : Cmd) { - let {line} = this.props; - let model = GlobalModel; - let formattedTime = getLineDateTimeStr(line.ts); - let termOpts = cmd.getTermOpts(); - let remote = model.getRemote(cmd.remoteId); - return ( -
-
-
{formattedTime}
-
 
-
- ({termOpts.rows}x{termOpts.cols}) -
-
- {this.renderCmdText(cmd, remote)} -
- ); - } - - render() { - let {sw, line, width, staticRender, visible, topBorder, renderMode} = this.props; - let model = GlobalModel; - let lineid = line.lineid; - let isVisible = visible.get(); - if (staticRender || !isVisible) { - return this.renderSimple(); - } - let formattedTime = getLineDateTimeStr(line.ts); - let cmd = model.getCmd(line); - if (cmd == null) { - return ( -
- [cmd not found '{line.cmdid}'] -
- ); - } - let status = cmd.getStatus(); - let lineNumStr = (line.linenumtemp ? "~" : "") + String(line.linenum); - let isSelected = mobx.computed(() => (sw.selectedLine.get() == line.linenum), {name: "computed-isSelected"}).get(); - let isPhysicalFocused = mobx.computed(() => sw.getIsFocused(line.linenum), {name: "computed-getIsFocused"}).get(); - let isFocused = mobx.computed(() => { - let swFocusType = sw.focusType.get(); - return isPhysicalFocused && (swFocusType == "cmd" || swFocusType == "cmd-fg") - }, {name: "computed-isFocused"}).get(); - let isFgFocused = mobx.computed(() => { - let swFocusType = sw.focusType.get(); - return isPhysicalFocused && swFocusType == "cmd-fg" - }, {name: "computed-isFgFocused"}).get(); - let isStatic = staticRender; - let isRunning = cmd.isRunning() - let isCollapsed = this.isCollapsed(); - let isExpanded = this.isCmdExpanded.get(); - let rsdiff = this.rtnStateDiff.get(); - // console.log("render", "#" + line.linenum, termHeight, usedRows, cmd.getStatus(), (this.rtnStateDiff.get() != null), (!cmd.isRunning() ? "cmd-done" : "running")); - let mainDivCn = cn( - "line", - "line-cmd", - {"focus": isFocused}, - {"cmd-done": !isRunning}, - {"has-rtnstate": cmd.getRtnState()}, - {"collapsed": isCollapsed}, - {"top-border": topBorder}, - ); - let RendererComponent : RendererComponentType = TerminalRenderer; - if (line.renderer == "image") { - RendererComponent = ImageRenderer; - } - return ( -
-
-
- - -
- - -
-
- {this.renderMetaWrap(cmd)} -
- -
-
- - - - - - -
-
- - -
- -
state unchanged
-
-
- -
new state
-
-
{this.rtnStateDiff.get()}
-
-
-
-
- ); - } -} - -@mobxReact.observer -class Line extends React.Component<{sw : ScreenWindow, line : LineType, width : number, staticRender : boolean, visible : OV, onHeightChange : HeightChangeCallbackType, overrideCollapsed : OV, topBorder : boolean, renderMode : RenderModeType}, {}> { - render() { - let line = this.props.line; - if (line.archived) { - return null; - } - if (line.linetype == "text") { - return ; - } - if (line.linetype == "cmd") { - return ; - } - return
[invalid line type '{line.linetype}']
; - } -} - @mobxReact.observer class TextAreaInput extends React.Component<{onHeightChange : () => void}, {}> { lastTab : boolean = false; @@ -2311,6 +1460,34 @@ class CmdInput extends React.Component<{}, {}> { } } +const DOW_STRS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + +function getDateStr(d : Date) : string { + let yearStr = String(d.getFullYear()); + let monthStr = String(d.getMonth()+1); + if (monthStr.length == 1) { + monthStr = "0" + monthStr; + } + let dayStr = String(d.getDate()); + if (dayStr.length == 1) { + dayStr = "0" + dayStr; + } + let dowStr = DOW_STRS[d.getDay()]; + return dowStr + " " + yearStr + "-" + monthStr + "-" + dayStr; +} + +function getLineDateStr(todayDate : string, yesterdayDate : string, ts : number) : string { + let lineDate = new Date(ts); + let dateStr = getDateStr(lineDate); + if (dateStr == todayDate) { + return "today"; + } + if (dateStr == yesterdayDate) { + return "yesterday"; + } + return dateStr; +} + @mobxReact.observer class LinesView extends React.Component<{sw : ScreenWindow, width : number, lines : LineType[], renderMode : RenderModeType}, {}> { rszObs : any; @@ -2456,7 +1633,7 @@ class LinesView extends React.Component<{sw : ScreenWindow, width : number, line else { this.restoreAnchorOffset("re-mount"); } - this.lastSelectedLine = sw.selectedLine.get(); + this.lastSelectedLine = sw.getSelectedLine(); this.lastLinesLength = lines.length; let linesElem = this.linesRef.current; @@ -2523,7 +1700,7 @@ class LinesView extends React.Component<{sw : ScreenWindow, width : number, line if (linesElem == null) { return null; } - let newLine = sw.selectedLine.get(); + let newLine = sw.getSelectedLine(); if (newLine == 0) { return; } @@ -2565,9 +1742,9 @@ class LinesView extends React.Component<{sw : ScreenWindow, width : number, line componentDidUpdate(prevProps, prevState, snapshot) : void { let {sw, lines} = this.props; - if (sw.selectedLine.get() != this.lastSelectedLine) { + if (sw.getSelectedLine() != this.lastSelectedLine) { this.updateSelectedLine(); - this.lastSelectedLine = sw.selectedLine.get(); + this.lastSelectedLine = sw.getSelectedLine(); } else if (lines.length != this.lastLinesLength) { this.restoreAnchorOffset("line-length-change"); } @@ -2626,7 +1803,7 @@ class LinesView extends React.Component<{sw : ScreenWindow, width : number, line render() { let {sw, width, lines, renderMode} = this.props; - let selectedLine = sw.selectedLine.get(); // for re-rendering + let selectedLine = sw.getSelectedLine(); // for re-rendering let line : LineType = null; for (let i=0; i { let screen = GlobalModel.getScreenById(sw.sessionId, sw.screenId); let session = GlobalModel.getSessionById(sw.sessionId); let isActive = sw.isActive(); - let selectedLine = sw.selectedLine.get(); + let selectedLine = sw.getSelectedLine(); let lines = win.getNonArchivedLines(); let renderMode = this.renderMode.get(); return ( @@ -2972,18 +2149,6 @@ class SessionView extends React.Component<{}, {}> { } } -@mobxReact.observer -class HistoryView extends React.Component<{}, {}> { - render() { - let isHidden = (GlobalModel.activeMainView.get() != "history"); - return ( -
-
HISTORY
-
- ); - } -} - function getConnVal(r : RemoteType) : number { if (r.status == "connected") { return 1; @@ -3067,7 +2232,19 @@ class MainSideBar extends React.Component<{}, {}> { @boundMethod handleHistoryClick() : void { - console.log("history click"); + if (GlobalModel.activeMainView.get() == "history") { + mobx.action(() => { + GlobalModel.activeMainView.set("session"); + })(); + return; + } + GlobalCommandRunner.historyView({}); + } + + @boundMethod + handlePlaybookClick() : void { + console.log("playbook click"); + return; } @boundMethod @@ -3109,6 +2286,7 @@ class MainSideBar extends React.Component<{}, {}> { } let isCollapsed = this.collapsed.get(); let mainView = GlobalModel.activeMainView.get(); + let activePlaybookId : string = null; return (

@@ -3149,17 +2327,24 @@ class MainSideBar extends React.Component<{}, {}> { -
    + -
      + +

      + Playbooks +

      +
      focus={sw.focusType.get()}
      - sline={sw.selectedLine.get()}
      + sline={sw.getSelectedLine()}
      termfocus={sw.termLineNumFocus.get()}
      diff --git a/src/model.ts b/src/model.ts index 3b460bdb..a5bb216a 100644 --- a/src/model.ts +++ b/src/model.ts @@ -5,7 +5,7 @@ import {debounce} from "throttle-debounce"; import {handleJsonFetchResponse, base64ToArray, genMergeData, genMergeSimpleData, boundInt, isModKeyPress} from "./util"; import {TermWrap} from "./term"; import {v4 as uuidv4} from "uuid"; -import type {SessionDataType, WindowDataType, LineType, RemoteType, HistoryItem, RemoteInstanceType, RemotePtrType, CmdDataType, FeCmdPacketType, TermOptsType, RemoteStateType, ScreenDataType, ScreenWindowType, ScreenOptsType, LayoutType, PtyDataUpdateType, ModelUpdateType, UpdateMessage, InfoType, CmdLineUpdateType, UIContextType, HistoryInfoType, HistoryQueryOpts, FeInputPacketType, TermWinSize, RemoteInputPacketType, FeStateType, ContextMenuOpts, RendererContext, RendererModel, PtyDataType, BookmarkType, ClientDataType} from "./types"; +import type {SessionDataType, WindowDataType, LineType, RemoteType, HistoryItem, RemoteInstanceType, RemotePtrType, CmdDataType, FeCmdPacketType, TermOptsType, RemoteStateType, ScreenDataType, ScreenWindowType, ScreenOptsType, LayoutType, PtyDataUpdateType, ModelUpdateType, UpdateMessage, InfoType, CmdLineUpdateType, UIContextType, HistoryInfoType, HistoryQueryOpts, FeInputPacketType, TermWinSize, RemoteInputPacketType, FeStateType, ContextMenuOpts, RendererContext, RendererModel, PtyDataType, BookmarkType, ClientDataType, HistoryViewDataType} from "./types"; import {WSControl} from "./ws"; import {ImageRendererModel} from "./imagerenderer"; import {measureText, getMonoFontSize} from "./textmeasure"; @@ -28,6 +28,17 @@ const VERSION = __PROMPT_VERSION__; // @ts-ignore const BUILD = __PROMPT_BUILD__; +type LineContainerModel = { + loadTerminalRenderer : (elem : Element, line : LineType, cmd : Cmd, width : number) => void, + loadImageRenderer : (imageDivElem : any, line : LineType, cmd : Cmd) => ImageRendererModel, + unloadRenderer : (cmdId : string) => void, + getUsedRows : (line : LineType, cmd : Cmd, width : number) => number, + getIsFocused : (lineNum : number) => boolean, + getRenderer : (cmdId : string) => RendererModel, + getFocusType : () => "input" | "cmd" | "cmd-fg", + getSelectedLine : () => number, +} + type SWLinePtr = { line : LineType, @@ -408,6 +419,10 @@ class ScreenWindow { })(); } + getFocusType() : "input" | "cmd" | "cmd-fg" { + return this.focusType.get(); + } + setAnchor(anchorLine : number, anchorOffset : number) : void { let setVal = ((anchorLine == null || anchorLine == 0) ? "0" : sprintf("%d:%d", anchorLine, anchorOffset)); GlobalCommandRunner.swSetAnchor(this.sessionId, this.screenId, this.windowId, setVal); @@ -648,6 +663,10 @@ class ScreenWindow { return (this.termLineNumFocus.get() == lineNum); } + getSelectedLine() : number { + return this.selectedLine.get(); + } + getWindow() : Window { return GlobalModel.getWindowById(this.sessionId, this.windowId); } @@ -1612,6 +1631,186 @@ type LineFocusType = { cmdid? : string, }; +class SpecialHistoryViewLineContainer { + historyItem : HistoryItem; + renderer : RendererModel; + + constructor(hitem : HistoryItem) { + this.historyItem = hitem; + } + + loadTerminalRenderer(elem : Element, line : LineType, cmd : Cmd, width : number) : void { + this.unloadRenderer(null); + } + + 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, + windowId: this.historyItem.windowid, + 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; + } + + unloadRenderer(cmdId : string) : void { + if (this.renderer != null) { + this.renderer.dispose(); + this.renderer = null; + } + } + + getUsedRows(line : LineType, cmd : Cmd, width : number) : number { + if (cmd == null) { + return 0; + } + let termOpts = cmd.getTermOpts(); + if (!termOpts.flexrows) { + return termOpts.rows; + } + let termWrap = this.getRenderer(cmd.cmdId); + if (termWrap == null) { + let cols = windowWidthToCols(width, GlobalModel.termFontSize.get()); + let usedRows = GlobalModel.getTUR(this.historyItem.sessionid, cmd.cmdId, cols); + if (usedRows != null) { + return usedRows; + } + if (line.contentheight != null && line.contentheight != -1) { + return line.contentheight; + } + return (cmd.isRunning() ? 1 : 0); + } + return termWrap.getUsedRows(); + } + + getIsFocused(lineNum : number) : boolean { + return false; + } + + getRenderer(cmdId : string) : RendererModel { + return this.renderer; + } + + getFocusType() : "input" | "cmd" | "cmd-fg" { + return "input"; + } + + getSelectedLine() : number { + return null; + } +} + +const HistoryPageSize = 50; + +class HistoryViewModel { + items : OArr = mobx.observable.array([], {name: "HistoryItems"}); + offset : OV = mobx.observable.box(0, {name: "historyview-offset"}); + searchText : OV = mobx.observable.box("", {name: "historyview-searchtext"}); + activeSearchText : string = null; + selectedItems : OMap = mobx.observable.map({}, {name: "historyview-selectedItems"}); + deleteActive : OV = mobx.observable.box(false, {name: "historyview-deleteActive"}); + activeItem : OV = mobx.observable.box(null, {name: "historyview-activeItem"}); + specialLineContainer : SpecialHistoryViewLineContainer; + + constructor() { + } + + closeView() : void { + mobx.action(() => { + GlobalModel.activeMainView.set("session"); + })(); + } + + setActiveItem(historyId : string) { + if (this.activeItem.get() == historyId) { + return; + } + let hitem : HistoryItem = null; + if (historyId != null) { + for (let i=0; i { + if (hitem == null) { + this.activeItem.set(null); + this.specialLineContainer = null; + } + else { + this.activeItem.set(hitem.historyid); + this.specialLineContainer = new SpecialHistoryViewLineContainer(hitem); + } + })(); + } + + doSelectedDelete() : void { + if (!this.deleteActive.get()) { + mobx.action(() => { + this.deleteActive.set(true); + })(); + setTimeout(this.clearActiveDelete, 2000); + return; + } + console.log("DELETE!"); + } + + @boundMethod + clearActiveDelete() : void { + mobx.action(() => { + this.deleteActive.set(false); + })(); + } + + goPrev() : void { + let offset = this.offset.get(); + offset = offset - HistoryPageSize; + if (offset < 0) { + offset = 0; + } + GlobalCommandRunner.historyView({offset: offset, searchText: this.activeSearchText}); + } + + goNext() : void { + let offset = this.offset.get(); + GlobalCommandRunner.historyView({offset: offset+HistoryPageSize, searchText: this.activeSearchText}); + } + + submitSearch() : void { + mobx.action(() => { + this.items.replace([]); + this.offset.set(0); + this.activeSearchText = this.searchText.get(); + })(); + GlobalCommandRunner.historyView({offset: 0, searchText: this.activeSearchText}); + } + + handleDocKeyDown(e : any) : void { + if (e.code == "Escape") { + e.preventDefault(); + this.closeView(); + return; + } + } + + showHistoryView(data : HistoryViewDataType) : void { + mobx.action(() => { + GlobalModel.activeMainView.set("history"); + this.items.replace(data.items || []); + this.offset.set(data.offset); + this.selectedItems.clear(); + })(); + } +} + class BookmarksModel { bookmarks : OArr = mobx.observable.array([], {name: "Bookmarks"}); activeBookmark : OV = mobx.observable.box(null, {name: "activeBookmark"}); @@ -1851,6 +2050,7 @@ class Model { inputModel : InputModel; bookmarksModel : BookmarksModel; + historyViewModel : HistoryViewModel; clientData : OV = mobx.observable.box(null, {name: "clientData"}); constructor() { @@ -1861,6 +2061,7 @@ class Model { this.ws.reconnect(); this.inputModel = new InputModel(); this.bookmarksModel = new BookmarksModel(); + this.historyViewModel = new HistoryViewModel(); let isLocalServerRunning = getApi().getLocalServerStatus(); this.localServerRunning = mobx.observable.box(isLocalServerRunning, {name: "model-local-server-running"}); this.termFontSize = mobx.computed(() => { @@ -1937,6 +2138,10 @@ class Model { this.bookmarksModel.handleDocKeyDown(e); return; } + if (this.activeMainView.get() == "history") { + this.historyViewModel.handleDocKeyDown(e); + return; + } if (e.code == "Escape") { e.preventDefault(); let inputModel = this.inputModel; @@ -2200,8 +2405,19 @@ class Model { } this.updateRemotes(update.remotes); } - if ("bookmarksview" in update) { - this.bookmarksModel.showBookmarksView(update.bookmarks); + if ("mainview" in update) { + if (update.mainview == "bookmarks") { + this.bookmarksModel.showBookmarksView(update.bookmarks); + } + else if (update.mainview == "session") { + this.activeMainView.set("session"); + } + else if (update.mainview == "history") { + this.historyViewModel.showHistoryView(update.historyviewdata); + } + else { + console.log("invalid mainview in update:", update.mainview); + } } else if ("bookmarks" in update) { this.bookmarksModel.mergeBookmarks(update.bookmarks); @@ -2236,6 +2452,27 @@ class Model { return this.getSessionById(this.activeSessionId.get()); } + getSessionNames() : Record { + let rtn : Record = {}; + for (let i=0; i { + let rtn : Record = {}; + for (let i=0; i { + let rtn : Record = {}; + for (let i=0; i