From 49c8b34a7c0f7d45d9b1abfde58e87077c039cee Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 11 Jul 2022 17:55:03 -0700 Subject: [PATCH] checkpoint, moving to model --- src/main.tsx | 93 +++++++++-------- src/model.ts | 274 +++++++++++++++++++++++++++++++++++++++++++++---- src/session.ts | 2 +- src/sh2.ts | 3 + src/term.ts | 131 ++++------------------- src/types.ts | 8 +- 6 files changed, 337 insertions(+), 174 deletions(-) diff --git a/src/main.tsx b/src/main.tsx index 66d2cbd0..1ce98b8d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -9,10 +9,14 @@ import cn from "classnames" import {TermWrap} from "./term"; import type {SessionDataType, LineType, CmdDataType, RemoteType} from "./types"; import localizedFormat from 'dayjs/plugin/localizedFormat'; -import {GlobalMode, Cmd, Window} from "./model"; +import {GlobalModel, Session, Cmd, Window} from "./model"; dayjs.extend(localizedFormat) +function getLineId(line : LineType) : string { + return sprintf("%s-%s-%s", line.sessionid, line.windowid, line.lineid); +} + @mobxReact.observer class LineMeta extends React.Component<{line : LineType}, {}> { render() { @@ -79,10 +83,11 @@ class LineCmd extends React.Component<{line : LineType}, {}> { componentDidMount() { let {line} = this.props; let model = GlobalModel; - let termElem = document.getElementById("term-" + getLineId(line)); - let termWrap = session.getTermWrapByLine(line); - termWrap.changeSizeCallback = this.props.changeSizeCallback; - termWrap.connectToElem(termElem); + let cmd = model.getCmd(line); + if (cmd != null) { + let termElem = document.getElementById("term-" + getLineId(line)); + cmd.connectToElem(termElem); + } if (line.isnew) { setTimeout(() => this.scrollIntoView(), 100); line.isnew = false; @@ -96,9 +101,11 @@ class LineCmd extends React.Component<{line : LineType}, {}> { @boundMethod doRefresh() { - let {session, line} = this.props; - let termWrap = session.getTermWrapByLine(line); - termWrap.reloadTerminal(true, 500); + let model = GlobalModel; + let cmd = model.getCmd(this.props.line); + if (cmd != null) { + cmd.reloadTerminal(true, 500); + } } replaceHomePath(path : string, homeDir : string) : string { @@ -136,40 +143,36 @@ class LineCmd extends React.Component<{line : LineType}, {}> { } } let cwd = "(unknown)"; - if (cmd.remotestate && cmd.remotestate.cwd) { - cwd = cmd.remotestate.cwd; + let remoteState = cmd.getRemoteState(); + if (remoteState && remoteState.cwd) { + cwd = remoteState.cwd; } if (remote.remotevars.home) { cwd = this.replaceHomePath(cwd, remote.remotevars.home) } return (
- [{promptStr} {cwd}] {this.singleLineCmdText(cmd.cmdstr)} + [{promptStr} {cwd}] {cmd.getSingleLineCmdText()}
); } render() { - let {session, line} = this.props; + let {line} = this.props; + let model = GlobalModel; let lineid = line.lineid.toString(); - let running = false; - let detached = false; - let rows = 0; - let cols = 0; - let termWrap = session.getTermWrapByLine(line); - let renderVersion = termWrap.getRenderVersion(); - termWrap.resizeToContent(); - let termSize = termWrap.getSize(); let formattedTime = getLineDateStr(line.ts); - let cellHeightPx = 17; - let totalHeight = cellHeightPx * termWrap.usedRows; - let cmd : CmdDataType = session.getCmd(line.cmdid); - let remote : RemoteType = null; - if (cmd != null) { - remote = session.getRemote(cmd.remoteid); - running = (cmd.status == "running"); - detached = (cmd.status == "detached"); + let cmd = model.getCmd(line); + if (cmd == null) { + return
[cmd not found '{line.cmdid}']
; } + let cellHeightPx = 17; + let totalHeight = cellHeightPx * cmd.usedRows.get(); + let remote = model.getRemote(cmd.remoteId); + let status = cmd.getStatus(); + let running = (status == "running"); + let detached = (status == "detached"); + let termOpts = cmd.getTermOpts(); return (
= 5}, {"running": running}, {"detached": detached})}> @@ -183,12 +186,11 @@ class LineCmd extends React.Component<{line : LineType}, {}> {
{line.cmdid} - 0}>({termSize.rows}x{termSize.cols}) - {termWrap.ptyPos} bytes, v{renderVersion} + ({termOpts.rows}x{termOpts.cols})
{this.renderCmdText(cmd, remote)}
-
+
@@ -201,7 +203,7 @@ class LineCmd extends React.Component<{line : LineType}, {}> { } @mobxReact.observer -class Line extends React.Component<{line : LineType, session : Session, changeSizeCallback? : (term : TermWrap) => void}, {}> { +class Line extends React.Component<{line : LineType, changeSizeCallback? : (term : TermWrap) => void}, {}> { render() { let line = this.props.line; if (line.linetype == "text") { @@ -215,7 +217,7 @@ class Line extends React.Component<{line : LineType, session : Session, changeSi } @mobxReact.observer -class CmdInput extends React.Component<{windowid : string}, {}> { +class CmdInput extends React.Component<{}, {}> { historyIndex : mobx.IObservableValue = mobx.observable.box(0, {name: "history-index"}); modHistory : mobx.IObservableArray = mobx.observable.array([""], {name: "mod-history"}); @@ -223,7 +225,7 @@ class CmdInput extends React.Component<{windowid : string}, {}> { onKeyDown(e : any) { mobx.action(() => { let model = GlobalModel; - let win = getActiveWindow(); + let win = model.getActiveWindow(); let ctrlMod = e.getModifierState("Control") || e.getModifierState("Meta") || e.getModifierState("Shift"); if (e.code == "Enter" && !ctrlMod) { e.preventDefault(); @@ -270,6 +272,10 @@ class CmdInput extends React.Component<{windowid : string}, {}> { if (hidx < this.modHistory.length && this.modHistory[hidx] != null) { return this.modHistory[hidx]; } + let win = model.getActiveWindow(); + if (win == null) { + return ""; + } let hitem = win.getHistoryItem(-hidx); if (hitem == null) { return ""; @@ -292,12 +298,11 @@ class CmdInput extends React.Component<{windowid : string}, {}> { @boundMethod doSubmitCmd() { - let {windowid} = this.props; let model = GlobalModel; let commandStr = this.getCurLine(); let hitem = {cmdtext: commandStr}; this.clearCurLine(); - model.submitCommand(windowid, commandStr); + model.submitCommand(commandStr); } render() { @@ -343,10 +348,9 @@ class SessionView extends React.Component<{}, {}> { @boundMethod changeSizeCallback(term : TermWrap) { if (this.shouldFollow.get()) { - let session = this.props.session; - let window = session.getActiveWindow(); + let window = GlobalModel.getActiveWindow(); let lines = window.lines; - if (lines == null) { + if (lines == null || lines.length == 0) { return; } let lastLine = lines[lines.length-1]; @@ -359,7 +363,7 @@ class SessionView extends React.Component<{}, {}> { let model = GlobalModel; let win = model.getActiveWindow(); if (win == null) { - return
(no active window {session.activeWindowId.get()})
; + return
(no active window)
; } if (!win.linesLoaded.get()) { return
(loading)
; @@ -373,7 +377,7 @@ class SessionView extends React.Component<{}, {}> {
- +
); } @@ -397,6 +401,7 @@ class MainSideBar extends React.Component<{}, {}> { render() { let model = GlobalModel; let curSessionId = model.curSessionId.get(); + let session : Session = null; return (
@@ -410,12 +415,12 @@ class MainSideBar extends React.Component<{}, {}> { Private Sessions

    - +
  • (loading)
  • - + -
  • this.handleSessionClick(session.sessionid)}>#{session.name}
  • +
  • this.handleSessionClick(session.sessionId)}>#{session.name.get()}
  • New Session
  • diff --git a/src/model.ts b/src/model.ts index 4037b4aa..e63cd328 100644 --- a/src/model.ts +++ b/src/model.ts @@ -4,25 +4,71 @@ import {boundMethod} from "autobind-decorator"; import {handleJsonFetchResponse} from "./util"; import {TermWrap} from "./term"; import {v4 as uuidv4} from "uuid"; -import type {SessionDataType, WindowDataType, LineType, RemoteType, HistoryItem, RemoteInstanceType, CmdDataType, FeCmdPacketType} from "./types"; +import type {SessionDataType, WindowDataType, LineType, RemoteType, HistoryItem, RemoteInstanceType, CmdDataType, FeCmdPacketType, TermOptsType, RemoteStateType} from "./types"; import {WSControl} from "./ws"; type OV = mobx.IObservableValue; type OArr = mobx.IObservableArray; +function isBlank(s : string) { + return (s == null || s == ""); +} + class Cmd { + sessionId : string; + windowId : string; + remoteId : string; cmdId : string; data : OV; - terminal : any; + termWrap : TermWrap; ptyPos : number = 0; atRowMax : boolean = false; usedRowsUpdated : () => void = null; watching : boolean = false; + isFocused : OV = mobx.observable.box(false, {name: "focus"}); + usedRows : OV; - constructor(cmd : CmdDataType) { + constructor(cmd : CmdDataType, windowId : string) { + 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"}); + } + } + + connectToElem(elem : Element) { + this.termWrap.connectToElem(elem, { + setFocus: this.setFocus.bind(this), + handleKey: this.handleKey.bind(this), + }); + } + + reloadTerminal(startTail : boolean, delayMs : number) { + if (this.termWrap == null) { + 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); + }); } setCmd(cmd : CmdDataType) { @@ -31,6 +77,40 @@ class Cmd { }); } + getStatus() : string { + return this.data.get().status; + } + + getTermOpts() : TermOptsType { + return this.data.get().termopts; + } + + getCmdStr() : string { + return this.data.get().cmdstr; + } + + getRemoteState() : RemoteStateType { + 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); + GlobalModel.termChangeSize(this.sessionId, this.windowId, this.cmdId, oldUsedRows, tur); + })(); + } + } + getSingleLineCmdText() { let cmdText = this.data.get().cmdstr; if (cmdText == null) { @@ -46,9 +126,46 @@ class Cmd { } return cmdText; } + + isRunning() : boolean { + let data = this.data.get(); + 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()) { + return; + } + let data = this.data.get(); + let inputPacket = { + type: "input", + ck: this.sessionId + "/" + this.cmdId, + inputdata: btoa(event.key), + remoteid: this.remoteId, + }; + GlobalModel.sendInputPacket(inputPacket); + } }; class Window { + sessionId : string; windowId : string; name : OV; curRemote : OV; @@ -56,20 +173,47 @@ class Window { lines : OArr = mobx.observable.array([]); linesLoaded : OV = mobx.observable.box(false); history : any[] = []; + cmds : Record = {}; constructor(wdata : WindowDataType) { + this.sessionId = wdata.sessionid; this.windowId = wdata.windowid; this.name = mobx.observable.box(wdata.name); this.curRemote = mobx.observable.box(wdata.curremote); } - + getNumHistoryItems() : number { return 0; } - getHistoryItem() : any { + getHistoryItem(hnum : number) : any { return null } + + updateWindow(win : WindowDataType, isActive : boolean) { + mobx.action(() => { + if (!isBlank(win.name)) { + this.name.set(win.name) + } + if (!isBlank(win.curremote)) { + this.curRemote.set(win.curremote); + } + if (!isActive) { + return; + } + this.linesLoaded.set(true); + this.lines.replace(win.lines || []); + this.history = win.history || []; + let cmds = win.cmds || []; + for (let i=0; i { + for (let i=0; i = mobx.observable.box(null); sessionListLoaded : OV = mobx.observable.box(false); sessionList : OArr = mobx.observable.array([], {name: "SessionList"}); - cmds : Record = {}; ws : WSControl; + remotes : OArr = mobx.observable.array([], {deep: false}); + remotesLoaded : OV = mobx.observable.box(false); constructor() { this.clientId = uuidv4(); + this.loadRemotes(); this.loadSessionList(); this.ws = new WSControl(this.clientId, this.onMessage.bind(this)) this.ws.reconnect(); @@ -129,12 +298,15 @@ class Model { } getActiveSession() : Session { - let sid = this.curSessionId.get(); - if (sid == null) { + return this.getSessionById(this.curSessionId.get()); + } + + getSessionById(sessionId : string) : Session { + if (sessionId == null) { return null; } for (let i=0; i { - console.log("error getting session list"); + this.errorHandler("getting session list", err); }); } + loadWindow(sessionId : string, windowId : string) { + let usp = new URLSearchParams({sessionid: sessionId, windowid: windowId}); + let url = new URL(sprintf("http://localhost:8080/api/get-window?") + usp.toString()); + fetch(url).then((resp) => handleJsonFetchResponse(url, resp)).then((data) => { + if (data.data == null) { + console.log("null window returned from get-window"); + return; + } + this.updateWindow(data.data); + return; + }).catch((err) => { + this.errorHandler(sprintf("getting window=%s", windowId), err); + }); + } + + loadRemotes() { + let url = new URL("http://localhost:8080/api/get-remotes"); + fetch(url).then((resp) => handleJsonFetchResponse(url, resp)).then((data) => { + mobx.action(() => { + this.remotes.replace(data.data || []) + this.remotesLoaded.set(true); + })(); + }).catch((err) => { + this.errorHandler("calling get-remotes", err) + }); + } + + getRemote(remoteId : string) : RemoteType { + for (let i=0; i", newUsedRows); + } + + errorHandler(str : string, err : any) { + console.log("[error]", str, err); + } + sendInputPacket(inputPacket : any) { this.ws.pushMessage(inputPacket); } @@ -191,7 +428,7 @@ if ((window as any).GlobalModal == null) { } GlobalModel = (window as any).GlobalModel; -export {Model, Window, GlobalModel, Cmd}; +export {Model, Session, Window, GlobalModel, Cmd}; // GlobalWS.registerAndSendGetCmd(getCmdPacket, (dataPacket) => { @@ -228,3 +465,4 @@ reloadTerminal(startTail : boolean, delayMs : number) { return this.cmdStatus == "running" || this.cmdStatus == "detached"; } */ + diff --git a/src/session.ts b/src/session.ts index 7829f7f0..6ac30c98 100644 --- a/src/session.ts +++ b/src/session.ts @@ -156,7 +156,7 @@ class Session { this.loadWindowLines(windowid); } - submitCommand(windowid : string, commandStr : string) { + submitCommand(windowId : string, commandStr : string) { let url = sprintf("http://localhost:8080/api/run-command"); let data : FeCmdPacketType = {type: "fecmd", sessionid: this.sessionId, windowid: windowid, cmdstr: commandStr, userid: GlobalUser, remotestate: null}; let curWindow = this.getCurWindow(); diff --git a/src/sh2.ts b/src/sh2.ts index c59c4ebf..90a808fe 100644 --- a/src/sh2.ts +++ b/src/sh2.ts @@ -1,3 +1,4 @@ +import * as mobx from "mobx"; import * as React from "react"; import {createRoot} from 'react-dom/client'; import {sprintf} from "sprintf-js"; @@ -17,4 +18,6 @@ document.addEventListener("DOMContentLoaded", () => { root.render(reactElem); }); +(window as any).mobx = mobx; + console.log("SCRIPTHAUS", VERSION) diff --git a/src/term.ts b/src/term.ts index 063fa10f..d8349d9a 100644 --- a/src/term.ts +++ b/src/term.ts @@ -4,6 +4,7 @@ import {sprintf} from "sprintf-js"; import {boundMethod} from "autobind-decorator"; import {v4 as uuidv4} from "uuid"; import {GlobalModel} from "./model"; +import type {TermOptsType} from "./types"; function loadPtyOut(term : Terminal, sessionId : string, cmdId : string, delayMs : number, callback?: (number) => void) { term.clear() @@ -18,86 +19,35 @@ function loadPtyOut(term : Terminal, sessionId : string, cmdId : string, delayMs }); } +type TermEventHandler = { + setFocus : (focus : boolean) => void, + handleKey : (event : any) => void, +}; + class TermWrap { terminal : any; - termId : string; - sessionId : string; - cmdId : string; - ptyPos : number = 0; - runPos : number = 0; - runData : string = ""; - renderVersion : mobx.IObservableValue = mobx.observable.box(1, {name: "renderVersion"}); - isFocused : mobx.IObservableValue = mobx.observable.box(false, {name: "focus"}); - flexRows : boolean = true; - maxRows : number = 25; - atRowMax : boolean = false; - initialized : boolean = false; - changeSizeCallback : (TermWrap) => void = null; usedRows : number; + flexRows : boolean; + tailReqId : string; cmdStatus : string; remoteId : string; - constructor(sessionId : string, cmdId : string, remoteId : string, status : string) { - this.termId = uuidv4(); - this.sessionId = sessionId; - this.cmdId = cmdId; - this.remoteId = remoteId; - this.cmdStatus = status; - this.terminal = new Terminal({rows: 25, cols: 80, theme: {foreground: "#d3d7cf"}}); + constructor(termOpts : TermOptsType) { + this.terminal = new Terminal({rows: termOpts.rows, cols: termOpts.cols, theme: {foreground: "#d3d7cf"}}); + this.flexRows = termOpts.flexrows; this.usedRows = 2; } - destroy() { - } - - isRunning() : boolean { - return this.cmdStatus == "running" || this.cmdStatus == "detached"; - } - - @boundMethod - onKeyHandler(event : any) { - console.log("onkey", event); - if (!this.isRunning()) { - return; - } - let inputPacket = { - type: "input", - ck: this.sessionId + "/" + this.cmdId, - inputdata: btoa(event.key), - remoteid: this.remoteId, - }; - GlobalModel.sendInputPacket(inputPacket); - } - - // datalen is passed because data could be utf-8 and data.length is not the actual *byte* length - updatePtyData(pos : number, data : string, 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.terminal.write(data, () => { - mobx.action(() => { - this.resizeToContent(); - this.incRenderVersion(); - })(); - }); - } - - resizeToContent() { - if (this.atRowMax) { - return; - } + getTermUsedRows() : number { let term = this.terminal; let termBuf = term._core.buffer; let termNumLines = termBuf.lines.length; let termYPos = termBuf.y; - let usedRows = 2; - if (termNumLines > term.rows) { - this.usedRows = term.rows; - this.atRowMax = true; - return; + if (termNumLines >= term.rows) { + return term.rows; } + let usedRows = 2; if (termYPos >= usedRows) { usedRows = termYPos + 1; } @@ -107,58 +57,19 @@ class TermWrap { usedRows = i+1; } } - if (this.usedRows != usedRows) { - this.usedRows = usedRows; - if (this.changeSizeCallback != null) { - let cb = this.changeSizeCallback; - setTimeout(() => cb(this), 0); - } - } - return; + return usedRows; } - setSize(rows : number, cols : number, flexRows : boolean) { - this.flexRows = true; - this.maxRows = rows; - if (!flexRows) { - this.terminal.resize(rows, cols); - setTimeout(() => this.incRenderVersion(), 10); - return; - } - this.resizeToContent(); - } - - getSize() : {rows : number, cols : number} { - return {rows: this.terminal.rows, cols: this.terminal.cols}; - } - - @boundMethod - setFocus(val : boolean) { - mobx.action(() => this.isFocused.set(val))(); - } - - getRenderVersion() : number { - return this.renderVersion.get(); - } - - @boundMethod - incRenderVersion() { - mobx.action(() => this.renderVersion.set(this.renderVersion.get() + 1))(); - } - - connectToElem(elem : Element) { + connectToElem(elem : Element, eventHandler : TermEventHandler) { this.terminal.open(elem); - if (this.isRunning()) { + if (eventHandler != null) { this.terminal.textarea.addEventListener("focus", () => { - this.setFocus(true); + eventHandler.setFocus(true); }); this.terminal.textarea.addEventListener("blur", () => { - this.setFocus(false); + eventHandler.setFocus(false); }); - this.terminal.onKey(this.onKeyHandler); - } - else { - this.terminal.onKey(this.onKeyHandler); + this.terminal.onKey(eventHandler.handleKey); } } } diff --git a/src/types.ts b/src/types.ts index 37a42ed2..b2efd11c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,6 +5,7 @@ type SessionDataType = { name : string, windows : WindowDataType[], cmds : CmdDataType[], + remove : boolean, }; type LineType = { @@ -40,6 +41,7 @@ type RemoteInstanceType = { remoteid : string, sessionscope : boolean, state : RemoteStateType, + version : number, } type WindowDataType = { @@ -48,11 +50,15 @@ type WindowDataType = { name : string, curremote : string, lines : LineType[], + history : HistoryItem[], + cmds : CmdDataType[], + remotes : RemoteInstanceType[], version : number, + remove : boolean, }; type HistoryItem = { - cmdtext : string, + cmdstr : string, }; type CmdRemoteStateType = {