add cmdinstances to allow windows/cmds to show in multiple screenwindows

This commit is contained in:
sawka
2022-07-13 00:44:19 -07:00
parent 059c98d1e0
commit f4b3f94b13
3 changed files with 207 additions and 134 deletions
+23 -13
View File
@@ -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<boolean> = 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 <div className="line line-invalid">[cmd not found '{line.cmdid}']</div>;
}
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 (
<div className="line line-cmd" id={"line-" + getLineId(line)}>
<div className={cn("avatar",{"num4": lineid.length == 4}, {"num5": lineid.length >= 5}, {"running": running}, {"detached": detached})}>
@@ -195,7 +205,7 @@ class LineCmd extends React.Component<{line : LineType}, {}> {
</div>
{this.renderCmdText(cmd, remote)}
</div>
<div className={cn("terminal-wrapper", {"focus": cmd.isFocused.get()})} style={{overflowY: "hidden"}}>
<div className={cn("terminal-wrapper", {"focus": isFocused})} style={{overflowY: "hidden"}}>
<div className="terminal" id={"term-" + getLineId(line)} data-cmdid={line.cmdid} style={{height: totalHeight}}></div>
</div>
</div>
@@ -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}, {}> {
<div className="window-view" style={this.getWindowViewStyle()}>
<div className="lines" onScroll={this.scrollHandler} id={this.getLinesId()}>
<For each="line" of={win.lines} index="idx">
<Line key={line.lineid} line={line}/>
<Line key={line.lineid} line={line} sw={sw}/>
</For>
</div>
</div>
+44 -99
View File
@@ -36,72 +36,61 @@ function ces(s : string) {
class Cmd {
sessionId : string;
windowId : string;
remoteId : string;
cmdId : string;
data : OV<CmdDataType>;
termWrap : TermWrap;
ptyPos : number = 0;
atRowMax : boolean = false;
usedRowsUpdated : () => void = null;
watching : boolean = false;
isFocused : OV<boolean> = mobx.observable.box(false, {name: "focus"});
usedRows : OV<number>;
connectedElem : Element;
instances : Record<string, TermWrap> = {};
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<cmds.length; i++) {
this.cmds[cmds[i].cmdid] = new Cmd(cmds[i], this.windowId);
this.cmds[cmds[i].cmdid] = new Cmd(cmds[i]);
}
})();
}
@@ -360,7 +305,7 @@ class Window {
}
mobx.action(() => {
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;
+140 -22
View File
@@ -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<number>;
isFocused : mobx.IObservableValue<boolean> = 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<boolean> = 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.dataUpdates.length; i++) {
this.updatePtyData(this.dataUpdates[i].pos, this.dataUpdates[i].data);
}
this.dataUpdates = [];
}, delayMs);
});
}
updatePtyData(pos : number, data : Uint8Array) {
if (this.loadError.get()) {
return;
}
if (this.reloading) {
this.dataUpdates.push({data: data, pos: pos});
return;
}
if (pos > 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();
});
}
}