waveterm/src/main.tsx

1267 lines
46 KiB
TypeScript
Raw Normal View History

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 {sprintf} from "sprintf-js";
import {boundMethod} from "autobind-decorator";
2022-07-14 08:11:45 +02:00
import {v4 as uuidv4} from "uuid";
2022-07-05 07:37:45 +02:00
import dayjs from 'dayjs'
2022-06-08 02:25:35 +02:00
import {If, For, When, Otherwise, Choose} from "tsx-control-statements/components";
import cn from "classnames"
import {TermWrap} from "./term";
2022-08-31 02:05:35 +02:00
import type {SessionDataType, LineType, CmdDataType, RemoteType, RemoteStateType, RemoteInstanceType, RemotePtrType, HistoryItem} from "./types";
import localizedFormat from 'dayjs/plugin/localizedFormat';
2022-08-31 02:05:35 +02:00
import {GlobalModel, GlobalCommandRunner, Session, Cmd, Window, Screen, ScreenWindow, riToRPtr} from "./model";
dayjs.extend(localizedFormat)
2022-06-13 20:12:39 +02:00
2022-08-13 03:34:56 +02:00
type InterObsValue = {
sessionid : string,
windowid : string,
lineid : string,
cmdid : string,
visible : mobx.IObservableValue<boolean>,
timeoutid? : any,
};
let globalLineWeakMap = new WeakMap<any, InterObsValue>();
2022-08-18 09:39:06 +02:00
function isBlank(s : string) : boolean {
return (s == null || s == "");
}
2022-08-29 22:54:11 +02:00
function windowLinesDOMId(windowid : string) {
return "window-lines-" + windowid;
}
function scrollDiv(div : any, amt : number) {
if (div == null) {
return;
}
let newScrollTop = div.scrollTop + amt;
if (newScrollTop < 0) {
newScrollTop = 0;
}
div.scrollTo({top: newScrollTop, behavior: "smooth"});
}
2022-08-30 00:42:50 +02:00
function pageSize(div : any) : number {
if (div == null) {
return 300;
}
let size = div.clientHeight;
if (size > 500) {
size = size - 100;
} else if (size > 200) {
size = size - 30;
}
return size;
}
2022-08-13 03:34:56 +02:00
function interObsCallback(entries) {
let now = Date.now();
entries.forEach((entry) => {
let line = globalLineWeakMap.get(entry.target);
if ((line.timeoutid != null) && (line.visible.get() == entry.isIntersecting)) {
clearTimeout(line.timeoutid);
line.timeoutid = null;
return;
}
if (line.visible.get() != entry.isIntersecting && line.timeoutid == null) {
line.timeoutid = setTimeout(() => {
line.timeoutid = null;
mobx.action(() => {
line.visible.set(entry.isIntersecting);
})();
}, 250);
return;
}
});
}
2022-07-12 02:55:03 +02:00
function getLineId(line : LineType) : string {
return sprintf("%s-%s-%s", line.sessionid, line.windowid, line.lineid);
}
function makeFullRemoteRef(ownerName : string, remoteRef : string, name : string) : string {
if (isBlank(ownerName) && isBlank(name)) {
return remoteRef;
}
if (!isBlank(ownerName) && isBlank(name)) {
return ownerName + ":" + remoteRef;
}
if (isBlank(ownerName) && !isBlank(name)) {
return remoteRef + ":" + name;
}
return ownerName + ":" + remoteRef + ":" + name;
}
function getRemoteStr(rptr : RemotePtrType) : string {
if (rptr == null || isBlank(rptr.remoteid)) {
return "(invalid remote)";
}
let username = (isBlank(rptr.ownerid) ? null : GlobalModel.resolveUserIdToName(rptr.ownerid));
let remoteRef = GlobalModel.resolveRemoteIdToRef(rptr.remoteid);
let fullRef = makeFullRemoteRef(username, remoteRef, rptr.name);
return fullRef;
}
function replaceHomePath(path : string, homeDir : string) : string {
if (path == homeDir) {
return "~";
}
if (path.startsWith(homeDir + "/")) {
return "~" + path.substr(homeDir.length);
}
return path;
}
function getCwdStr(remote : RemoteType, state : RemoteStateType) : string {
if ((state == null || state.cwd == null) && remote != null) {
return "~";
}
let cwd = "(unknown)";
if (state && state.cwd) {
cwd = state.cwd;
}
if (remote && remote.remotevars.home) {
cwd = replaceHomePath(cwd, remote.remotevars.home)
}
return cwd;
}
function getLineDateStr(ts : number) : string {
let lineDate = new Date(ts);
let nowDate = new Date();
if (nowDate.getFullYear() != lineDate.getFullYear()) {
return dayjs(lineDate).format("ddd L LTS");
}
else if (nowDate.getMonth() != lineDate.getMonth() || nowDate.getDate() != lineDate.getDate()) {
let yesterdayDate = (new Date());
yesterdayDate.setDate(yesterdayDate.getDate()-1);
if (yesterdayDate.getMonth() == lineDate.getMonth() && yesterdayDate.getDate() == lineDate.getDate()) {
return "Yesterday " + dayjs(lineDate).format("LTS");;
}
return dayjs(lineDate).format("ddd L LTS");
}
else {
return dayjs(ts).format("LTS");
}
}
2022-06-08 02:25:35 +02:00
@mobxReact.observer
class LineText extends React.Component<{sw : ScreenWindow, line : LineType}, {}> {
2022-06-08 02:25:35 +02:00
render() {
let line = this.props.line;
let formattedTime = getLineDateStr(line.ts);
2022-06-08 02:25:35 +02:00
return (
<div className="line line-text" data-lineid={line.lineid} data-windowid={line.windowid}>
2022-06-08 02:25:35 +02:00
<div className="avatar">
S
2022-06-08 02:25:35 +02:00
</div>
<div className="line-content">
<div className="meta">
<div className="user">{line.userid}</div>
<div className="ts">{formattedTime}</div>
2022-06-08 02:25:35 +02:00
</div>
<div className="text">
{line.text}
</div>
</div>
</div>
);
}
}
@mobxReact.observer
class Prompt extends React.Component<{rptr : RemotePtrType, rstate : RemoteStateType}, {}> {
render() {
let remote : RemoteType = null;
if (this.props.rptr && !isBlank(this.props.rptr.remoteid)) {
remote = GlobalModel.getRemote(this.props.rptr.remoteid);
}
let remoteStr = getRemoteStr(this.props.rptr);
let cwd = getCwdStr(remote, this.props.rstate);
let isRoot = false;
if (remote && remote.remotevars) {
if (remote.remotevars["sudo"] || remote.remotevars["bestuser"] == "root") {
isRoot = true;
}
}
let className = (isRoot ? "term-bright-red" : "term-bright-green");
return (
<span className="term-bright-green">[{remoteStr}] {cwd} {isRoot ? "#" : "$"}</span>
);
}
}
2022-06-08 02:25:35 +02:00
@mobxReact.observer
class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType, width : number, interObs : IntersectionObserver, initVis : boolean, cmdRefNum : number}, {}> {
termLoaded : mobx.IObservableValue<boolean> = mobx.observable.box(false);
2022-08-13 03:34:56 +02:00
lineRef : React.RefObject<any> = React.createRef();
iobsVal : InterObsValue = null;
autorunDisposer : () => void = null;
2022-06-16 09:31:54 +02:00
constructor(props) {
super(props);
2022-08-13 03:34:56 +02:00
let line = props.line;
let ival : InterObsValue = {
sessionid: line.sessionid,
windowid: line.windowid,
lineid: line.lineid,
cmdid: line.cmdid,
visible: mobx.observable.box(this.props.initVis),
};
this.iobsVal = ival;
2022-06-16 09:31:54 +02:00
}
2022-08-13 03:34:56 +02:00
visibilityChanged(vis : boolean) : void {
if (vis && !this.termLoaded.get()) {
this.loadTerminal();
}
else if (!vis && this.termLoaded.get()) {
let {line} = this.props;
}
}
loadTerminal() : void {
let {sw, line} = this.props;
2022-07-11 23:43:18 +02:00
let model = GlobalModel;
2022-07-12 02:55:03 +02:00
let cmd = model.getCmd(line);
2022-08-13 03:34:56 +02:00
if (cmd == null) {
return;
}
let termId = "term-" + getLineId(line);
let termElem = document.getElementById(termId);
if (termElem == null) {
console.log("cannot load terminal, no term elem found", termId);
return;
}
sw.connectElem(termElem, cmd, this.props.width);
mobx.action(() => this.termLoaded.set(true))();
}
componentDidMount() {
let {line} = this.props;
if (this.lineRef.current == null || this.props.interObs == null) {
console.log("LineCmd lineRef current is null or interObs is null", line, this.lineRef.current, this.props.interObs);
}
else {
globalLineWeakMap.set(this.lineRef.current, this.iobsVal);
this.props.interObs.observe(this.lineRef.current);
this.autorunDisposer = mobx.autorun(() => {
let vis = this.iobsVal.visible.get();
this.visibilityChanged(vis);
});
2022-07-12 02:55:03 +02:00
}
2022-06-15 01:02:20 +02:00
}
componentWillUnmount() {
let {sw, line} = this.props;
let model = GlobalModel;
2022-08-13 03:34:56 +02:00
if (this.termLoaded.get()) {
sw.disconnectElem(line.cmdid);
}
if (this.lineRef.current != null && this.props.interObs != null) {
this.props.interObs.unobserve(this.lineRef.current);
}
if (this.autorunDisposer != null) {
this.autorunDisposer();
}
}
scrollIntoView() {
let lineElem = document.getElementById("line-" + getLineId(this.props.line));
lineElem.scrollIntoView({block: "end"});
2022-06-08 02:25:35 +02:00
}
2022-06-13 20:12:39 +02:00
@boundMethod
doRefresh() {
let {sw, line} = this.props;
2022-07-12 02:55:03 +02:00
let model = GlobalModel;
2022-08-13 03:34:56 +02:00
let termWrap = sw.getTermWrap(line.cmdid);
if (termWrap != null) {
termWrap.reloadTerminal(500);
2022-07-12 02:55:03 +02:00
}
2022-06-15 01:02:20 +02:00
}
2022-07-11 23:43:18 +02:00
renderCmdText(cmd : Cmd, remote : RemoteType) : any {
2022-07-07 22:27:44 +02:00
if (cmd == null) {
return (
<div className="metapart-mono cmdtext">
<span className="term-bright-green">(cmd not found)</span>
</div>
);
}
let remoteStr = getRemoteStr(cmd.remote);
let cwd = getCwdStr(remote, cmd.getRemoteState());
2022-07-07 22:27:44 +02:00
return (
<div className="metapart-mono cmdtext">
<Prompt rptr={cmd.remote} rstate={cmd.getRemoteState()}/> {cmd.getSingleLineCmdText()}
2022-07-07 22:27:44 +02:00
</div>
);
}
@boundMethod
2022-08-25 21:12:56 +02:00
clickTermBlock(e : any) {
let {sw, line} = this.props;
let model = GlobalModel;
let termWrap = sw.getTermWrap(line.cmdid);
if (termWrap != null) {
termWrap.terminal.focus();
}
}
2022-06-08 02:25:35 +02:00
render() {
2022-08-13 03:34:56 +02:00
let {sw, line, width} = this.props;
2022-07-12 02:55:03 +02:00
let model = GlobalModel;
let lineid = line.lineid;
let formattedTime = getLineDateStr(line.ts);
2022-07-12 02:55:03 +02:00
let cmd = model.getCmd(line);
if (cmd == null) {
2022-08-13 03:34:56 +02:00
return (
<div className="line line-invalid" id={"line-" + getLineId(line)} ref={this.lineRef}>
[cmd not found '{line.cmdid}']
</div>
);
2022-07-07 22:27:44 +02:00
}
let termLoaded = this.termLoaded.get();
let cellHeightPx = 16;
let cellWidthPx = 8;
2022-08-13 03:34:56 +02:00
let termWidth = Math.max(Math.trunc((width - 20)/cellWidthPx), 10);
let usedRows = sw.getUsedRows(cmd, width);
let totalHeight = cellHeightPx * usedRows;
2022-07-12 02:55:03 +02:00
let remote = model.getRemote(cmd.remoteId);
let status = cmd.getStatus();
let termOpts = cmd.getTermOpts();
2022-08-13 03:34:56 +02:00
let isFocused = sw.getIsFocused(line.cmdid);
let cmdRefNumStr = (this.props.cmdRefNum == null ? "?" : this.props.cmdRefNum.toString());
2022-06-08 02:25:35 +02:00
return (
<div className={cn("line", "line-cmd", {"focus": isFocused})} id={"line-" + getLineId(line)} ref={this.lineRef} style={{position: "relative"}} data-lineid={line.lineid} data-windowid={line.windowid} data-cmdid={line.cmdid}>
<div className="line-header">
2022-08-23 22:14:57 +02:00
<div className={cn("avatar",{"num4": cmdRefNumStr.length == 4}, {"num5": cmdRefNumStr.length >= 5}, "status-" + status, {"ephemeral": line.ephemeral})} onClick={this.doRefresh}>
{cmdRefNumStr}
2022-08-20 01:35:38 +02:00
<If condition={status == "hangup" || status == "error"}>
<i className="fa fa-exclamation-triangle status-icon"/>
</If>
<If condition={status == "detached"}>
<i className="fa fa-refresh status-icon"/>
</If>
</div>
<div className="meta-wrap">
<div className="meta">
<div className="user" style={{display: "none"}}>{line.userid}</div>
<div className="ts">{formattedTime}</div>
</div>
<div className="meta">
<div className="metapart-mono" style={{display: "none"}}>
{line.cmdid}
({termOpts.rows}x{termOpts.cols})
</div>
{this.renderCmdText(cmd, remote)}
</div>
2022-06-08 02:25:35 +02:00
</div>
</div>
<div className={cn("terminal-wrapper", {"focus": isFocused})} style={{overflowY: "hidden"}}>
<If condition={!isFocused}>
<div className="term-block" onClick={this.clickTermBlock}></div>
</If>
<div className="terminal" id={"term-" + getLineId(line)} data-cmdid={line.cmdid} style={{height: totalHeight}}></div>
2022-08-13 03:34:56 +02:00
<If condition={!termLoaded}><div style={{position: "absolute", top: 60, left: 30}}>(loading)</div></If>
</div>
2022-06-08 02:25:35 +02:00
</div>
);
}
}
@mobxReact.observer
class Line extends React.Component<{sw : ScreenWindow, line : LineType, width : number, interObs : IntersectionObserver, initVis : boolean, cmdRefNum : number}, {}> {
2022-06-08 02:25:35 +02:00
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
class TextAreaInput extends React.Component<{}, {}> {
lastTab : boolean = false;
lastHistoryUpDown : boolean = false;
lastTabCurLine : mobx.IObservableValue<string> = mobx.observable.box(null);
2022-08-24 02:27:12 +02:00
componentDidMount() {
let input = document.getElementById("main-cmd-input");
if (input != null) {
input.focus();
}
}
isModKeyPress(e : any) {
return e.code.match(/^(Control|Meta|Alt|Shift)(Left|Right)$/);
}
getLinePos(elem : any) : {numLines : number, linePos : number} {
let numLines = elem.value.split("\n").length;
let linePos = elem.value.substr(0, elem.selectionStart).split("\n").length;
return {numLines, linePos};
}
2022-06-08 02:25:35 +02:00
@mobx.action @boundMethod
onKeyDown(e : any) {
mobx.action(() => {
if (this.isModKeyPress(e)) {
return;
}
2022-07-11 23:43:18 +02:00
let model = GlobalModel;
let inputModel = model.inputModel;
2022-07-12 02:55:03 +02:00
let win = model.getActiveWindow();
2022-06-08 02:25:35 +02:00
let ctrlMod = e.getModifierState("Control") || e.getModifierState("Meta") || e.getModifierState("Shift");
let curLine = inputModel.getCurLine();
let lastTab = this.lastTab;
this.lastTab = (e.code == "Tab");
let lastHist = this.lastHistoryUpDown;
this.lastHistoryUpDown = false;
if (e.code == "Tab") {
e.preventDefault();
if (lastTab) {
GlobalModel.submitCommand("compgen", null, [curLine], {"comppos": String(curLine.length), "compshow": "1", "nohist": "1"}, true);
return;
}
else {
GlobalModel.submitCommand("compgen", null, [curLine], {"comppos": String(curLine.length), "nohist": "1"}, true);
return;
}
}
if (e.code == "Enter") {
2022-06-08 02:25:35 +02:00
e.preventDefault();
2022-08-31 02:05:35 +02:00
if (inputModel.historyShow.get()) {
inputModel.grabSelectedHistoryItem();
return;
}
if (!ctrlMod) {
2022-08-25 21:12:56 +02:00
setTimeout(() => GlobalModel.inputModel.uiSubmitCommand(), 0);
return;
}
e.target.setRangeText("\n", e.target.selectionStart, e.target.selectionEnd, "end");
GlobalModel.inputModel.setCurLine(e.target.value);
2022-06-08 02:25:35 +02:00
return;
}
if (e.code == "Escape") {
e.preventDefault();
GlobalModel.inputModel.toggleInfoMsg();
return;
}
if (e.code == "KeyC" && e.getModifierState("Control")) {
e.preventDefault();
inputModel.clearCurLine();
return;
}
2022-08-31 02:05:35 +02:00
if (e.code == "KeyR" && e.getModifierState("Control")) {
e.preventDefault();
GlobalCommandRunner.openHistory();
return;
}
if (e.code == "ArrowUp" || e.code == "ArrowDown") {
2022-08-31 02:05:35 +02:00
if (inputModel.historyShow.get()) {
inputModel.moveHistorySelection(e.code == "ArrowUp" ? -1 : 1);
return;
}
let linePos = this.getLinePos(e.target);
if (e.code == "ArrowUp") {
if (!lastHist && linePos.linePos > 1) {
// regular arrow
return;
}
e.preventDefault();
inputModel.prevHistoryItem();
this.lastHistoryUpDown = true;
return;
}
if (e.code == "ArrowDown") {
if (!lastHist && linePos.linePos < linePos.numLines) {
// regular arrow
return;
}
e.preventDefault();
inputModel.nextHistoryItem();
this.lastHistoryUpDown = true;
return;
}
2022-06-21 01:06:37 +02:00
}
2022-08-29 22:54:11 +02:00
if (e.code == "PageUp" || e.code == "PageDown") {
e.preventDefault();
2022-08-31 02:05:35 +02:00
if (inputModel.historyShow.get()) {
inputModel.moveHistorySelection(e.code == "PageUp" ? -10 : 10);
return;
}
let infoScroll = inputModel.hasScrollingInfoMsg();
2022-08-29 22:54:11 +02:00
if (infoScroll) {
2022-08-30 00:42:50 +02:00
let div = document.querySelector(".cmd-input-info");
let amt = pageSize(div);
scrollDiv(div, (e.code == "PageUp" ? -amt : amt));
2022-08-29 22:54:11 +02:00
}
else {
let win = GlobalModel.getActiveWindow();
if (win == null) {
return;
}
let id = windowLinesDOMId(win.windowId);
let div = document.getElementById(id);
2022-08-30 00:42:50 +02:00
let amt = pageSize(div);
scrollDiv(div, (e.code == "PageUp" ? -amt : amt));
2022-08-29 22:54:11 +02:00
}
}
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(() => {
GlobalModel.inputModel.setCurLine(e.target.value);
2022-06-08 02:25:35 +02:00
})();
}
2022-06-13 20:12:39 +02:00
render() {
let model = GlobalModel;
let inputModel = model.inputModel;
let curLine = inputModel.getCurLine();
let numLines = curLine.split("\n").length;
let displayLines = numLines;
if (displayLines > 5) {
displayLines = 5;
}
return (
<textarea id="main-cmd-input" rows={displayLines} value={curLine} onKeyDown={this.onKeyDown} onChange={this.onChange} className="textarea"></textarea>
);
}
}
2022-08-31 00:25:51 +02:00
@mobxReact.observer
class HistoryInfo extends React.Component<{}, {}> {
2022-08-31 02:05:35 +02:00
lastClickHNum : string = null;
lastClickTs : number = 0;
2022-08-31 00:25:51 +02:00
componentDidMount() {
let inputModel = GlobalModel.inputModel;
let selNum = inputModel.historySelectedNum.get();
if (selNum != null) {
2022-08-31 02:05:35 +02:00
inputModel.scrollHistoryItemIntoView(selNum);
2022-08-31 00:25:51 +02:00
}
}
2022-08-31 02:05:35 +02:00
@boundMethod
handleItemClick(hitem : HistoryItem) {
let inputModel = GlobalModel.inputModel;
let selNum = inputModel.historySelectedNum.get();
if (this.lastClickHNum == hitem.historynum && selNum == hitem.historynum) {
inputModel.grabSelectedHistoryItem();
return;
}
inputModel.focusCmdInput();
inputModel.setHistorySelectionNum(hitem.historynum);
let now = Date.now();
this.lastClickHNum = hitem.historynum;
this.lastClickTs = now;
setTimeout(() => {
if (this.lastClickTs == now) {
this.lastClickHNum = null;
this.lastClickTs = 0;
}
}, 3000);
}
renderHItem(hitem : HistoryItem, selNum : string) : any {
2022-08-31 00:25:51 +02:00
let lines = hitem.cmdstr.split("\n");
let line : string = "";
let idx = 0;
return (
2022-08-31 02:05:35 +02:00
<div key={hitem.historynum} className={cn("history-item", {"is-selected": selNum == hitem.historynum}, "hnum-" + hitem.historynum)} onClick={() => this.handleItemClick(hitem)}>
2022-08-31 00:25:51 +02:00
<div className="history-line">{(selNum == hitem.historynum ? "*" : " ")}{sprintf("%5s", hitem.historynum)} {lines[0]}</div>
<For each="line" index="index" of={lines.slice(1)}>
<div key={idx} className="history-line">{line}</div>
</For>
</div>
);
}
2022-08-31 02:05:35 +02:00
@boundMethod
handleClose() {
GlobalModel.inputModel.toggleInfoMsg();
}
2022-08-31 00:25:51 +02:00
render() {
let inputModel = GlobalModel.inputModel;
let idx : number = 0;
let selNum = inputModel.historySelectedNum.get();
2022-08-31 02:05:35 +02:00
let hitems = inputModel.getFilteredHistoryItems();
2022-08-31 00:25:51 +02:00
hitems = hitems.slice().reverse();
2022-08-31 02:05:35 +02:00
let hitem : HistoryItem = null;
2022-08-31 00:25:51 +02:00
return (
<div className="cmd-history">
<div className="history-title">
2022-08-31 02:05:35 +02:00
history
2022-08-31 00:25:51 +02:00
{" "}
<span className="term-bright-white">[containing '']</span>
{" "}
<span className="term-bright-white">[this session &#x2318;S]</span>
{" "}
<span className="term-bright-white">[this window &#x2318;W]</span>
{" "}
<span className="term-bright-white">[this remote &#x2318;R]</span>
{" "}
<span className="term-bright-white">[including metacmds &#x2318;M]</span>
2022-08-31 02:05:35 +02:00
{" "} <span className="history-clickable-opt" onClick={this.handleClose}>(close ESC)</span>
2022-08-31 00:25:51 +02:00
</div>
<div className="history-items">
<If condition={hitems.length == 0}>
[no history]
</If>
<If condition={hitems.length > 0}>
<For each="hitem" index="idx" of={hitems}>
{this.renderHItem(hitem, selNum)}
</For>
</If>
</div>
</div>
);
}
}
@mobxReact.observer
class CmdInput extends React.Component<{}, {}> {
2022-08-11 19:22:43 +02:00
getAfterSlash(s : string) : string {
2022-08-24 02:27:12 +02:00
if (s.startsWith("^/")) {
return s.substr(1);
}
2022-08-11 19:22:43 +02:00
let slashIdx = s.lastIndexOf("/");
if (slashIdx == s.length-1) {
slashIdx = s.lastIndexOf("/", slashIdx-1);
}
if (slashIdx == -1) {
return s;
}
return s.substr(slashIdx+1);
}
2022-06-08 02:25:35 +02:00
render() {
let model = GlobalModel;
let inputModel = model.inputModel;
let win = GlobalModel.getActiveWindow();
let ri : RemoteInstanceType = null;
let rptr : RemotePtrType = null;
if (win != null) {
ri = win.getCurRemoteInstance();
rptr = win.curRemote.get();
}
let remote : RemoteType = null;
let remoteState : RemoteStateType = null;
if (ri != null) {
remote = GlobalModel.getRemote(ri.remoteid);
remoteState = ri.state;
}
let remoteStr = getRemoteStr(rptr);
let cwdStr = getCwdStr(remote, remoteState);
let infoMsg = inputModel.infoMsg.get();
let infoShow = inputModel.infoShow.get();
let historyShow = !infoShow && inputModel.historyShow.get();
let istr : string = null;
let istrIdx : number = 0;
2022-08-23 03:54:01 +02:00
let line : string = null;
2022-08-23 22:14:57 +02:00
let idx : number = 0;
2022-06-08 02:25:35 +02:00
return (
<div className={cn("box cmd-input has-background-black", {"has-info": infoShow || historyShow})}>
2022-08-31 00:25:51 +02:00
<If condition={historyShow}>
<HistoryInfo/>
</If>
<div className="cmd-input-info" style={{display: (infoShow ? "block" : "none")}}>
<If condition={infoMsg && infoMsg.infotitle != null}>
<div className="info-title">
{infoMsg.infotitle}
</div>
</If>
<If condition={infoMsg && infoMsg.infomsg != null}>
<div className="info-msg">
{infoMsg.infomsg}
</div>
</If>
2022-08-23 03:54:01 +02:00
<If condition={infoMsg && infoMsg.infolines != null}>
<div className="info-lines">
2022-08-23 22:14:57 +02:00
<For index="idx" each="line" of={infoMsg.infolines}>
2022-08-24 02:27:12 +02:00
<div key={idx}>{line == "" ? " " : line}</div>
2022-08-23 03:54:01 +02:00
</For>
</div>
</If>
2022-08-11 19:22:43 +02:00
<If condition={infoMsg && infoMsg.infocomps != null && infoMsg.infocomps.length > 0}>
<div className="info-comps">
<For each="istr" index="istrIdx" of={infoMsg.infocomps}>
2022-08-24 02:27:12 +02:00
<div key={istrIdx} className={cn("info-comp", {"metacmd-comp": istr.startsWith("^")})}>
2022-08-11 19:22:43 +02:00
{this.getAfterSlash(istr)}
</div>
</For>
2022-08-11 19:22:43 +02:00
<If condition={infoMsg.infocompsmore}>
<div key="more" className="info-comp">
...
</div>
</If>
</div>
</If>
<If condition={infoMsg && infoMsg.infoerror != null}>
<div className="info-error">
[error] {infoMsg.infoerror}
</div>
</If>
</div>
2022-06-08 02:25:35 +02:00
<div className="cmd-input-context">
<div className="has-text-white">
<Prompt rptr={rptr} rstate={remoteState}/>
2022-06-08 02:25:35 +02:00
</div>
</div>
<div className="cmd-input-field field has-addons">
<div className="control cmd-quick-context">
<div className="button is-static">{remoteStr}</div>
2022-06-08 02:25:35 +02:00
</div>
<div className="control cmd-input-control is-expanded">
<TextAreaInput/>
2022-06-08 02:25:35 +02:00
</div>
<div className="control cmd-exec">
2022-08-25 21:12:56 +02:00
<div onClick={GlobalModel.inputModel.uiSubmitCommand} 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
2022-07-13 08:29:39 +02:00
class ScreenWindowView extends React.Component<{sw : ScreenWindow}, {}> {
mutObs : any;
2022-08-13 03:34:56 +02:00
rszObs : any;
interObs : IntersectionObserver;
2022-07-14 08:11:45 +02:00
randomId : string;
width : mobx.IObservableValue<number> = mobx.observable.box(0);
lastHeight : number = null;
2022-07-14 08:11:45 +02:00
scrollToBottom(reason : string) {
let elem = document.getElementById(this.getLinesDOMId());
2022-07-14 08:11:45 +02:00
if (elem == null) {
return;
}
let oldST = elem.scrollTop;
elem.scrollTop = elem.scrollHeight;
// console.log("scroll-elem", oldST, elem.scrollHeight, elem.scrollTop, elem.scrollLeft, elem);
}
@boundMethod
scrollHandler(event : any) {
2022-07-13 08:29:39 +02:00
let {sw} = this.props;
let target = event.target;
let atBottom = (target.scrollTop + 30 > (target.scrollHeight - target.offsetHeight));
2022-07-13 08:29:39 +02:00
if (sw && sw.shouldFollow.get() != atBottom) {
2022-08-13 03:34:56 +02:00
mobx.action(() => sw.shouldFollow.set(atBottom))();
}
// console.log("scroll-handler (sw)>", atBottom, target.scrollTop, target.scrollHeight, event);
}
componentDidMount() {
let elem = document.getElementById(this.getLinesDOMId());
if (elem != null) {
this.mutObs = new MutationObserver(this.handleDomMutation.bind(this));
this.mutObs.observe(elem, {childList: true});
elem.addEventListener("termresize", this.handleTermResize);
let {sw} = this.props;
if (sw && sw.shouldFollow.get()) {
setTimeout(() => this.scrollToBottom("mount"), 0);
}
2022-08-13 03:34:56 +02:00
this.interObs = new IntersectionObserver(interObsCallback, {
root: elem,
rootMargin: "800px",
threshold: 0.0,
});
}
let wvElem = document.getElementById(this.getWindowViewDOMId());
if (wvElem != null) {
this.rszObs = new ResizeObserver(this.handleResize.bind(this));
this.rszObs.observe(wvElem);
2022-07-14 08:11:45 +02:00
}
}
updateWidth(width : number) {
mobx.action(() => {
this.width.set(width);
})();
}
componentWillUnmount() {
2022-07-14 08:11:45 +02:00
if (this.mutObs) {
this.mutObs.disconnect();
}
if (this.rszObs) {
this.rszObs.disconnect();
}
2022-08-13 03:34:56 +02:00
if (this.interObs) {
this.interObs.disconnect();
}
}
handleResize(entries : any) {
if (entries.length == 0) {
return;
}
let entry = entries[0];
let width = entry.target.offsetWidth;
this.updateWidth(width);
if (this.lastHeight == null) {
this.lastHeight = entry.target.offsetHeight;
return;
}
if (this.lastHeight != entry.target.offsetHeight) {
this.lastHeight = entry.target.offsetHeight;
this.doConditionalScrollToBottom("resize-height");
}
}
handleDomMutation(mutations, mutObs) {
this.doConditionalScrollToBottom("mut");
}
doConditionalScrollToBottom(reason : string) {
2022-07-13 08:29:39 +02:00
let {sw} = this.props;
if (sw && sw.shouldFollow.get()) {
setTimeout(() => this.scrollToBottom(reason), 0);
}
}
getWindow() : Window {
2022-07-13 08:29:39 +02:00
let {sw} = this.props;
if (sw == null) {
return null;
}
let win = GlobalModel.getWindowById(sw.sessionId, sw.windowId);
if (win == null) {
win = GlobalModel.loadWindow(sw.sessionId, sw.windowId);
}
return win;
}
getLinesDOMId() {
2022-08-29 22:54:11 +02:00
return windowLinesDOMId(this.getWindowId());
}
@boundMethod
handleTermResize(e : any) {
2022-07-13 08:29:39 +02:00
let {sw} = this.props;
if (sw && sw.shouldFollow.get()) {
2022-07-14 08:11:45 +02:00
setTimeout(() => this.scrollToBottom("termresize"), 0);
}
}
2022-07-13 08:29:39 +02:00
getWindowViewStyle() : any {
// return {width: "100%", height: "100%"};
return {position: "absolute", width: "100%", height: "100%", overflowX: "hidden"};
}
getWindowId() : string {
let {sw} = this.props;
if (sw == null) {
if (!this.randomId) {
this.randomId = uuidv4();
}
return this.randomId;
}
return sw.windowId;
}
getWindowViewDOMId() {
return sprintf("window-view-%s", this.getWindowId());
2022-07-13 08:29:39 +02:00
}
renderError(message : string) {
2022-07-14 08:11:45 +02:00
let {sw} = this.props;
return (
<div className="window-view" style={this.getWindowViewStyle()} id={this.getWindowViewDOMId()}>
2022-07-14 08:11:45 +02:00
<div key="window-tag" className="window-tag">
<If condition={sw != null}>
<span>{sw.name.get()}{sw.shouldFollow.get() ? "*" : ""}</span>
</If>
</div>
<div key="lines" className="lines" id={this.getLinesDOMId()}></div>
2022-07-14 08:11:45 +02:00
<div key="window-empty" className="window-empty">
<div>{message}</div>
</div>
</div>
);
}
2022-06-08 02:25:35 +02:00
render() {
2022-07-13 08:29:39 +02:00
let {sw} = this.props;
if (sw == null) {
return this.renderError("(no screen window)");
}
2022-07-13 08:29:39 +02:00
let win = this.getWindow();
if (win == null || !win.loaded.get()) {
return this.renderError("(loading)");
}
if (win.loadError.get() != null) {
return this.renderError(sprintf("(%s)", win.loadError.get()));
}
if (this.width.get() == 0) {
return this.renderError("");
}
2022-07-05 07:18:36 +02:00
let idx = 0;
2022-07-05 07:37:45 +02:00
let line : LineType = null;
2022-07-14 08:11:45 +02:00
let screen = GlobalModel.getScreenById(sw.sessionId, sw.screenId);
let session = GlobalModel.getSessionById(sw.sessionId);
let linesStyle : any = {};
if (win.lines.length == 0) {
linesStyle.display = "none";
}
let cmdRefMap : Record<string, number> = {};
let cmdNum = 1;
for (let i=0; i<win.lines.length; i++) {
let line = win.lines[i];
if (line.cmdid != null) {
cmdRefMap[line.lineid] = cmdNum;
cmdNum++;
}
}
2022-06-08 02:25:35 +02:00
return (
<div className="window-view" style={this.getWindowViewStyle()} id={this.getWindowViewDOMId()}>
2022-07-14 08:11:45 +02:00
<div key="window-tag" className="window-tag">
<span>{sw.name.get()}{sw.shouldFollow.get() ? "*" : ""}</span>
</div>
<div key="lines" className="lines" onScroll={this.scrollHandler} id={this.getLinesDOMId()} style={linesStyle}>
<div className="lines-spacer"></div>
2022-07-11 23:43:18 +02:00
<For each="line" of={win.lines} index="idx">
<Line key={line.lineid} line={line} sw={sw} width={this.width.get()} interObs={this.interObs} initVis={idx > win.lines.length-1-7} cmdRefNum={cmdRefMap[line.lineid] ?? 0}/>
2022-06-08 02:25:35 +02:00
</For>
</div>
2022-07-14 08:11:45 +02:00
<If condition={win.lines.length == 0}>
<div key="window-empty" className="window-empty">
<div><code>[session="{session.name.get()}" screen="{screen.name.get()}" window="{sw.name.get()}"]</code></div>
</div>
</If>
</div>
);
}
}
2022-07-13 08:29:39 +02:00
@mobxReact.observer
class ScreenView extends React.Component<{screen : Screen}, {}> {
render() {
let {screen} = this.props;
if (screen == null) {
return (
<div className="screen-view">
(no screen)
</div>
);
}
let sw = screen.getActiveSW();
return (
<div className="screen-view">
2022-07-14 08:11:45 +02:00
<ScreenWindowView key={sw.windowId} sw={sw}/>
2022-07-13 08:29:39 +02:00
</div>
);
}
}
@mobxReact.observer
2022-07-14 08:11:45 +02:00
class ScreenTabs extends React.Component<{session : Session}, {}> {
@boundMethod
handleNewScreen() {
let {session} = this.props;
2022-08-31 02:05:35 +02:00
GlobalCommandRunner.createNewScreen();
2022-07-14 08:11:45 +02:00
}
@boundMethod
handleSwitchScreen(screenId : string) {
let {session} = this.props;
if (session == null) {
return;
}
if (session.activeScreenId.get() == screenId) {
return;
}
let screen = session.getScreenById(screenId);
if (screen == null) {
return;
}
2022-08-31 02:05:35 +02:00
GlobalCommandRunner.switchScreen(screenId);
2022-07-14 08:11:45 +02:00
}
handleContextMenu(e : any, screenId : string) : void {
e.preventDefault();
console.log("handle context menu!", screenId);
let model = GlobalModel;
2022-08-11 20:49:46 +02:00
model.contextScreen(e, screenId);
}
2022-08-27 06:43:48 +02:00
2022-07-13 08:29:39 +02:00
render() {
2022-07-14 08:11:45 +02:00
let {session} = this.props;
2022-07-13 08:29:39 +02:00
if (session == null) {
return null;
}
2022-07-14 08:11:45 +02:00
let screen : Screen = null;
let index = 0;
2022-07-13 08:29:39 +02:00
return (
<div className="screen-tabs">
<For each="screen" index="index" of={session.screens}>
2022-08-27 06:43:48 +02:00
<div key={screen.screenId} className={cn("screen-tab", {"is-active": session.activeScreenId.get() == screen.screenId}, "color-" + screen.getTabColor())} onClick={() => this.handleSwitchScreen(screen.screenId)} onContextMenu={(event) => this.handleContextMenu(event, screen.screenId)}>
2022-07-14 08:11:45 +02:00
{screen.name.get()}
<If condition={index+1 <= 9}>
<div className="tab-index">&#x2318;{index+1}</div>
</If>
2022-07-14 08:11:45 +02:00
</div>
</For>
<div key="new-screen" className="screen-tab new-screen" onClick={this.handleNewScreen}>
+
</div>
2022-07-13 08:29:39 +02:00
</div>
);
}
}
@mobxReact.observer
class SessionView extends React.Component<{}, {}> {
render() {
let model = GlobalModel;
let session = model.getActiveSession();
if (session == null) {
return <div className="session-view">(no active session)</div>;
}
2022-07-13 08:29:39 +02:00
let activeScreen = session.getActiveScreen();
return (
<div className="session-view">
2022-07-13 08:29:39 +02:00
<ScreenView screen={activeScreen}/>
2022-07-14 08:11:45 +02:00
<ScreenTabs session={session}/>
2022-07-12 02:55:03 +02:00
<CmdInput/>
2022-06-08 02:25:35 +02:00
</div>
);
}
}
2022-06-20 22:03:20 +02:00
@mobxReact.observer
class MainSideBar extends React.Component<{}, {}> {
collapsed : mobx.IObservableValue<boolean> = mobx.observable.box(false);
@boundMethod
toggleCollapsed() {
mobx.action(() => {
this.collapsed.set(!this.collapsed.get());
})();
}
handleSessionClick(sessionId : string) {
2022-08-31 02:05:35 +02:00
GlobalCommandRunner.switchSession(sessionId);
}
handleNewSession() {
2022-08-31 02:05:35 +02:00
GlobalCommandRunner.createNewSession();
}
2022-07-09 10:37:19 +02:00
2022-08-18 09:39:06 +02:00
clickRemotes() {
mobx.action(() => {
GlobalModel.remotesModalOpen.set(true);
})();
}
2022-06-20 22:03:20 +02:00
render() {
2022-07-11 23:43:18 +02:00
let model = GlobalModel;
2022-07-13 23:16:47 +02:00
let activeSessionId = model.activeSessionId.get();
2022-07-12 02:55:03 +02:00
let session : Session = null;
let remotes = model.remotes;
let remote : RemoteType = null;
let idx : number = 0;
2022-06-20 22:03:20 +02:00
return (
<div className={cn("main-sidebar", {"collapsed": this.collapsed.get()})}>
<div className="collapse-container">
<div className="arrow-container" onClick={this.toggleCollapsed}>
<If condition={!this.collapsed.get()}><i className="fa fa-arrow-left"/></If>
<If condition={this.collapsed.get()}><i className="fa fa-arrow-right"/></If>
</div>
</div>
<div className="menu">
<p className="menu-label">
Private Sessions
2022-06-20 22:03:20 +02:00
</p>
<ul className="menu-list">
2022-07-12 02:55:03 +02:00
<If condition={!model.sessionListLoaded.get()}>
2022-07-11 23:43:18 +02:00
<li><a>(loading)</a></li>
</If>
2022-07-12 02:55:03 +02:00
<If condition={model.sessionListLoaded.get()}>
<For each="session" index="idx" of={model.sessionList}>
<li key={session.sessionId}><a className={cn({"is-active": activeSessionId == session.sessionId})} onClick={() => this.handleSessionClick(session.sessionId)}>
<span className="session-num">{idx+1}&nbsp;</span>
{session.name.get()}
</a></li>
2022-07-11 23:43:18 +02:00
</For>
<li className="new-session"><a className="new-session" onClick={() => this.handleNewSession()}><i className="fa fa-plus"/> New Session</a></li>
2022-07-11 23:43:18 +02:00
</If>
2022-06-20 22:03:20 +02:00
</ul>
<p className="menu-label">
Shared Sessions
2022-06-20 22:03:20 +02:00
</p>
<ul className="menu-list">
<li><a>server-status</a></li>
<li><a className="activity">bug-3458 <div className="tag is-link">3</div></a></li>
<li><a>dev-build</a></li>
2022-06-20 22:03:20 +02:00
<li className="new-session"><a className="new-session"><i className="fa fa-plus"/> New Session</a></li>
</ul>
<p className="menu-label">
Direct Messages
</p>
<ul className="menu-list">
<li><a>
<i className="user-status status fa fa-circle"/>
<img className="avatar" src="https://i.pravatar.cc/48?img=4"/>
Mike S <span className="sub-label">you</span>
</a></li>
<li><a>
<i className="user-status status offline fa fa-circle"/>
<img className="avatar" src="https://i.pravatar.cc/48?img=8"/>
Matt P
</a></li>
<li><a>
<i className="user-status status offline fa fa-circle"/>
<img className="avatar" src="https://i.pravatar.cc/48?img=12"/>
Adam B
</a></li>
<li><a className="activity">
<i className="user-status status fa fa-circle"/>
2022-06-21 01:06:37 +02:00
<img className="avatar" src="https://i.pravatar.cc/48?img=5"/>
2022-06-20 22:03:20 +02:00
Michelle T <div className="tag is-link">2</div>
</a></li>
</ul>
<div className="spacer"></div>
<p className="menu-label">
2022-08-18 09:39:06 +02:00
<a onClick={() => this.clickRemotes()}>Remotes</a>
2022-06-20 22:03:20 +02:00
</p>
<ul className="menu-list">
<For each="remote" of={remotes}>
2022-08-18 09:39:06 +02:00
<li key={remote.remoteid}><a><i className={cn("remote-status fa fa-circle", "status-" + remote.status)}/>{remote.remotealias ?? remote.remotecanonicalname}</a></li>
</For>
2022-06-20 22:03:20 +02:00
</ul>
<div className="bottom-spacer"></div>
</div>
</div>
);
}
}
2022-08-18 09:39:06 +02:00
@mobxReact.observer
class RemoteModal extends React.Component<{}, {}> {
@boundMethod
handleModalClose() : void {
mobx.action(() => {
GlobalModel.remotesModalOpen.set(false);
})();
}
@boundMethod
handleAddRemote() : void {
console.log("add-remote");
}
render() {
let model = GlobalModel;
let remotes = model.remotes;
let remote : RemoteType = null;
return (
<div className="remote-modal modal is-active">
<div onClick={this.handleModalClose} className="modal-background"></div>
<div className="modal-content message">
<div className="message-header">
<p>Remotes</p>
</div>
<div className="remotes-content">
<table className="table">
<thead>
<tr>
<th className="status-header">Status</th>
<th>Alias</th>
<th>User@Host</th>
2022-08-21 21:26:10 +02:00
<th>Connect</th>
2022-08-18 09:39:06 +02:00
</tr>
</thead>
<tbody>
<For each="remote" of={remotes}>
<tr>
<td className="status-cell">
<div><i className={cn("remote-status fa fa-circle", "status-" + remote.status)}/></div>
</td>
<td>
{remote.remotealias}
<If condition={isBlank(remote.remotealias)}>
-
</If>
</td>
<td>
{remote.remotecanonicalname}
</td>
<td>
2022-08-21 21:26:10 +02:00
{remote.connectmode}
2022-08-18 09:39:06 +02:00
</td>
</tr>
</For>
</tbody>
</table>
</div>
<div className="remotes-footer">
<button onClick={this.handleAddRemote} className="button is-primary">
<span className="icon">
<i className="fa fa-plus"/>
</span>
<span>Add Remote</span>
</button>
<div className="spacer"></div>
<button onClick={this.handleModalClose} className="button">Close</button>
</div>
</div>
<button onClick={this.handleModalClose} className="modal-close is-large" aria-label="close"></button>
</div>
);
}
}
2022-06-16 03:12:22 +02:00
@mobxReact.observer
2022-06-17 00:51:17 +02:00
class Main extends React.Component<{}, {}> {
2022-06-16 03:12:22 +02:00
constructor(props : any) {
super(props);
}
render() {
return (
2022-06-20 22:03:20 +02:00
<div id="main">
2022-06-16 03:12:22 +02:00
<h1 className="title scripthaus-logo-small">
<div className="title-cursor">&#9608;</div>
ScriptHaus
</h1>
2022-06-20 22:03:20 +02:00
<div className="main-content">
<MainSideBar/>
2022-07-11 23:43:18 +02:00
<SessionView/>
2022-06-20 22:03:20 +02:00
</div>
2022-08-18 09:39:06 +02:00
<If condition={GlobalModel.remotesModalOpen.get()}>
<RemoteModal/>
</If>
2022-06-16 03:12:22 +02:00
</div>
);
}
}
2022-06-08 02:25:35 +02:00
export {Main};
2022-07-07 22:27:44 +02:00