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, throttle} from "throttle-debounce"; import {v4 as uuidv4} from "uuid"; import dayjs from "dayjs"; import {If, For, When, Otherwise, Choose} from "tsx-control-statements/components"; import cn from "classnames"; import {TermWrap} from "./term"; import type {SessionDataType, LineType, CmdDataType, RemoteType, RemoteStateType, RemoteInstanceType, RemotePtrType, HistoryItem, HistoryQueryOpts, RemoteEditType} from "./types"; import localizedFormat from 'dayjs/plugin/localizedFormat'; import {GlobalModel, GlobalCommandRunner, Session, Cmd, Window, Screen, ScreenWindow, riToRPtr, widthToCols, termWidthFromCols, termHeightFromRows} from "./model"; dayjs.extend(localizedFormat) const RemotePtyRows = 8; const RemotePtyCols = 80; const PasswordUnchangedSentinel = "--unchanged--"; const LinesVisiblePadding = 500; const RemoteColors = ["red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"]; type OV = mobx.IObservableValue; type OArr = mobx.IObservableArray; type OMap = mobx.ObservableMap; type HeightChangeCallbackType = (lineNum : number, newHeight : number, oldHeight : number) => void; type InterObsValue = { sessionid : string, windowid : string, lineid : string, cmdid : string, visible : mobx.IObservableValue, timeoutid? : any, }; function isBlank(s : string) : boolean { return (s == null || s == ""); } 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"}); } 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; } 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"); } } @mobxReact.observer class LineText extends React.Component<{sw : ScreenWindow, line : LineType}, {}> { render() { let {sw, line} = this.props; let formattedTime = getLineDateStr(line.ts); let isSelected = (sw.selectedLine.get() == line.linenum); let isFocused = (sw.focusType.get() == "lines"); return (
S
{line.userid}
{formattedTime}
{line.text}
); } } @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 ( [{remoteStr}] {cwd} {isRoot ? "#" : "$"} ); } } @mobxReact.observer class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType, width : number, staticRender: boolean, visible : OV, onHeightChange : HeightChangeCallbackType}, {}> { termLoaded : mobx.IObservableValue = mobx.observable.box(false); lineRef : React.RefObject = React.createRef(); constructor(props) { super(props); } checkLoad() : void { let {line, staticRender, visible} = this.props; if (staticRender) { return; } let vis = visible && visible.get(); let curVis = this.termLoaded.get(); if (vis && !curVis) { this.loadTerminal(); } else if (!vis && curVis) { this.unloadTerminal(false); } } loadTerminal() : void { let {sw, line} = this.props; let model = GlobalModel; let cmd = model.getCmd(line); 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(unmount : boolean) : void { let {sw, line} = this.props; sw.disconnectElem(line.cmdid); if (!unmount) { mobx.action(() => this.termLoaded.set(false))(); let termId = "term-" + getLineId(line); let termElem = document.getElementById(termId); if (termElem != null) { termElem.replaceChildren(); } } } componentDidMount() { } componentWillUnmount() { if (this.termLoaded.get()) { this.unloadTerminal(true); } } // FIXME scrollIntoView() { let lineElem = document.getElementById("line-" + getLineId(this.props.line)); lineElem.scrollIntoView({block: "end"}); } @boundMethod doRefresh() { let {sw, line} = this.props; let model = GlobalModel; let termWrap = sw.getTermWrap(line.cmdid); if (termWrap != null) { termWrap.reloadTerminal(500); } } renderCmdText(cmd : Cmd, remote : RemoteType) : any { if (cmd == null) { return (
(cmd not found)
); } let remoteStr = getRemoteStr(cmd.remote); let cwd = getCwdStr(remote, cmd.getRemoteState()); return (
{cmd.getSingleLineCmdText()}
); } @boundMethod clickTermBlock(e : any) { let {sw, line} = this.props; let model = GlobalModel; let termWrap = sw.getTermWrap(line.cmdid); if (termWrap != null) { termWrap.terminal.focus(); } } getSnapshotBeforeUpdate(prevProps, prevState) : {height : number} { let elem = this.lineRef.current; if (elem == null) { return {height: 0}; } return {height: elem.offsetHeight}; } componentDidUpdate(prevProps, prevState, snapshot : {height : number}) : void { let {line} = this.props; let curHeight = 0; let elem = this.lineRef.current; if (elem != null) { curHeight = elem.offsetHeight; } if (snapshot.height != curHeight && this.props.onHeightChange != null) { this.props.onHeightChange(line.linenum, curHeight, snapshot.height); } this.checkLoad(); } render() { let {sw, line, width, staticRender, visible} = this.props; let model = GlobalModel; let lineid = line.lineid; let formattedTime = getLineDateStr(line.ts); let cmd = model.getCmd(line); if (cmd == null) { return (
[cmd not found '{line.cmdid}']
); } let termLoaded = this.termLoaded.get(); let usedRows = sw.getUsedRows(cmd, width); let termHeight = termHeightFromRows(usedRows); let remote = model.getRemote(cmd.remoteId); let status = cmd.getStatus(); let termOpts = cmd.getTermOpts(); let isFocused = sw.getIsFocused(line.cmdid); let lineNumStr = (line.linenumtemp ? "~" : "") + String(line.linenum); let isSelected = (sw.selectedLine.get() == line.linenum); let isStatic = staticRender; let isVisible = visible.get(); return (
{lineNumStr}
{line.userid}
{formattedTime}
{line.cmdid} ({termOpts.rows}x{termOpts.cols})
{this.renderCmdText(cmd, remote)}
(loading)
); } } @mobxReact.observer class Line extends React.Component<{sw : ScreenWindow, line : LineType, width : number, staticRender : boolean, visible : OV, onHeightChange : HeightChangeCallbackType}, {}> { render() { let line = this.props.line; if (line.linetype == "text") { return ; } if (line.linetype == "cmd") { return ; } return
[invalid line type '{line.linetype}']
; } } @mobxReact.observer class TextAreaInput extends React.Component<{}, {}> { lastTab : boolean = false; lastHistoryUpDown : boolean = false; lastTabCurLine : mobx.IObservableValue = mobx.observable.box(null); 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}; } @mobx.action @boundMethod onKeyDown(e : any) { mobx.action(() => { if (this.isModKeyPress(e)) { return; } let model = GlobalModel; let inputModel = model.inputModel; let win = model.getActiveWindow(); 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") { e.preventDefault(); if (!ctrlMod) { setTimeout(() => GlobalModel.inputModel.uiSubmitCommand(), 0); return; } e.target.setRangeText("\n", e.target.selectionStart, e.target.selectionEnd, "end"); GlobalModel.inputModel.setCurLine(e.target.value); return; } if (e.code == "Escape") { e.preventDefault(); GlobalModel.inputModel.toggleInfoMsg(); return; } if (e.code == "KeyC" && e.getModifierState("Control")) { e.preventDefault(); inputModel.resetInput(); return; } if (e.code == "KeyR" && e.getModifierState("Control")) { e.preventDefault(); inputModel.openHistory(); return; } if (e.code == "ArrowUp" || e.code == "ArrowDown") { if (!inputModel.isHistoryLoaded()) { if (e.code == "ArrowUp") { this.lastHistoryUpDown = true; inputModel.loadHistory(false, 1, "window"); } 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; } } if (e.code == "PageUp" || e.code == "PageDown") { e.preventDefault(); let infoScroll = inputModel.hasScrollingInfoMsg(); if (infoScroll) { let div = document.querySelector(".cmd-input-info"); let amt = pageSize(div); scrollDiv(div, (e.code == "PageUp" ? -amt : amt)); } } // console.log(e.code, e.keyCode, e.key, event.which, ctrlMod, e); })(); } @boundMethod onChange(e : any) { mobx.action(() => { GlobalModel.inputModel.setCurLine(e.target.value); })(); } @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; } if (e.code == "KeyM" && (e.getModifierState("Meta") || e.getModifierState("Control"))) { e.preventDefault(); let opts = mobx.toJS(inputModel.historyQueryOpts.get()); opts.includeMeta = !opts.includeMeta; inputModel.setHistoryQueryOpts(opts); return; } if (e.code == "KeyR" && ((e.getModifierState("Meta") || e.getModifierState("Control")) && !e.getModifierState("Shift"))) { 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; } 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(() => { 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(); } else { inputModel.setPhysicalInputFocused(true); } } @boundMethod handleMainBlur(e : any) { GlobalModel.inputModel.setPhysicalInputFocused(false); } @boundMethod handleHistoryFocus(e : any) { let inputModel = GlobalModel.inputModel; if (!inputModel.historyShow.get()) { e.preventDefault(); inputModel.giveFocus(); } else { inputModel.setPhysicalInputFocused(true); } } @boundMethod handleHistoryBlur(e : any) { GlobalModel.inputModel.setPhysicalInputFocused(false); } 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(); if (disabled) { displayLines = 1; } return (
); } } @mobxReact.observer class InfoRemoteShowAll extends React.Component<{}, {}> { clickRow(remoteId : string) : void { GlobalCommandRunner.showRemote(remoteId); } render() { let inputModel = GlobalModel.inputModel; let infoMsg = inputModel.infoMsg.get(); if (infoMsg == null || !infoMsg.remoteshowall) { return null; } let remotes = GlobalModel.remotes ?? []; let remote : RemoteType = null; let idx : number = 0; remotes = sortAndFilterRemotes(remotes); return (
show all remotes
this.clickRow(remote.remoteid)}>
status id alias user@host connectmode
{remote.status}
{remote.remoteid.substr(0, 8)} {isBlank(remote.remotealias) ? "-" : remote.remotealias} {remote.remotecanonicalname} {remote.connectmode}
); } } @mobxReact.observer class InfoRemoteShow extends React.Component<{}, {}> { getRemoteTypeStr(remote : RemoteType) : string { let mshellStr = ""; if (!isBlank(remote.mshellversion)) { mshellStr = "mshell=" + remote.mshellversion; } if (!isBlank(remote.uname)) { if (mshellStr != "") { mshellStr += " "; } mshellStr += "uname=\"" + remote.uname + "\""; } if (mshellStr == "") { return remote.remotetype; } return remote.remotetype + " (" + mshellStr + ")"; } @boundMethod connectRemote(remoteId : string) { GlobalCommandRunner.connectRemote(remoteId); } @boundMethod disconnectRemote(remoteId : string) { GlobalCommandRunner.disconnectRemote(remoteId); } @boundMethod installRemote(remoteId : string) { GlobalCommandRunner.installRemote(remoteId); } @boundMethod cancelInstall(remoteId : string) { GlobalCommandRunner.installCancelRemote(remoteId); } @boundMethod editRemote(remoteId : string) { GlobalCommandRunner.openEditRemote(remoteId); } renderConnectButton(remote : RemoteType) : any { if (remote.status == "connected" || remote.status == "connecting") { return
this.disconnectRemote(remote.remoteid)} className="text-button disconnect-button">[disconnect remote]
} else { return
this.connectRemote(remote.remoteid)} className="text-button connect-button">[connect remote]
} } renderEditButton(remote : RemoteType) : any { return
this.editRemote(remote.remoteid)} className="text-button">[edit remote]
} renderInstallButton(remote : RemoteType) : any { if (remote.status == "connected" || remote.status == "connecting") { return "(must disconnect to install)"; } if (remote.installstatus == "disconnected" || remote.installstatus == "error") { return
this.installRemote(remote.remoteid)} className="text-button connect-button">[run install]
} if (remote.installstatus == "connecting") { return
this.cancelInstall(remote.remoteid)} className="text-button disconnect-button">[cancel install]
} return null; } renderInstallStatus(remote : RemoteType) : any { let statusStr : string = null; if (remote.installstatus == "disconnected") { if (remote.needsmshellupgrade) { statusStr = "needs upgrade" } } else { statusStr = remote.installstatus; } if (statusStr == null) { return null; } let installButton = this.renderInstallButton(remote); return (
install-status
{statusStr} | {this.renderInstallButton(remote)}
); } @boundMethod clickTermBlock(e : any) { let inputModel = GlobalModel.inputModel; if (inputModel.remoteTermWrap != null) { inputModel.remoteTermWrap.terminal.focus(); } } getCanonicalNameDisplayWithPort(remote : RemoteType) { if (isBlank(remote.remotevars.port) || remote.remotevars.port == "22") { return remote.remotecanonicalname; } return remote.remotecanonicalname + " (port " + remote.remotevars.port + ")"; } render() { let inputModel = GlobalModel.inputModel; let infoMsg = inputModel.infoMsg.get(); let ptyRemoteId = (infoMsg == null ? null : infoMsg.ptyremoteid); let isTermFocused = (inputModel.remoteTermWrap == null ? false : inputModel.remoteTermWrap.isFocused.get()); let remote : RemoteType; if (ptyRemoteId != null) { remote = GlobalModel.getRemote(ptyRemoteId); } if (ptyRemoteId == null || remote == null) { return ( <>
); } return ( <>
show remote [{remote.remotecanonicalname}]
remoteid
{remote.remoteid} | {this.renderEditButton(remote)}
type
{this.getRemoteTypeStr(remote)}
canonicalname
{this.getCanonicalNameDisplayWithPort(remote)}
alias
{isBlank(remote.remotealias) ? "-" : remote.remotealias}
connectmode
{remote.connectmode}
status
{remote.status} | {this.renderConnectButton(remote)}
error
{remote.errorstr}
{this.renderInstallStatus(remote)}
install error
{remote.installerrorstr}
input is only allowed while status is 'connecting'
); } } @mobxReact.observer class InfoRemoteEdit extends React.Component<{}, {}> { alias : mobx.IObservableValue; hostName : mobx.IObservableValue; keyStr : mobx.IObservableValue; portStr : mobx.IObservableValue; passwordStr : mobx.IObservableValue; colorStr : mobx.IObservableValue; connectMode : mobx.IObservableValue; sudoBool : mobx.IObservableValue; autoInstallBool : mobx.IObservableValue; authMode : mobx.IObservableValue; archiveConfirm : mobx.IObservableValue = mobx.observable.box(false); constructor(props) { super(props); this.resetForm(); } getEditAuthMode(redit : RemoteEditType) : string { if (!isBlank(redit.keystr) && redit.haspassword) { return "key+pw"; } else if (!isBlank(redit.keystr)) { return "key"; } else if (redit.haspassword) { return "pw"; } else { return "none"; } } resetForm() { let redit = this.getRemoteEdit(); let remote = this.getEditingRemote(); if (redit == null) { return; } let isEditMode = !isBlank(redit.remoteid); if (isEditMode && remote == null) { return; } // not editable this.hostName = mobx.observable.box(""); this.portStr = mobx.observable.box(""); this.sudoBool = mobx.observable.box(false); // editable if (isEditMode) { this.authMode = mobx.observable.box(this.getEditAuthMode(redit)); this.alias = mobx.observable.box(remote.remotealias ?? ""); this.passwordStr = mobx.observable.box(redit.haspassword ? PasswordUnchangedSentinel : ""); this.keyStr = mobx.observable.box(redit.keystr ?? ""); this.colorStr = mobx.observable.box(remote.remotevars["color"] ?? ""); this.connectMode = mobx.observable.box(remote.connectmode); this.autoInstallBool = mobx.observable.box(remote.autoinstall); } else { this.authMode = mobx.observable.box("none"); this.alias = mobx.observable.box(""); this.passwordStr = mobx.observable.box(""); this.keyStr = mobx.observable.box(""); this.colorStr = mobx.observable.box(""); this.connectMode = mobx.observable.box("startup"); this.autoInstallBool = mobx.observable.box(true); } } canResetPw() : boolean { let redit = this.getRemoteEdit(); if (redit == null) { return false; } return redit.haspassword && this.passwordStr.get() != PasswordUnchangedSentinel; } @boundMethod resetPw() : void { mobx.action(() => { this.passwordStr.set(PasswordUnchangedSentinel); })(); } @boundMethod updateArchiveConfirm(e : any) : void { mobx.action(() => { this.archiveConfirm.set(e.target.checked); })(); } @boundMethod doArchiveRemote(e : any) { e.preventDefault(); if (!this.archiveConfirm.get()) { return; } let redit = this.getRemoteEdit(); if (redit == null || isBlank(redit.remoteid)) { return; } GlobalCommandRunner.archiveRemote(redit.remoteid); } @boundMethod doSubmitRemote() { let redit = this.getRemoteEdit(); let isEditing = !isBlank(redit.remoteid); let cname = this.hostName.get(); let kwargs : Record = {}; let authMode = this.authMode.get(); if (!isEditing) { if (this.sudoBool.get()) { kwargs["sudo"] = "1"; } } kwargs["alias"] = this.alias.get(); kwargs["color"] = this.colorStr.get(); if (authMode == "key" || authMode == "key+pw") { kwargs["key"] = this.keyStr.get(); } else { kwargs["key"] = ""; } if (authMode == "pw" || authMode == "key+pw") { kwargs["password"] = this.passwordStr.get(); } else { kwargs["password"] = "" } kwargs["connectmode"] = this.connectMode.get(); kwargs["autoinstall"] = (this.autoInstallBool.get() ? "1" : "0"); kwargs["visual"] = "1"; kwargs["submit"] = "1"; console.log("submit remote", (isEditing ? redit.remoteid : cname), kwargs); mobx.action(() => { if (isEditing) { GlobalCommandRunner.editRemote(redit.remoteid, kwargs); } else { GlobalCommandRunner.createRemote(cname, kwargs); } })(); } @boundMethod doCancel() { mobx.action(() => { this.resetForm(); GlobalModel.inputModel.clearInfoMsg(true); })(); } @boundMethod keyDownCreateRemote(e : any) { if (e.code == "Enter") { this.doSubmitRemote(); } } @boundMethod keyDownCancel(e : any) { if (e.code == "Enter") { this.doCancel(); } } @boundMethod onChangeAlias(e : any) { mobx.action(() => { this.alias.set(e.target.value); })(); } @boundMethod onChangeHostName(e : any) { mobx.action(() => { this.hostName.set(e.target.value); })(); } @boundMethod onChangeKeyStr(e : any) { mobx.action(() => { this.keyStr.set(e.target.value); })(); } @boundMethod onChangePortStr(e : any) { mobx.action(() => { this.portStr.set(e.target.value); })(); } @boundMethod onChangePasswordStr(e : any) { mobx.action(() => { this.passwordStr.set(e.target.value); })(); } @boundMethod onFocusPasswordStr(e : any) { if (this.passwordStr.get() == PasswordUnchangedSentinel) { e.target.select(); } } @boundMethod onChangeColorStr(e : any) { mobx.action(() => { this.colorStr.set(e.target.value); })(); } @boundMethod onChangeConnectMode(e : any) { mobx.action(() => { this.connectMode.set(e.target.value); })(); } @boundMethod onChangeAuthMode(e : any) { mobx.action(() => { this.authMode.set(e.target.value); })(); } @boundMethod onChangeSudo(e : any) { mobx.action(() => { this.sudoBool.set(e.target.checked); })(); } @boundMethod onChangeAutoInstall(e : any) { mobx.action(() => { this.autoInstallBool.set(e.target.checked); })(); } getRemoteEdit() : RemoteEditType { let inputModel = GlobalModel.inputModel; let infoMsg = inputModel.infoMsg.get(); if (infoMsg == null) { return null; } return infoMsg.remoteedit; } getEditingRemote() : RemoteType { let inputModel = GlobalModel.inputModel; let infoMsg = inputModel.infoMsg.get(); if (infoMsg == null) { return null; } let redit = infoMsg.remoteedit; if (redit == null || isBlank(redit.remoteid)) { return null; } let remote = GlobalModel.getRemote(redit.remoteid); return remote; } remoteCName() : string { let redit = this.getRemoteEdit(); if (isBlank(redit.remoteid)) { // new-mode let hostName = this.hostName.get(); if (hostName == "") { return "[no host]"; } if (hostName.indexOf("@") == -1) { hostName = "[no user]@" + hostName; } if (!hostName.startsWith("sudo@") && this.sudoBool.get()) { return "sudo@" + hostName; } return hostName; } else { let remote = this.getEditingRemote(); if (remote == null) { return "[no remote]"; } return remote.remotecanonicalname; } } render() { let inputModel = GlobalModel.inputModel; let infoMsg = inputModel.infoMsg.get(); if (infoMsg == null || !infoMsg.remoteedit) { return null; } let redit = infoMsg.remoteedit; if (!redit.remoteedit) { return null; } let isEditMode = !isBlank(redit.remoteid); let remote = this.getEditingRemote(); if (isEditMode && remote == null) { return (
cannot edit, remote {redit.remoteid} not found
); } let colorStr : string = null; return (
add new remote '{this.remoteCName()}' edit remote '{this.remoteCName()}'
type
ssh
user@host
port
user@host
{remote.remotecanonicalname}  (port {remote.remotevars.port})
alias
authmode
ssh keyfile
ssh password
sudo
connectmode
autoinstall
color
{redit.errorstr}
{redit.infostr}
); } } @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); } 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 titleStr = null; let remoteEditKey = "inforemoteedit"; if (infoMsg != null) { titleStr = infoMsg.infotitle; if (infoMsg.remoteedit != null) { remoteEditKey += (infoMsg.remoteedit.remoteid == null ? "-new" : "-" + infoMsg.remoteedit.remoteid); } } return (
{titleStr}
{infoMsg.infomsg}
{line == "" ? " " : line}
0}>
{this.getAfterSlash(istr)}
...
[error] {infoMsg.infoerror}
); } } @mobxReact.observer class HistoryInfo extends React.Component<{}, {}> { lastClickHNum : string = null; lastClickTs : number = 0; containingText : mobx.IObservableValue = mobx.observable.box(""); componentDidMount() { let inputModel = GlobalModel.inputModel; let hitem = inputModel.getHistorySelectedItem(); if (hitem == null) { hitem = inputModel.getFirstHistoryItem(); } if (hitem != null) { inputModel.scrollHistoryItemIntoView(hitem.historynum); } } @boundMethod handleItemClick(hitem : HistoryItem) { let inputModel = GlobalModel.inputModel; let selItem = inputModel.getHistorySelectedItem(); if (this.lastClickHNum == hitem.historynum && selItem != null && selItem.historynum == hitem.historynum) { inputModel.grabSelectedHistoryItem(); return; } inputModel.giveFocus(); 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); } 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 { let lines = hitem.cmdstr.split("\n"); let line : string = ""; let idx = 0; let limitRemote = opts.limitRemote; 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); } } } return (
this.handleItemClick(hitem)}>
{(isSelected ? "*" : " ")}{sprintf("%5s", hitem.historynum)} {opts.queryType == "global" ? sessionStr : ""}{!limitRemote ? this.renderRemote(hitem) : ""} {lines[0]}
{line}
); } @boundMethod handleClose() { GlobalModel.inputModel.toggleInfoMsg(); } render() { let inputModel = GlobalModel.inputModel; let idx : number = 0; let selItem = inputModel.getHistorySelectedItem(); let hitems = inputModel.getFilteredHistoryItems(); hitems = hitems.slice().reverse(); let hitem : HistoryItem = null; let opts = inputModel.historyQueryOpts.get(); return (
history
[for {opts.queryType} ⌘S]
[containing '{opts.queryStr}']
[{opts.limitRemote ? "this" : "any"} remote ⌘R]
[{opts.includeMeta ? "" : "no "}metacmds ⌘M]
(ESC)
[no history] 0}> {this.renderHItem(hitem, opts, (hitem == selItem))}
); } } @mobxReact.observer class CmdInput extends React.Component<{}, {}> { @boundMethod onInfoToggle() : void { GlobalModel.inputModel.toggleInfoMsg(); return; } 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(); let infoMsg = inputModel.infoMsg.get(); let hasInfo = (infoMsg != null); let remoteShow = (infoMsg != null && !isBlank(infoMsg.ptyremoteid)); let focusVal = inputModel.physicalInputFocused.get(); return (
{remoteStr}
); } } @mobxReact.observer class LinesView extends React.Component<{sw : ScreenWindow, width : number, lines : LineType[]}, {}> { rszObs : any; linesRef : React.RefObject; staticRender : OV = mobx.observable.box(true); lastOffsetHeight : number = 0; lastOffsetWidth : number = 0; ignoreNextScroll : boolean = false; visibleMap : Map>; // lineid => OV lastSelectedLine : number = 0; lastLinesLength : number = 0; computeAnchorLine_throttled : () => void; computeVisibleMap_debounced : () => void; constructor(props) { super(props); this.linesRef = React.createRef(); this.computeAnchorLine_throttled = throttle(100, this.computeAnchorLine.bind(this), {noLeading: true, noTrailing: false}); this.visibleMap = new Map(); this.computeVisibleMap_debounced = debounce(1000, this.computeVisibleMap.bind(this)); } @boundMethod scrollHandler() { if (this.ignoreNextScroll) { this.ignoreNextScroll = false; return; } // console.log("scroll", this.linesRef.current.scrollTop); this.computeAnchorLine_throttled(); this.computeVisibleMap_debounced(); } computeAnchorLine() : void { let {sw} = this.props; let linesElem = this.linesRef.current; if (linesElem == null) { sw.anchorLine = null; sw.anchorOffset = 0; return; } let lineElemArr = linesElem.querySelectorAll(".line"); if (lineElemArr == null) { sw.anchorLine = null; sw.anchorOffset = 0; return; } let scrollTop = linesElem.scrollTop; let height = linesElem.clientHeight; let containerBottom = scrollTop + height; let anchorElem : HTMLElement = null; for (let i=lineElemArr.length-1; i >= 0; i--) { let lineElem = lineElemArr[i]; let bottomPos = lineElem.offsetTop + lineElem.offsetHeight; if (anchorElem == null && (bottomPos <= containerBottom || lineElem.offsetTop <= scrollTop)) { anchorElem = lineElem; } } if (anchorElem == null) { anchorElem = lineElemArr[0]; } sw.anchorLine = parseInt(anchorElem.dataset.linenum); sw.anchorOffset = containerBottom - (anchorElem.offsetTop + anchorElem.offsetHeight); // console.log("anchor", this.anchorLine, this.anchorOffset); } computeVisibleMap() : void { let linesElem = this.linesRef.current; if (linesElem == null) { return; } let lineElemArr = linesElem.querySelectorAll(".line"); if (lineElemArr == null) { return; } let containerTop = linesElem.scrollTop - LinesVisiblePadding; let containerBot = linesElem.scrollTop + linesElem.clientHeight + LinesVisiblePadding; let newMap = new Map(); for (let i=0; i { for (let [k, v] of newMap) { let oldVal = this.visibleMap.get(k); if (oldVal == null) { oldVal = mobx.observable.box(v); this.visibleMap.set(k, oldVal); } if (oldVal.get() != v) { oldVal.set(v); } } for (let [k, v] of this.visibleMap) { if (!newMap.has(k)) { this.visibleMap.delete(k); } } })(); } restoreAnchorOffset(reason : string) : void { let {sw} = this.props; let linesElem = this.linesRef.current; if (linesElem == null) { return; } if (sw.anchorLine == null || sw.anchorLine == 0) { return; } let anchorElem = linesElem.querySelector(sprintf(".line[data-linenum=\"%d\"]", sw.anchorLine)); if (anchorElem == null) { return; } let scrollTop = linesElem.scrollTop; let height = linesElem.clientHeight; let containerBottom = scrollTop + height; let curAnchorOffset = containerBottom - (anchorElem.offsetTop + anchorElem.offsetHeight); if (curAnchorOffset != sw.anchorOffset) { let offsetDiff = curAnchorOffset - sw.anchorOffset; let newScrollTop = scrollTop - offsetDiff; // console.log("update scrolltop", reason, "line=" + sw.anchorLine, -offsetDiff, linesElem.scrollTop, "=>", newScrollTop); linesElem.scrollTop = newScrollTop; this.ignoreNextScroll = true; } } componentDidMount() : void { let {sw, lines} = this.props; if (sw.anchorLine == null) { this.computeAnchorLine(); } else { this.restoreAnchorOffset("re-mount"); } this.lastSelectedLine = sw.selectedLine.get(); this.lastLinesLength = lines.length; let linesElem = this.linesRef.current; if (linesElem != null) { this.lastOffsetHeight = linesElem.offsetHeight; this.lastOffsetWidth = linesElem.offsetWidth; this.rszObs = new ResizeObserver(this.handleResize.bind(this)); this.rszObs.observe(linesElem); } mobx.action(() => { this.staticRender.set(false) this.computeVisibleMap(); })(); } getLineElem(lineNum : number) : HTMLElement { let linesElem = this.linesRef.current; if (linesElem == null) { return null; } let elem = linesElem.querySelector(sprintf(".line[data-linenum=\"%d\"]", lineNum)); return elem; } getLineViewInfo(lineNum : number) : {height: number, topOffset: number, botOffset: number, anchorOffset: number} { let linesElem = this.linesRef.current; if (linesElem == null) { return null; } let lineElem = this.getLineElem(lineNum); if (lineElem == null) { return null; } let rtn = { height: lineElem.offsetHeight, topOffset: 0, botOffset: 0, anchorOffset: 0, }; let containerTop = linesElem.scrollTop; let containerBot = linesElem.scrollTop + linesElem.clientHeight; let lineTop = lineElem.offsetTop; let lineBot = lineElem.offsetTop + lineElem.offsetHeight; if (lineTop < containerTop) { rtn.topOffset = lineTop - containerTop; } else if (lineTop > containerBot) { rtn.topOffset = lineTop - containerBot; } if (lineBot < containerTop) { rtn.botOffset = lineBot - containerTop; } else if (lineBot > containerBot) { rtn.botOffset = lineBot - containerBot; } rtn.anchorOffset = containerBot - lineBot; return rtn; } updateSelectedLine() : void { let {sw, lines} = this.props; let linesElem = this.linesRef.current; if (linesElem == null) { return null; } let newLine = sw.selectedLine.get(); if (newLine == sw.anchorLine) { return; } let viewInfo = this.getLineViewInfo(newLine); if (viewInfo == null) { return; } sw.anchorLine = newLine; sw.anchorOffset = viewInfo.anchorOffset; let isFirst = (newLine == lines[0].linenum); let isLast = (newLine == lines[lines.length-1].linenum); if (viewInfo.botOffset > 0) { linesElem.scrollTop = linesElem.scrollTop + viewInfo.botOffset + (isLast ? 10 : 0); this.ignoreNextScroll = true; sw.anchorOffset = (isLast ? 10 : 0); } else if (viewInfo.topOffset < 0) { linesElem.scrollTop = linesElem.scrollTop + viewInfo.topOffset + (isFirst ? -10 : 0); this.ignoreNextScroll = true; sw.anchorOffset = linesElem.clientHeight - viewInfo.height; } this.setLineVisible(newLine, true); } setLineVisible(lineNum : number, vis : boolean) : void { mobx.action(() => { let key = String(lineNum); let visObj = this.visibleMap.get(key); if (visObj == null) { visObj = mobx.observable.box(true); this.visibleMap.set(key, visObj); } else { visObj.set(true); } })(); } componentDidUpdate(prevProps, prevState, snapshot) : void { let {sw, lines} = this.props; if (sw.selectedLine.get() != this.lastSelectedLine) { this.updateSelectedLine(); this.lastSelectedLine = sw.selectedLine.get(); } else if (lines.length != this.lastLinesLength) { this.restoreAnchorOffset("line-length-change"); } } componentWillUnmount() : void { if (this.rszObs != null) { this.rszObs.disconnect(); } } handleResize(entries : any) { let linesElem = this.linesRef.current; if (linesElem == null) { return; } let heightDiff = linesElem.offsetHeight - this.lastOffsetHeight; if (heightDiff != 0) { linesElem.scrollTop = linesElem.scrollTop - heightDiff; this.lastOffsetHeight = linesElem.offsetHeight; this.ignoreNextScroll = true; } if (this.lastOffsetWidth != linesElem.offsetWidth) { this.restoreAnchorOffset("resize-width"); this.lastOffsetWidth = linesElem.offsetWidth; } this.computeVisibleMap_debounced(); } @boundMethod onHeightChange(lineNum : number, newHeight : number, oldHeight : number) : void { // console.log("height-change", lineNum, oldHeight, "=>", newHeight); this.restoreAnchorOffset("height-change"); this.computeVisibleMap_debounced(); } render() { let {sw, width, lines} = this.props; let selectedLine = sw.selectedLine.get(); // for re-rendering let line : LineType = null; let idx : number = 0; for (let i=0; i
); } } // sw is not null @mobxReact.observer class ScreenWindowView extends React.Component<{sw : ScreenWindow}, {}> { rszObs : any; windowViewRef : React.RefObject; width : mobx.IObservableValue = mobx.observable.box(0); setWidth_debounced : (width : number) => void; constructor(props : any) { super(props); this.setWidth_debounced = debounce(1000, this.setWidth.bind(this)); this.windowViewRef = React.createRef(); } 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); })(); } componentDidMount() { let wvElem = this.windowViewRef.current; if (wvElem != null) { let width = wvElem.offsetWidth; this.setWidth(width); this.rszObs = new ResizeObserver(this.handleResize.bind(this)); this.rszObs.observe(wvElem); } } componentWillUnmount() { if (this.rszObs) { this.rszObs.disconnect(); } } handleResize(entries : any) { if (entries.length == 0) { return; } let entry = entries[0]; let width = entry.target.offsetWidth; this.setWidth_debounced(width); } getWindow() : Window { let {sw} = this.props; let win = GlobalModel.getWindowById(sw.sessionId, sw.windowId); if (win == null) { win = GlobalModel.loadWindow(sw.sessionId, sw.windowId); } return win; } getWindowViewStyle() : any { return {position: "absolute", width: "100%", height: "100%", overflowX: "hidden"}; } renderError(message : string) { let {sw} = this.props; return (
{sw.name.get()}
{message}
); } render() { 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(""); } let idx = 0; let line : LineType = null; let screen = GlobalModel.getScreenById(sw.sessionId, sw.screenId); let session = GlobalModel.getSessionById(sw.sessionId); let isActive = sw.isActive(); let selectedLine = sw.selectedLine.get(); return (
{sw.name.get()}
0}>
[session="{session.name.get()}" screen="{screen.name.get()}" window="{sw.name.get()}"]
); } } @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) { return (
(no screen or window)
); } return (
); } } @mobxReact.observer class ScreenTabs extends React.Component<{session : Session}, {}> { @boundMethod handleNewScreen() { let {session} = this.props; GlobalCommandRunner.createNewScreen(); } @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; } GlobalCommandRunner.switchScreen(screenId); } handleContextMenu(e : any, screenId : string) : void { e.preventDefault(); console.log("handle context menu!", screenId); let model = GlobalModel; model.contextScreen(e, screenId); } render() { let {session} = this.props; if (session == null) { return null; } let screen : Screen = null; let index = 0; return (
this.handleSwitchScreen(screen.screenId)} onContextMenu={(event) => this.handleContextMenu(event, screen.screenId)}> {screen.name.get()}
⌘{index+1}
+
); } } @mobxReact.observer class SessionView extends React.Component<{}, {}> { render() { let model = GlobalModel; let session = model.getActiveSession(); if (session == null) { return
(no active session)
; } let activeScreen = session.getActiveScreen(); return (
); } } 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; } @mobxReact.observer class RemoteStatusLight extends React.Component<{remote : RemoteType}, {}> { render() { let remote = this.props.remote; let status = "error"; let wfp = false; if (remote != null) { status = remote.status; wfp = remote.waitingforpassword; } let icon = "fa-circle" if (status == "connecting") { icon = (wfp ? "fa-key" : "fa-refresh"); } return ( ); } } @mobxReact.observer class MainSideBar extends React.Component<{}, {}> { collapsed : mobx.IObservableValue = mobx.observable.box(false); @boundMethod toggleCollapsed() { mobx.action(() => { this.collapsed.set(!this.collapsed.get()); })(); } handleSessionClick(sessionId : string) { GlobalCommandRunner.switchSession(sessionId); } handleNewSession() { GlobalCommandRunner.createNewSession(); } clickRemotes() { GlobalCommandRunner.showAllRemotes(); } remoteDisplayName(remote : RemoteType) : any { if (!isBlank(remote.remotealias)) { return ( <> {remote.remotealias} {remote.remotecanonicalname} ); } return ({remote.remotecanonicalname}); } clickRemote(remote : RemoteType) { GlobalCommandRunner.showRemote(remote.remoteid); } @boundMethod handleAddRemote() : void { GlobalCommandRunner.openCreateRemote(); } render() { let model = GlobalModel; let activeSessionId = model.activeSessionId.get(); let activeWindow = model.getActiveWindow(); let activeRemoteId : string = null; if (activeWindow != null) { let rptr = activeWindow.curRemote.get(); if (rptr != null && !isBlank(rptr.remoteid)) { activeRemoteId = rptr.remoteid; } } let session : Session = null; let remotes = model.remotes ?? []; let remote : RemoteType = null; let idx : number = 0; remotes = sortAndFilterRemotes(remotes); return ( ); } } 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; } @mobxReact.observer class AddRemoteModal extends React.Component<{}, {}> { @boundMethod handleModalClose() : void { mobx.action(() => { GlobalModel.addRemoteModalOpen.set(false); })(); } render() { return (

Add Remote

hello
); } } @mobxReact.observer class RemoteModal extends React.Component<{}, {}> { @boundMethod handleModalClose() : void { mobx.action(() => { GlobalModel.remotesModalOpen.set(false); })(); } @boundMethod handleAddRemote() : void { mobx.action(() => { GlobalModel.addRemoteModalOpen.set(true); })(); } render() { let model = GlobalModel; let remotes = sortAndFilterRemotes(model.remotes); let remote : RemoteType = null; return (

Remotes

Status Alias User@Host Connect
{remote.remotealias} - {remote.remotecanonicalname} {remote.connectmode}
); } } @mobxReact.observer class Main extends React.Component<{}, {}> { constructor(props : any) { super(props); } render() { return (

ScriptHaus

); } } export {Main};