From f4b3f94b137b086427490beb004b7bb571774646 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 13 Jul 2022 00:44:19 -0700 Subject: [PATCH] add cmdinstances to allow windows/cmds to show in multiple screenwindows --- src/main.tsx | 36 +++++++----- src/model.ts | 143 ++++++++++++++------------------------------- src/term.ts | 162 ++++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 207 insertions(+), 134 deletions(-) diff --git a/src/main.tsx b/src/main.tsx index 1ce310d5..eb448a23 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -51,7 +51,7 @@ function getLineDateStr(ts : number) : string { } @mobxReact.observer -class LineText extends React.Component<{line : LineType}, {}> { +class LineText extends React.Component<{sw : ScreenWindow, line : LineType}, {}> { render() { let line = this.props.line; let formattedTime = getLineDateStr(line.ts); @@ -75,27 +75,30 @@ class LineText extends React.Component<{line : LineType}, {}> { } @mobxReact.observer -class LineCmd extends React.Component<{line : LineType}, {}> { +class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType}, {}> { + termLoaded : mobx.IObservableValue = mobx.observable.box(false); + constructor(props) { super(props); } componentDidMount() { - let {line} = this.props; + let {sw, line} = this.props; let model = GlobalModel; let cmd = model.getCmd(line); if (cmd != null) { let termElem = document.getElementById("term-" + getLineId(line)); - cmd.connectElem(termElem); + cmd.connectElem(termElem, sw.screenId, sw.windowId); + mobx.action(() => this.termLoaded.set(true))(); } } componentWillUnmount() { - let {line} = this.props; + let {sw, line} = this.props; let model = GlobalModel; let cmd = model.getCmd(line); if (cmd != null) { - cmd.disconnectElem(); + cmd.disconnectElem(sw.screenId, sw.windowId); } } @@ -106,10 +109,14 @@ class LineCmd extends React.Component<{line : LineType}, {}> { @boundMethod doRefresh() { + let {sw, line} = this.props; let model = GlobalModel; - let cmd = model.getCmd(this.props.line); + let cmd = model.getCmd(line); if (cmd != null) { - cmd.reloadTerminal(500); + let termWrap = cmd.getTermWrap(sw.screenId, sw.windowId); + if (termWrap != null) { + termWrap.reloadTerminal(500); + } } } @@ -163,7 +170,7 @@ class LineCmd extends React.Component<{line : LineType}, {}> { } render() { - let {line} = this.props; + let {sw, line} = this.props; let model = GlobalModel; let lineid = line.lineid.toString(); let formattedTime = getLineDateStr(line.ts); @@ -171,13 +178,16 @@ class LineCmd extends React.Component<{line : LineType}, {}> { if (cmd == null) { return
[cmd not found '{line.cmdid}']
; } + let termLoaded = this.termLoaded.get(); let cellHeightPx = 17; - let totalHeight = cellHeightPx * cmd.usedRows.get(); + let usedRows = cmd.getUsedRows(sw.screenId, sw.windowId); + let totalHeight = cellHeightPx * usedRows; let remote = model.getRemote(cmd.remoteId); let status = cmd.getStatus(); let running = (status == "running"); let detached = (status == "detached"); let termOpts = cmd.getTermOpts(); + let isFocused = cmd.getIsFocused(sw.screenId, sw.windowId); return (
= 5}, {"running": running}, {"detached": detached})}> @@ -195,7 +205,7 @@ class LineCmd extends React.Component<{line : LineType}, {}> {
{this.renderCmdText(cmd, remote)}
-
+
@@ -208,7 +218,7 @@ class LineCmd extends React.Component<{line : LineType}, {}> { } @mobxReact.observer -class Line extends React.Component<{line : LineType}, {}> { +class Line extends React.Component<{sw : ScreenWindow, line : LineType}, {}> { render() { let line = this.props.line; if (line.linetype == "text") { @@ -429,7 +439,7 @@ class ScreenWindowView extends React.Component<{sw : ScreenWindow}, {}> {
- +
diff --git a/src/model.ts b/src/model.ts index c3e139b8..1c50caa1 100644 --- a/src/model.ts +++ b/src/model.ts @@ -36,72 +36,61 @@ function ces(s : string) { class Cmd { sessionId : string; - windowId : string; remoteId : string; cmdId : string; data : OV; - - termWrap : TermWrap; - ptyPos : number = 0; - atRowMax : boolean = false; - usedRowsUpdated : () => void = null; watching : boolean = false; - isFocused : OV = mobx.observable.box(false, {name: "focus"}); - usedRows : OV; - connectedElem : Element; + instances : Record = {}; - constructor(cmd : CmdDataType, windowId : string) { + constructor(cmd : CmdDataType) { this.sessionId = cmd.sessionid; - this.windowId = windowId; this.cmdId = cmd.cmdid; this.remoteId = cmd.remoteid; this.data = mobx.observable.box(cmd, {deep: false}); - if (cmd.termopts.flexrows) { - this.atRowMax = false; - this.usedRows = mobx.observable.box(2, {name: "usedRows"}); - } - else { - this.atRowMax = true; - this.usedRows = mobx.observable.box(cmd.termopts.rows, {name: "usedRows"}); - } } - disconnectElem() { - this.connectedElem = null; - } - - connectElem(elem : Element) { - if (this.connectedElem != null) { - console.log("WARNING element already connected to cmd", this.cmdId, this.connectedElem); - } - this.connectedElem = elem; - if (this.termWrap == null) { - this.termWrap = new TermWrap(this.getTermOpts()); - this.reloadTerminal(0); - } - this.termWrap.connectToElem(elem, { - setFocus: this.setFocus.bind(this), - handleKey: this.handleKey.bind(this), - }); - } - - reloadTerminal(delayMs : number) { - if (this.termWrap == null) { + connectElem(elem : Element, screenId : string, windowId : string) { + let termWrap = this.getTermWrap(screenId, windowId); + if (termWrap != null) { + console.log("term-wrap already exists for", screenId, windowId); return; } - this.termWrap.terminal.clear(); - let url = sprintf("http://localhost:8080/api/ptyout?sessionid=%s&cmdid=%s", this.sessionId, this.cmdId); - fetch(url).then((resp) => { - if (!resp.ok) { - throw new Error(sprintf("Bad fetch response for /api/ptyout: %d %s", resp.status, resp.statusText)); - } - return resp.arrayBuffer() - }).then((buf) => { - setTimeout(() => { - this.ptyPos = 0; - this.updatePtyData(0, new Uint8Array(buf), buf.byteLength); - }, delayMs); - }); + termWrap = new TermWrap(elem, this.sessionId, this.cmdId, 0, this.getTermOpts(), this.handleKey); + this.instances[screenId + "/" + windowId] = termWrap; + return; + } + + disconnectElem(screenId : string, windowId : string) { + let key = screenId + "/" + windowId; + let termWrap = this.instances[key]; + if (termWrap != null) { + termWrap.dispose(); + delete this.instances[key]; + } + } + + getTermWrap(screenId : string, windowId : string) : TermWrap { + return this.instances[screenId + "/" + windowId]; + } + + getUsedRows(screenId : string, windowId : string) : number { + let termOpts = this.getTermOpts(); + if (!termOpts.flexrows) { + return termOpts.rows; + } + let termWrap = this.getTermWrap(screenId, windowId); + if (termWrap == null) { + return 2; + } + return termWrap.usedRows.get(); + } + + getIsFocused(screenId : string, windowId : string) : boolean { + let termWrap = this.getTermWrap(screenId, windowId); + if (termWrap == null) { + return false; + } + return termWrap.isFocused.get(); } setCmd(cmd : CmdDataType) { @@ -126,34 +115,6 @@ class Cmd { return this.data.get().remotestate; } - updateUsedRows() { - if (this.atRowMax) { - return; - } - let tur = this.termWrap.getTermUsedRows(); - if (tur >= this.termWrap.terminal.rows) { - this.atRowMax = true; - } - if (tur > this.usedRows.get()) { - mobx.action(() => { - let data = this.data.get(); - let oldUsedRows = this.usedRows.get(); - this.usedRows.set(tur); - if (this.connectedElem) { - let resizeEvent = new CustomEvent("termresize", { - bubbles: true, - detail: { - cmdId: this.cmdId, - oldUsedRows: oldUsedRows, - newUsedRows: tur, - }, - }); - this.connectedElem.dispatchEvent(resizeEvent); - } - })(); - } - } - getSingleLineCmdText() { let cmdText = this.data.get().cmdstr; if (cmdText == null) { @@ -175,22 +136,6 @@ class Cmd { return data.status == "running" || data.status == "detached"; } - updatePtyData(pos : number, data : string | Uint8Array, datalen : number) { - if (pos != this.ptyPos) { - throw new Error(sprintf("invalid pty-update, data-pos[%d] does not match term-pos[%d]", pos, this.ptyPos)); - } - this.ptyPos += datalen; - this.termWrap.terminal.write(data, () => { - this.updateUsedRows(); - }); - } - - setFocus(focus : boolean) { - mobx.action(() => { - this.isFocused.set(focus); - })(); - } - handleKey(event : any) { console.log("onkey", event); if (!this.isRunning()) { @@ -315,7 +260,7 @@ class Window { this.history = win.history || []; let cmds = win.cmds || []; for (let i=0; i { if (cmd != null) { - this.cmds[cmd.cmdid] = new Cmd(cmd, this.windowId); + this.cmds[cmd.cmdid] = new Cmd(cmd); } let lines = this.lines; let lineIdx = 0; diff --git a/src/term.ts b/src/term.ts index 8f159ca1..3ce58ca9 100644 --- a/src/term.ts +++ b/src/term.ts @@ -19,28 +19,75 @@ function loadPtyOut(term : Terminal, sessionId : string, cmdId : string, delayMs }); } -type TermEventHandler = { - setFocus : (focus : boolean) => void, - handleKey : (event : any) => void, -}; +type DataUpdate = { + data : Uint8Array, + pos : number, +} +// cmd-instance class TermWrap { terminal : any; - usedRows : number; + sessionId : string; + cmdId : string; + atRowMax : boolean; + usedRows : mobx.IObservableValue; + isFocused : mobx.IObservableValue = mobx.observable.box(false, {name: "focus"}); flexRows : boolean; - - tailReqId : string; - cmdStatus : string; - remoteId : string; + connectedElem : Element; + ptyPos : number = 0; + reloading : boolean = false; + dataUpdates : DataUpdate[] = []; + loadError : mobx.IObservableValue = mobx.observable.box(false); - constructor(termOpts : TermOptsType) { + constructor(elem : Element, sessionId : string, cmdId : string, usedRows : number, termOpts : TermOptsType, keyHandler : (event : any) => void) { + this.sessionId = sessionId; + this.cmdId = cmdId; + this.connectedElem = elem; + this.flexRows = termOpts.flexrows ?? false; + if (this.flexRows) { + this.atRowMax = false; + this.usedRows = mobx.observable.box(usedRows || 2); + } + else { + this.atRowMax = true; + this.usedRows = mobx.observable.box(termOpts.rows); + } this.terminal = new Terminal({rows: termOpts.rows, cols: termOpts.cols, theme: {foreground: "#d3d7cf"}}); - this.flexRows = termOpts.flexrows; - this.usedRows = 2; + this.terminal.open(elem); + if (keyHandler != null) { + this.terminal.onKey(keyHandler); + } + this.terminal.textarea.addEventListener("focus", () => { + this.setFocus(true); + }); + this.terminal.textarea.addEventListener("blur", () => { + this.setFocus(false); + }); + this.reloadTerminal(0); + } + + dispose() { + if (this.terminal != null) { + this.terminal.dispose(); + this.terminal = null; + } + } + + disconnectElem() { + this.connectedElem = null; + } + + setFocus(focus : boolean) { + mobx.action(() => { + this.isFocused.set(focus); + })(); } getTermUsedRows() : number { let term = this.terminal; + if (term == null) { + return 0; + } let termBuf = term._core.buffer; let termNumLines = termBuf.lines.length; let termYPos = termBuf.y; @@ -60,17 +107,88 @@ class TermWrap { return usedRows; } - connectToElem(elem : Element, eventHandler : TermEventHandler) { - this.terminal.open(elem); - if (eventHandler != null) { - this.terminal.textarea.addEventListener("focus", () => { - eventHandler.setFocus(true); - }); - this.terminal.textarea.addEventListener("blur", () => { - eventHandler.setFocus(false); - }); - this.terminal.onKey(eventHandler.handleKey); + updateUsedRows() { + if (this.terminal == null) { + return; } + if (this.atRowMax) { + return; + } + let tur = this.getTermUsedRows(); + if (tur >= this.terminal.rows) { + this.atRowMax = true; + } + if (tur <= this.usedRows.get()) { + return; + } + mobx.action(() => { + let oldUsedRows = this.usedRows.get(); + this.usedRows.set(tur); + if (this.connectedElem) { + let resizeEvent = new CustomEvent("termresize", { + bubbles: true, + detail: { + cmdId: this.cmdId, + oldUsedRows: oldUsedRows, + newUsedRows: tur, + }, + }); + this.connectedElem.dispatchEvent(resizeEvent); + } + })(); + } + + reloadTerminal(delayMs : number) { + if (this.terminal == null) { + return; + } + this.reloading = true; + this.terminal.clear(); + let url = sprintf("http://localhost:8080/api/ptyout?sessionid=%s&cmdid=%s", this.sessionId, this.cmdId); + fetch(url).then((resp) => { + if (!resp.ok) { + mobx.action(() => { this.loadError.set(true); })(); + this.dataUpdates = []; + throw new Error(sprintf("Bad fetch response for /api/ptyout: %d %s", resp.status, resp.statusText)); + } + return resp.arrayBuffer() + }).then((buf) => { + setTimeout(() => { + this.reloading = false; + this.ptyPos = 0; + this.updatePtyData(0, new Uint8Array(buf)); + for (let i=0; i this.ptyPos) { + throw new Error(sprintf("invalid pty-update, data-pos[%d] does not match term-pos[%d]", pos, this.ptyPos)); + } + if (pos < this.ptyPos) { + let diff = this.ptyPos - pos; + if (diff >= data.length) { + // already contains all the data + return; + } + data = data.slice(diff); + pos += diff; + } + this.ptyPos += data.length; + this.terminal.write(data, () => { + this.updateUsedRows(); + }); } }