waveterm/src/main.tsx

1591 lines
59 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";
import {debounce} from "throttle-debounce";
2022-07-14 08:11:45 +02:00
import {v4 as uuidv4} from "uuid";
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 21:00:53 +02:00
import type {SessionDataType, LineType, CmdDataType, RemoteType, RemoteStateType, RemoteInstanceType, RemotePtrType, HistoryItem, HistoryQueryOpts} from "./types";
import localizedFormat from 'dayjs/plugin/localizedFormat';
import {GlobalModel, GlobalCommandRunner, Session, Cmd, Window, Screen, ScreenWindow, riToRPtr, widthToCols} from "./model";
dayjs.extend(localizedFormat)
2022-06-13 20:12:39 +02:00
const CellHeightPx = 16;
const CellWidthPx = 8;
const RemotePtyRows = 8;
const RemotePtyCols = 80;
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}, {}> {
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 {
let curVis = this.termLoaded.get();
if (vis && !curVis) {
2022-08-13 03:34:56 +02:00
this.loadTerminal();
}
else if (!vis && curVis) {
this.unloadTerminal();
2022-08-13 03:34:56 +02:00
}
}
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))();
}
unloadTerminal() : void {
let {sw, line} = this.props;
let model = GlobalModel;
let cmd = model.getCmd(line);
if (cmd == null) {
return;
}
let termId = "term-" + getLineId(line);
sw.disconnectElem(line.cmdid);
mobx.action(() => this.termLoaded.set(false))();
let termElem = document.getElementById(termId);
if (termElem != null) {
termElem.replaceChildren();
}
}
2022-08-13 03:34:56 +02:00
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 termWidth = Math.max(Math.trunc((width - 20)/CellWidthPx), 10);
2022-08-13 03:34:56 +02:00
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 lineNumStr = (line.linenumtemp ? "~" : "") + String(line.linenum);
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">
<div className={cn("avatar", "num-"+lineNumStr.length, "status-" + status, {"ephemeral": line.ephemeral})} onClick={this.doRefresh}>
{lineNumStr}
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}, {}> {
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();
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.resetInput();
return;
}
2022-08-31 02:05:35 +02:00
if (e.code == "KeyR" && e.getModifierState("Control")) {
e.preventDefault();
inputModel.openHistory();
2022-08-31 02:05:35 +02:00
return;
}
if (e.code == "ArrowUp" || e.code == "ArrowDown") {
if (!inputModel.isHistoryLoaded()) {
if (e.code == "ArrowUp") {
2022-08-31 22:29:59 +02:00
this.lastHistoryUpDown = true;
inputModel.loadHistory(false, 1, "window");
}
2022-08-31 02:05:35 +02:00
return;
}
// invisible history movement
let linePos = this.getLinePos(e.target);
if (e.code == "ArrowUp") {
if (!lastHist && linePos.linePos > 1) {
// regular arrow
return;
}
e.preventDefault();
inputModel.moveHistorySelection(1);
this.lastHistoryUpDown = true;
return;
}
if (e.code == "ArrowDown") {
if (!lastHist && linePos.linePos < linePos.numLines) {
// regular arrow
return;
}
e.preventDefault();
inputModel.moveHistorySelection(-1);
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
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
@boundMethod
onHistoryKeyDown(e : any) {
let inputModel = GlobalModel.inputModel;
if (e.code == "Escape") {
e.preventDefault();
inputModel.resetHistory();
return;
}
if (e.code == "Enter") {
e.preventDefault();
inputModel.grabSelectedHistoryItem();
return;
}
if (e.code == "KeyC" && e.getModifierState("Control")) {
e.preventDefault();
inputModel.resetInput();
return;
}
2022-08-31 22:29:59 +02:00
if (e.code == "KeyM" && (e.getModifierState("Meta") || e.getModifierState("Control"))) {
2022-08-31 09:02:16 +02:00
e.preventDefault();
let opts = mobx.toJS(inputModel.historyQueryOpts.get());
opts.includeMeta = !opts.includeMeta;
inputModel.setHistoryQueryOpts(opts);
2022-08-31 21:00:53 +02:00
return;
}
2022-08-31 22:29:59 +02:00
if (e.code == "KeyR" && ((e.getModifierState("Meta") || e.getModifierState("Control")) && !e.getModifierState("Shift"))) {
2022-08-31 21:00:53 +02:00
console.log("meta-r");
e.preventDefault();
let opts = mobx.toJS(inputModel.historyQueryOpts.get());
if (opts.limitRemote) {
opts.limitRemote = false;
opts.limitRemoteInstance = false;
}
else {
opts.limitRemote = true;
opts.limitRemoteInstance = true;
}
inputModel.setHistoryQueryOpts(opts);
return;
2022-08-31 09:02:16 +02:00
}
2022-08-31 22:29:59 +02:00
if (e.code == "KeyS" && (e.getModifierState("Meta") || e.getModifierState("Control"))) {
e.preventDefault();
let opts = mobx.toJS(inputModel.historyQueryOpts.get());
let htype = opts.queryType;
if (htype == "window") {
htype = "session";
}
else if (htype == "session") {
htype = "global";
}
else {
htype = "window";
}
inputModel.setHistoryType(htype);
return;
}
if (e.code == "Tab") {
e.preventDefault();
return;
}
if (e.code == "ArrowUp" || e.code == "ArrowDown") {
e.preventDefault();
inputModel.moveHistorySelection(e.code == "ArrowUp" ? 1 : -1);
return;
}
if (e.code == "PageUp" || e.code == "PageDown") {
e.preventDefault();
inputModel.moveHistorySelection(e.code == "PageUp" ? 10 : -10);
return;
}
}
@boundMethod
handleHistoryInput(e : any) {
let inputModel = GlobalModel.inputModel;
mobx.action(() => {
2022-08-31 09:02:16 +02:00
let opts = mobx.toJS(inputModel.historyQueryOpts.get());
opts.queryStr = e.target.value;
inputModel.setHistoryQueryOpts(opts);
})();
}
@boundMethod
handleMainFocus(e : any) {
let inputModel = GlobalModel.inputModel;
if (inputModel.historyShow.get()) {
e.preventDefault();
inputModel.giveFocus();
}
}
@boundMethod
handleHistoryFocus(e : any) {
let inputModel = GlobalModel.inputModel;
if (!inputModel.historyShow.get()) {
e.preventDefault();
inputModel.giveFocus();
}
}
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;
}
let disabled = inputModel.historyShow.get();
2022-08-31 21:00:53 +02:00
if (disabled) {
displayLines = 1;
}
return (
<div className="control cmd-input-control is-expanded">
<textarea spellCheck="false" id="main-cmd-input" onFocus={this.handleMainFocus} rows={displayLines} value={curLine} onKeyDown={this.onKeyDown} onChange={this.onChange} className={cn("textarea", {"display-disabled": disabled})}></textarea>
<input spellCheck="false" className="history-input" type="text" onFocus={this.handleHistoryFocus} onKeyDown={this.onHistoryKeyDown} onChange={this.handleHistoryInput} value={inputModel.historyQueryOpts.get().queryStr}/>
</div>
);
}
}
2022-09-15 02:14:27 +02:00
@mobxReact.observer
class InfoMsg extends React.Component<{}, {}> {
getAfterSlash(s : string) : string {
if (s.startsWith("^/")) {
return s.substr(1);
}
let slashIdx = s.lastIndexOf("/");
if (slashIdx == s.length-1) {
slashIdx = s.lastIndexOf("/", slashIdx-1);
}
if (slashIdx == -1) {
return s;
}
return s.substr(slashIdx+1);
}
@boundMethod
clickTermBlock(e : any) {
let inputModel = GlobalModel.inputModel;
if (inputModel.remoteTermWrap != null) {
inputModel.remoteTermWrap.terminal.focus();
}
}
2022-09-15 09:37:52 +02:00
@boundMethod
connectRemote(remoteId : string) {
GlobalCommandRunner.connectRemote(remoteId);
}
@boundMethod
disconnectRemote(remoteId : string) {
GlobalCommandRunner.disconnectRemote(remoteId);
}
renderConnectButton(remote : RemoteType) : any {
2022-09-16 02:44:58 +02:00
if (remote.status == "connected" || remote.status == "connecting") {
return <div onClick={() => this.disconnectRemote(remote.remoteid)} className="text-button disconnect-button">[disconnect remote]</div>
}
else {
return <div onClick={() => this.connectRemote(remote.remoteid)} className="text-button connect-button">[connect remote]</div>
}
}
2022-09-15 02:14:27 +02:00
render() {
let model = GlobalModel;
let inputModel = model.inputModel;
let infoMsg = inputModel.infoMsg.get();
let infoShow = inputModel.infoShow.get();
let line : string = null;
let istr : string = null;
let idx : number = 0;
let ptyRemoteId = (infoMsg == null ? null : infoMsg.ptyremoteid);
let isTermFocused = (inputModel.remoteTermWrap == null ? false : inputModel.remoteTermWrap.isFocused.get());
2022-09-15 09:37:52 +02:00
let remote : RemoteType;
if (ptyRemoteId != null) {
remote = GlobalModel.getRemote(ptyRemoteId);
}
2022-09-15 02:14:27 +02:00
return (
<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>
<If condition={infoMsg && infoMsg.infolines != null}>
<div className="info-lines">
<For index="idx" each="line" of={infoMsg.infolines}>
<div key={idx}>{line == "" ? " " : line}</div>
</For>
</div>
</If>
2022-09-15 09:37:52 +02:00
<If condition={ptyRemoteId != null && remote != null}>
<div className="info-remote">
<div className="remote-field">
<div className="remote-field-def"> remoteid</div>
<div className="remote-field-val">{remote.remoteid}</div>
</div>
<div className="remote-field">
<div className="remote-field-def"> type</div>
<div className="remote-field-val">{remote.remotetype}</div>
</div>
<div className="remote-field">
<div className="remote-field-def"> alias</div>
<div className="remote-field-val">{isBlank(remote.remotealias) ? "-" : remote.remotealias}</div>
</div>
<div className="remote-field">
<div className="remote-field-def"> canonicalname</div>
<div className="remote-field-val">{remote.remotecanonicalname}</div>
</div>
<div className="remote-field">
<div className="remote-field-def"> connectmode</div>
<div className="remote-field-val">{remote.connectmode}</div>
</div>
<div className="remote-field">
<div className="remote-field-def"> status</div>
<div className="remote-field-val">{remote.status} | {this.renderConnectButton(remote)}</div>
</div>
<If condition={!isBlank(remote.errorstr)}>
<div className="remote-field">
<div className="remote-field-def"> error</div>
<div className="remote-field-val">{remote.errorstr}</div>
</div>
2022-09-15 09:37:52 +02:00
</If>
</div>
</If>
2022-09-16 21:02:46 +02:00
<div className={cn("terminal-wrapper", {"focus": isTermFocused}, (remote != null ? "status-" + remote.status : null))} style={{overflowY: "hidden", display: (ptyRemoteId == null ? "none" : "block"), width: CellWidthPx*RemotePtyCols+15}}>
<If condition={!isTermFocused}>
<div className="term-block" onClick={this.clickTermBlock}></div>
</If>
2022-09-16 21:02:46 +02:00
<If condition={inputModel.showNoInputMsg.get()}>
<div className="term-tag">input is only allowed while status is 'connecting'</div>
</If>
<div className="terminal" id="term-remote" data-remoteid={ptyRemoteId} style={{height: CellHeightPx*RemotePtyRows}}></div>
</div>
2022-09-15 02:14:27 +02:00
<If condition={infoMsg && infoMsg.infocomps != null && infoMsg.infocomps.length > 0}>
<div className="info-comps">
<For each="istr" index="idx" of={infoMsg.infocomps}>
<div key={idx} className={cn("info-comp", {"metacmd-comp": istr.startsWith("^")})}>
{this.getAfterSlash(istr)}
</div>
</For>
<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-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;
containingText : mobx.IObservableValue<string> = mobx.observable.box("");
2022-08-31 02:05:35 +02:00
2022-08-31 00:25:51 +02:00
componentDidMount() {
let inputModel = GlobalModel.inputModel;
let hitem = inputModel.getHistorySelectedItem();
if (hitem == null) {
hitem = inputModel.getFirstHistoryItem();
}
if (hitem != null) {
inputModel.scrollHistoryItemIntoView(hitem.historynum);
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 selItem = inputModel.getHistorySelectedItem();
if (this.lastClickHNum == hitem.historynum && selItem != null && selItem.historynum == hitem.historynum) {
2022-08-31 02:05:35 +02:00
inputModel.grabSelectedHistoryItem();
return;
}
inputModel.giveFocus();
2022-08-31 02:05:35 +02:00
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);
}
2022-08-31 21:00:53 +02:00
renderRemote(hitem : HistoryItem) : any {
if (hitem.remote == null || isBlank(hitem.remote.remoteid)) {
return sprintf("%-15s ", "")
}
let r = GlobalModel.getRemote(hitem.remote.remoteid);
if (r == null) {
return sprintf("%-15s ", "???")
}
let rname = "";
if (!isBlank(r.remotealias)) {
rname = r.remotealias;
}
else {
rname = r.remotecanonicalname;
}
if (!isBlank(hitem.remote.name)) {
rname = rname + ":" + hitem.remote.name;
}
let rtn = sprintf("%-15s ", "[" + rname + "]")
return rtn;
}
renderHItem(hitem : HistoryItem, opts : HistoryQueryOpts, isSelected : boolean) : any {
2022-08-31 00:25:51 +02:00
let lines = hitem.cmdstr.split("\n");
let line : string = "";
let idx = 0;
2022-08-31 21:00:53 +02:00
let limitRemote = opts.limitRemote;
2022-08-31 22:29:59 +02:00
let sessionStr = "";
if (opts.queryType == "global") {
if (!isBlank(hitem.sessionid)) {
let s = GlobalModel.getSessionById(hitem.sessionid);
if (s != null) {
sessionStr = s.name.get();
if (sessionStr.indexOf(" ") != -1) {
sessionStr = "[" + sessionStr + "]";
}
sessionStr = sprintf("#%-15s ", sessionStr);
}
}
}
2022-08-31 00:25:51 +02:00
return (
2022-08-31 09:02:16 +02:00
<div key={hitem.historynum} className={cn("history-item", {"is-selected": isSelected}, {"history-haderror": hitem.haderror}, "hnum-" + hitem.historynum)} onClick={() => this.handleItemClick(hitem)}>
2022-08-31 22:29:59 +02:00
<div className="history-line">{(isSelected ? "*" : " ")}{sprintf("%5s", hitem.historynum)} {opts.queryType == "global" ? sessionStr : ""}{!limitRemote ? this.renderRemote(hitem) : ""} {lines[0]}</div>
<For each="line" index="idx" of={lines.slice(1)}>
2022-08-31 00:25:51 +02:00
<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 selItem = inputModel.getHistorySelectedItem();
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 09:02:16 +02:00
let opts = inputModel.historyQueryOpts.get();
2022-08-31 00:25:51 +02:00
return (
<div className="cmd-history">
<div className="history-title">
2022-08-31 21:00:53 +02:00
<div>history</div>
<div className="spacer"></div>
2022-08-31 22:29:59 +02:00
<div className="history-opt">[for {opts.queryType} &#x2318;S]</div>
2022-08-31 21:00:53 +02:00
<div className="spacer"></div>
<div className="history-opt">[containing '{opts.queryStr}']</div>
<div className="spacer"></div>
<div className="history-opt">[{opts.limitRemote ? "this" : "any"} remote &#x2318;R]</div>
<div className="spacer"></div>
<div className="history-opt">[{opts.includeMeta ? "" : "no "}metacmds &#x2318;M]</div>
<div className="grow-spacer"></div>
<div className="history-clickable-opt" onClick={this.handleClose}>(ESC)</div>
<div className="spacer"></div>
2022-08-31 00:25:51 +02:00
</div>
2022-08-31 22:29:59 +02:00
<div className={cn("history-items", {"show-remotes": !opts.limitRemote}, {"show-sessions": opts.queryType == "global"})}>
2022-08-31 00:25:51 +02:00
<If condition={hitems.length == 0}>
[no history]
</If>
<If condition={hitems.length > 0}>
<For each="hitem" index="idx" of={hitems}>
2022-08-31 21:00:53 +02:00
{this.renderHItem(hitem, opts, (hitem == selItem))}
2022-08-31 00:25:51 +02:00
</For>
</If>
</div>
</div>
);
}
}
@mobxReact.observer
class CmdInput extends React.Component<{}, {}> {
2022-09-17 01:37:54 +02:00
@boundMethod
onInfoToggle() : void {
GlobalModel.inputModel.toggleInfoMsg();
return;
}
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 infoShow = inputModel.infoShow.get();
let historyShow = !infoShow && inputModel.historyShow.get();
2022-09-17 01:37:54 +02:00
let hasInfo = (inputModel.infoMsg.get() != null);
2022-06-08 02:25:35 +02:00
return (
2022-08-31 21:00:53 +02:00
<div className={cn("box cmd-input has-background-black", {"has-info": infoShow}, {"has-history": historyShow})}>
2022-09-17 01:37:54 +02:00
<div onClick={this.onInfoToggle} className="input-minmax-control">
<If condition={infoShow || historyShow}>
<i className="fa fa-chevron-down"/>
</If>
<If condition={!(infoShow || historyShow) && hasInfo}>
<i className="fa fa-chevron-up"/>
</If>
</div>
2022-08-31 00:25:51 +02:00
<If condition={historyShow}>
2022-08-31 21:00:53 +02:00
<div className="cmd-input-grow-spacer"></div>
2022-08-31 00:25:51 +02:00
<HistoryInfo/>
</If>
2022-09-15 02:14:27 +02:00
<InfoMsg/>
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>
<TextAreaInput/>
2022-06-08 02:25:35 +02:00
<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>
);
}
}
// sw is not null
2022-06-08 02:25:35 +02:00
@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;
lastHeight : number = null;
width : mobx.IObservableValue<number> = mobx.observable.box(0);
setWidth_debounced : (width : number) => void;
constructor(props : any) {
super(props);
this.setWidth_debounced = debounce(1000, this.setWidth.bind(this));
}
setWidth(width : number) : void {
mobx.action(() => {
this.width.set(width);
let {sw} = this.props;
let cols = widthToCols(width);
if (sw == null || cols == 0) {
return;
}
sw.colsCallback(cols);
})();
}
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));
if (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.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) {
let width = wvElem.offsetWidth;
this.setWidth(width);
this.rszObs = new ResizeObserver(this.handleResize.bind(this));
this.rszObs.observe(wvElem);
2022-07-14 08:11:45 +02:00
}
}
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.setWidth_debounced(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.shouldFollow.get()) {
setTimeout(() => this.scrollToBottom(reason), 0);
}
}
getWindow() : Window {
2022-07-13 08:29:39 +02:00
let {sw} = this.props;
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.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;
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()} data-windowid={sw.windowId}>
2022-07-14 08:11:45 +02:00
<div key="window-tag" className="window-tag">
<span>{sw.name.get()}{sw.shouldFollow.get() ? "*" : ""}</span>
2022-07-14 08:11:45 +02:00
</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;
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 isActive = sw.isActive();
2022-06-08 02:25:35 +02:00
return (
<div className="window-view" style={this.getWindowViewStyle()} id={this.getWindowViewDOMId()}>
<div key="window-tag" className={cn("window-tag", {"is-active": isActive})}>
<span>
{sw.name.get()}
<If condition={sw.shouldFollow.get()}>
&nbsp;<i className="fa fa-caret-down"/>
</If>
</span>
2022-07-14 08:11:45 +02:00
</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}/>
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;
let sw : ScreenWindow = null;
if (screen != null) {
sw = screen.getActiveSW();
}
if (screen == null || sw == null) {
2022-07-13 08:29:39 +02:00
return (
<div className="screen-view">
(no screen or window)
2022-07-13 08:29:39 +02:00
</div>
);
}
return (
<div className="screen-view" data-screenid={sw.screenId}>
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" data-sessionid={session.sessionId}>
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>
);
}
}
function getConnVal(r : RemoteType) : number {
if (r.status == "connected") {
return 1;
}
if (r.status == "init" || r.status == "disconnected") {
return 2;
}
if (r.status == "error") {
return 3;
}
return 4;
}
2022-09-22 07:17:04 +02:00
@mobxReact.observer
class RemoteStatusLight extends React.Component<{status : string}, {}> {
render() {
let status = this.props.status;
return (
<i className={cn("remote-status fa", "status-" + status, (status == "connecting" ? "fa-refresh" : "fa-circle"))}/>
);
}
}
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-09-14 02:17:52 +02:00
remoteDisplayName(remote : RemoteType) : any {
if (!isBlank(remote.remotealias)) {
return (
<>
<span>{remote.remotealias}</span>
<span className="small-text"> {remote.remotecanonicalname}</span>
</>
);
}
return (<span>{remote.remotecanonicalname}</span>);
}
2022-09-14 22:02:33 +02:00
clickRemote(remote : RemoteType) {
GlobalCommandRunner.showRemote(remote.remoteid);
}
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;
2022-09-14 02:17:52 +02:00
let remotes = model.remotes ?? [];
let remote : RemoteType = null;
let idx : number = 0;
2022-09-22 07:17:04 +02:00
remotes = sortAndFilterRemotes(remotes);
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 remotes-menu-list">
<For each="remote" of={remotes}>
2022-09-14 22:02:33 +02:00
<li key={remote.remoteid} className="remote-menu-item"><a onClick={() => this.clickRemote(remote)}>
2022-09-22 07:17:04 +02:00
<RemoteStatusLight status={remote.status}/>
2022-09-14 02:17:52 +02:00
{this.remoteDisplayName(remote)}
</a></li>
</For>
2022-06-20 22:03:20 +02:00
</ul>
<div className="bottom-spacer"></div>
</div>
</div>
);
}
}
2022-09-22 07:17:04 +02:00
function sortAndFilterRemotes(origRemotes : RemoteType[]) : RemoteType[] {
let remotes = origRemotes.filter((r) => !r.archived);
remotes.sort((a, b) => {
let connValA = getConnVal(a);
let connValB = getConnVal(b);
if (connValA != connValB) {
return connValA - connValB;
}
return a.remoteidx - b.remoteidx;
});
return remotes;
}
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;
2022-09-22 07:17:04 +02:00
let remotes = sortAndFilterRemotes(model.remotes);
2022-08-18 09:39:06 +02:00
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">
2022-09-22 07:17:04 +02:00
<div><RemoteStatusLight status={remote.status}/></div>
2022-08-18 09:39:06 +02:00
</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