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 = {