2022-06-08 02:25:35 +02:00
|
|
|
import * as React from "react";
|
|
|
|
import * as mobxReact from "mobx-react";
|
|
|
|
import * as mobx from "mobx";
|
|
|
|
import {Terminal} from 'xterm';
|
|
|
|
import {sprintf} from "sprintf-js";
|
|
|
|
import {boundMethod} from "autobind-decorator";
|
|
|
|
import * as dayjs from 'dayjs'
|
|
|
|
import {If, For, When, Otherwise, Choose} from "tsx-control-statements/components";
|
|
|
|
import cn from "classnames"
|
|
|
|
|
|
|
|
type LineType = {
|
|
|
|
lineid : number,
|
|
|
|
ts : number,
|
|
|
|
userid : string,
|
|
|
|
linetype : string,
|
|
|
|
text : string,
|
|
|
|
cmdid : string,
|
|
|
|
cmdtext : string,
|
2022-06-13 20:12:39 +02:00
|
|
|
isnew : boolean,
|
2022-06-08 02:25:35 +02:00
|
|
|
};
|
|
|
|
|
2022-06-13 20:12:39 +02:00
|
|
|
var GlobalLines = mobx.observable.box([
|
|
|
|
{lineid: 1, userid: "sawka", ts: 1654631122000, linetype: "text", text: "hello"},
|
|
|
|
{lineid: 2, userid: "sawka", ts: 1654631125000, linetype: "text", text: "again"},
|
|
|
|
]);
|
|
|
|
|
2022-06-14 23:16:32 +02:00
|
|
|
var TermMap = {};
|
|
|
|
window.TermMap = TermMap;
|
|
|
|
|
2022-06-13 20:12:39 +02:00
|
|
|
function fetchJsonData(resp : any, ctErr : boolean) : Promise<any> {
|
|
|
|
let contentType = resp.headers.get("Content-Type");
|
|
|
|
if (contentType != null && contentType.startsWith("application/json")) {
|
|
|
|
return resp.text().then((textData) => {
|
|
|
|
try {
|
|
|
|
return JSON.parse(textData);
|
|
|
|
}
|
|
|
|
catch (err) {
|
|
|
|
let errMsg = sprintf("Unparseable JSON: " + err.message);
|
|
|
|
let rtnErr = new Error(errMsg);
|
|
|
|
throw rtnErr;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
if (ctErr) {
|
|
|
|
throw new Error("non-json content-type");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function handleJsonFetchResponse(url : URL, resp : any) : Promise<any> {
|
|
|
|
if (!resp.ok) {
|
|
|
|
let errData = fetchJsonData(resp, false);
|
|
|
|
if (errData && errData["error"]) {
|
|
|
|
throw new Error(errData["error"])
|
|
|
|
}
|
|
|
|
let errMsg = sprintf("Bad status code response from fetch '%s': %d %s", url.toString(), resp.status, resp.statusText);
|
|
|
|
let rtnErr = new Error(errMsg);
|
|
|
|
throw rtnErr;
|
|
|
|
}
|
|
|
|
let rtnData = fetchJsonData(resp, true);
|
|
|
|
return rtnData;
|
|
|
|
}
|
|
|
|
|
2022-06-08 02:25:35 +02:00
|
|
|
@mobxReact.observer
|
|
|
|
class LineMeta extends React.Component<{line : LineType}, {}> {
|
|
|
|
render() {
|
|
|
|
let line = this.props.line;
|
|
|
|
return (
|
|
|
|
<div className="meta">
|
|
|
|
<div className="lineid">{line.lineid}</div>
|
|
|
|
<div className="user">{line.userid}</div>
|
|
|
|
<div className="ts">{dayjs(line.ts).format("hh:mm:ss a")}</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@mobxReact.observer
|
|
|
|
class LineText extends React.Component<{line : LineType}, {}> {
|
|
|
|
render() {
|
|
|
|
let line = this.props.line;
|
|
|
|
return (
|
|
|
|
<div className="line line-text">
|
|
|
|
<div className="avatar">
|
|
|
|
M
|
|
|
|
</div>
|
|
|
|
<div className="line-content">
|
|
|
|
<div className="meta">
|
|
|
|
<div className="user">{line.userid}</div>
|
|
|
|
<div className="ts">{dayjs(line.ts).format("hh:mm:ss a")}</div>
|
|
|
|
</div>
|
|
|
|
<div className="text">
|
|
|
|
{line.text}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-14 23:16:32 +02:00
|
|
|
function loadPtyOut(term : Terminal, sessionId : string, cmdId : string, callback?: () => void) {
|
2022-06-13 20:12:39 +02:00
|
|
|
term.clear()
|
2022-06-08 02:25:35 +02:00
|
|
|
let url = sprintf("http://localhost:8080/api/ptyout?sessionid=%s&cmdid=%s", sessionId, 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.text()
|
|
|
|
}).then((resptext) => {
|
2022-06-14 23:16:32 +02:00
|
|
|
setTimeout(() => term.write(resptext, callback), 0);
|
2022-06-08 02:25:35 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
@mobxReact.observer
|
|
|
|
class LineCmd extends React.Component<{line : LineType}, {}> {
|
2022-06-14 23:16:32 +02:00
|
|
|
terminal : mobx.IObservableValue<Terminal> = mobx.observable.box(null, {name: "terminal"});
|
2022-06-08 02:25:35 +02:00
|
|
|
focus : mobx.IObservableValue<boolean> = mobx.observable.box(false, {name: "focus"});
|
2022-06-14 23:16:32 +02:00
|
|
|
version : mobx.IObservableValue<int> = mobx.observable.box(0, {name: "lineversion"});
|
2022-06-08 02:25:35 +02:00
|
|
|
|
|
|
|
componentDidMount() {
|
|
|
|
let {line, sessionid} = this.props;
|
2022-06-15 01:02:20 +02:00
|
|
|
let terminal = new Terminal({rows: 2, cols: 80});
|
2022-06-14 23:16:32 +02:00
|
|
|
TermMap[line.cmdid] = terminal;
|
2022-06-13 20:12:39 +02:00
|
|
|
let termElem = document.getElementById(this.getId());
|
2022-06-14 23:16:32 +02:00
|
|
|
terminal.open(termElem);
|
2022-06-15 01:02:20 +02:00
|
|
|
mobx.action(() => this.terminal.set(terminal))();
|
|
|
|
this.reloadTerminal();
|
2022-06-14 23:16:32 +02:00
|
|
|
terminal.textarea.addEventListener("focus", () => {
|
2022-06-08 02:25:35 +02:00
|
|
|
mobx.action(() => {
|
|
|
|
this.focus.set(true);
|
|
|
|
})();
|
|
|
|
});
|
2022-06-14 23:16:32 +02:00
|
|
|
terminal.textarea.addEventListener("blur", () => {
|
2022-06-08 02:25:35 +02:00
|
|
|
mobx.action(() => {
|
|
|
|
this.focus.set(false);
|
|
|
|
})();
|
|
|
|
});
|
2022-06-13 20:12:39 +02:00
|
|
|
if (line.isnew) {
|
|
|
|
setTimeout(() => {
|
|
|
|
let lineElem = document.getElementById("line-" + this.getId());
|
|
|
|
lineElem.scrollIntoView({block: "end"});
|
|
|
|
mobx.action(() => {
|
|
|
|
line.isnew = false;
|
|
|
|
})();
|
|
|
|
}, 100);
|
|
|
|
setTimeout(() => {
|
2022-06-15 01:02:20 +02:00
|
|
|
this.reloadTerminal();
|
2022-06-13 20:12:39 +02:00
|
|
|
}, 1000);
|
|
|
|
}
|
2022-06-15 01:02:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
reloadTerminal() {
|
|
|
|
let {line, sessionid} = this.props;
|
|
|
|
let terminal = this.terminal.get();
|
|
|
|
loadPtyOut(terminal, sessionid, line.cmdid, this.incVersion);
|
2022-06-14 23:16:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@boundMethod
|
|
|
|
incVersion() : void {
|
|
|
|
mobx.action(() => this.version.set(this.version.get() + 1))();
|
2022-06-08 02:25:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
getId() : string {
|
|
|
|
let {line} = this.props;
|
|
|
|
return "cmd-" + line.lineid + "-" + line.cmdid;
|
|
|
|
}
|
2022-06-13 20:12:39 +02:00
|
|
|
|
|
|
|
@boundMethod
|
|
|
|
doRefresh() {
|
2022-06-15 01:02:20 +02:00
|
|
|
this.reloadTerminal();
|
|
|
|
}
|
|
|
|
|
|
|
|
@boundMethod
|
|
|
|
singleLineCmdText(cmdText : string) {
|
|
|
|
if (cmdText == null) {
|
|
|
|
return "(none)";
|
|
|
|
}
|
|
|
|
cmdText = cmdText.trim();
|
|
|
|
let nlIdx = cmdText.indexOf("\n");
|
|
|
|
if (nlIdx != -1) {
|
|
|
|
cmdText = cmdText.substr(0, nlIdx) + "...";
|
|
|
|
}
|
|
|
|
if (cmdText.length > 80) {
|
|
|
|
cmdText = cmdText.substr(0, 77) + "...";
|
|
|
|
}
|
|
|
|
return cmdText;
|
2022-06-13 20:12:39 +02:00
|
|
|
}
|
2022-06-08 02:25:35 +02:00
|
|
|
|
|
|
|
render() {
|
|
|
|
let {line} = this.props;
|
|
|
|
let lineid = line.lineid.toString();
|
|
|
|
let running = false;
|
2022-06-14 23:16:32 +02:00
|
|
|
let term = this.terminal.get();
|
|
|
|
let version = this.version.get();
|
2022-06-15 01:02:20 +02:00
|
|
|
let rows = 0;
|
|
|
|
let cols = 0;
|
2022-06-14 23:16:32 +02:00
|
|
|
if (term != null) {
|
2022-06-15 01:02:20 +02:00
|
|
|
let termNumLines = term._core.buffer.lines.length;
|
|
|
|
let termYPos = term._core.buffer.y;
|
|
|
|
if (term.rows < 25 && termNumLines > term.rows) {
|
|
|
|
term.resize(80, Math.min(25, termNumLines));
|
|
|
|
} else if (term.rows < 25 && termYPos >= term.rows) {
|
|
|
|
term.resize(80, Math.min(25, termYPos+1));
|
|
|
|
}
|
|
|
|
rows = term.rows;
|
|
|
|
cols = term.cols;
|
2022-06-14 23:16:32 +02:00
|
|
|
}
|
2022-06-08 02:25:35 +02:00
|
|
|
return (
|
2022-06-13 20:12:39 +02:00
|
|
|
<div className="line line-cmd" id={"line-" + this.getId()}>
|
2022-06-08 02:25:35 +02:00
|
|
|
<div className={cn("avatar",{"num4": lineid.length == 4}, {"num5": lineid.length >= 5}, {"running": running})}>
|
|
|
|
{lineid}
|
|
|
|
</div>
|
|
|
|
<div className="line-content">
|
|
|
|
<div className="meta">
|
|
|
|
<div className="user">{line.userid}</div>
|
|
|
|
<div className="ts">{dayjs(line.ts).format("hh:mm:ss a")}</div>
|
2022-06-15 01:02:20 +02:00
|
|
|
<div className="cmdid">{line.cmdid} <If condition={rows > 0}>({rows}x{cols})</If> v{version}</div>
|
|
|
|
<div className="cmdtext">> {this.singleLineCmdText(line.cmdtext)}</div>
|
2022-06-08 02:25:35 +02:00
|
|
|
</div>
|
|
|
|
<div className={cn("terminal-wrapper", {"focus": this.focus.get()})}>
|
|
|
|
<div className="terminal" id={this.getId()}></div>
|
|
|
|
</div>
|
|
|
|
</div>
|
2022-06-13 20:12:39 +02:00
|
|
|
<div>
|
|
|
|
<div onClick={this.doRefresh} className="button">Refresh</div>
|
|
|
|
</div>
|
2022-06-08 02:25:35 +02:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@mobxReact.observer
|
|
|
|
class Line extends React.Component<{line : LineType}, {}> {
|
|
|
|
render() {
|
|
|
|
let line = this.props.line;
|
|
|
|
if (line.linetype == "text") {
|
|
|
|
return <LineText {...this.props}/>;
|
|
|
|
}
|
|
|
|
if (line.linetype == "cmd") {
|
|
|
|
return <LineCmd {...this.props}/>;
|
|
|
|
}
|
|
|
|
return <div className="line line-invalid">[invalid line type '{line.linetype}']</div>;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@mobxReact.observer
|
2022-06-13 20:12:39 +02:00
|
|
|
class CmdInput extends React.Component<{line : LineType, sessionid : string}, {}> {
|
2022-06-08 02:25:35 +02:00
|
|
|
curLine : mobx.IObservableValue<string> = mobx.observable("", {name: "command-line"});
|
|
|
|
|
|
|
|
@mobx.action @boundMethod
|
|
|
|
onKeyDown(e : any) {
|
|
|
|
mobx.action(() => {
|
|
|
|
let ctrlMod = e.getModifierState("Control") || e.getModifierState("Meta") || e.getModifierState("Shift");
|
|
|
|
if (e.code == "Enter" && !ctrlMod) {
|
|
|
|
e.preventDefault();
|
2022-06-13 20:12:39 +02:00
|
|
|
setTimeout(() => this.doSubmitCmd(), 0);
|
2022-06-08 02:25:35 +02:00
|
|
|
return;
|
|
|
|
}
|
2022-06-13 20:12:39 +02:00
|
|
|
// console.log(e.code, e.keyCode, e.key, event.which, ctrlMod, e);
|
2022-06-08 02:25:35 +02:00
|
|
|
})();
|
|
|
|
}
|
|
|
|
|
|
|
|
@boundMethod
|
|
|
|
onChange(e : any) {
|
|
|
|
mobx.action(() => {
|
|
|
|
this.curLine.set(e.target.value);
|
|
|
|
})();
|
|
|
|
}
|
2022-06-13 20:12:39 +02:00
|
|
|
|
|
|
|
@boundMethod
|
|
|
|
doSubmitCmd() {
|
|
|
|
let commandStr = this.curLine.get();
|
|
|
|
mobx.action(() => {
|
|
|
|
this.curLine.set("");
|
|
|
|
})();
|
|
|
|
let url = sprintf("http://localhost:8080/api/run-command");
|
|
|
|
let data = {sessionid: this.props.sessionid, command: commandStr};
|
|
|
|
fetch(url, {method: "post", body: JSON.stringify(data)}).then((resp) => handleJsonFetchResponse(url, resp)).then((data) => {
|
|
|
|
console.log("got success data", data);
|
|
|
|
mobx.action(() => {
|
|
|
|
let lines = GlobalLines.get();
|
|
|
|
data.data.line.isnew = true;
|
|
|
|
lines.push(data.data.line);
|
|
|
|
})();
|
|
|
|
}).catch((err) => {
|
|
|
|
console.log("error calling run-command", err)
|
|
|
|
});
|
|
|
|
}
|
2022-06-08 02:25:35 +02:00
|
|
|
|
|
|
|
render() {
|
|
|
|
return (
|
|
|
|
<div className="box cmd-input has-background-black">
|
|
|
|
<div className="cmd-input-context">
|
|
|
|
<div className="has-text-white">
|
|
|
|
<span className="bold term-blue">[ mike@imac27 master ~/work/gopath/src/github.com/sawka/darktile-termutil ]</span>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div className="cmd-input-field field has-addons">
|
|
|
|
<div className="control cmd-quick-context">
|
|
|
|
<div className="button is-static">mike@local</div>
|
|
|
|
</div>
|
|
|
|
<div className="control cmd-input-control is-expanded">
|
|
|
|
<textarea value={this.curLine.get()} onKeyDown={this.onKeyDown} onChange={this.onChange} className="input" type="text"></textarea>
|
|
|
|
</div>
|
|
|
|
<div className="control cmd-exec">
|
2022-06-13 20:12:39 +02:00
|
|
|
<div onClick={this.doSubmitCmd} className="button">
|
2022-06-08 02:25:35 +02:00
|
|
|
<span className="icon">
|
|
|
|
<i className="fa fa-rocket"/>
|
|
|
|
</span>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@mobxReact.observer
|
|
|
|
class Main extends React.Component<{sessionid : string}, {}> {
|
|
|
|
render() {
|
2022-06-13 20:12:39 +02:00
|
|
|
let lines = GlobalLines.get();
|
|
|
|
console.log("main-lines", mobx.toJS(lines));
|
2022-06-08 02:25:35 +02:00
|
|
|
return (
|
|
|
|
<div className="main">
|
|
|
|
<h1 className="title scripthaus-logo-small">
|
|
|
|
<div className="title-cursor">█</div>
|
|
|
|
ScriptHaus
|
|
|
|
</h1>
|
|
|
|
<div className="lines">
|
|
|
|
<For each="line" of={lines}>
|
|
|
|
<Line key={line.lineid} line={line} sessionid={this.props.sessionid}/>
|
|
|
|
</For>
|
|
|
|
</div>
|
2022-06-13 20:12:39 +02:00
|
|
|
<CmdInput sessionid={this.props.sessionid}/>
|
2022-06-08 02:25:35 +02:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export {Main};
|