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 {If, For, When, Otherwise, Choose} from "tsx-control-statements/components";
import cn from "classnames";
import {debounce, throttle} from "throttle-debounce";
import {v4 as uuidv4} from "uuid";
import dayjs from "dayjs";
import type {SessionDataType, LineType, CmdDataType, RemoteType, RemoteStateType, RemoteInstanceType, RemotePtrType, HistoryItem, HistoryQueryOpts, RemoteEditType, FeStateType, ContextMenuOpts, BookmarkType, RenderModeType, ClientMigrationInfo, LineFactoryProps} from "./types";
import type * as T from "./types";
import localizedFormat from 'dayjs/plugin/localizedFormat';
import {GlobalModel, GlobalCommandRunner, Session, Cmd, ScreenLines, Screen, riToRPtr, TabColors, RemoteColors} from "./model";
import {windowWidthToCols, windowHeightToRows, termHeightFromRows, termWidthFromCols} from "./textmeasure";
import {isModKeyPress, boundInt} from "./util";
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import {BookmarksView} from "./bookmarks";
import {HistoryView} from "./history";
import {Line, Prompt} from "./linecomps";
import {ScreenSettingsModal, SessionSettingsModal, LineSettingsModal, ClientSettingsModal} from "./settings";
import {renderCmdText} from "./elements";
import {LinesView} from "./linesview";

dayjs.extend(localizedFormat)

const RemotePtyRows = 8;
const RemotePtyCols = 80;
const PasswordUnchangedSentinel = "--unchanged--";
const LinesVisiblePadding = 500;
const TDots = "⋮";

type OV<V> = mobx.IObservableValue<V>;
type OArr<V> = mobx.IObservableArray<V>;
type OMap<K,V> = mobx.ObservableMap<K,V>;

type VisType = "visible" | "";

type InterObsValue = {
    sessionid : string,
    lineid : string,
    cmdid : string,
    visible : mobx.IObservableValue<boolean>,
    timeoutid? : any,
};

function isBlank(s : string) : boolean {
    return (s == null || s == "");
}

function makeExternLink(url : string) : string {
    return "https://extern?" + encodeURIComponent(url);
}

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 truncateWithTDots(str : string, maxLen : number) : string {
    if (str == null) {
        return null;
    }
    if (str.length <= maxLen) {
        return str;
    }
    return str.slice(0, maxLen-1) + TDots;
}

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;
}

@mobxReact.observer
class TextAreaInput extends React.Component<{onHeightChange : () => void}, {}> {
    lastTab : boolean = false;
    lastHistoryUpDown : boolean = false;
    lastTabCurLine : mobx.IObservableValue<string> = mobx.observable.box(null);
    lastFocusType : string = null;
    mainInputRef : React.RefObject<any>;
    historyInputRef : React.RefObject<any>;
    controlRef : React.RefObject<any>;
    lastHeight : number = 0;

    constructor(props) {
        super(props);
        this.mainInputRef = React.createRef();
        this.historyInputRef = React.createRef();
        this.controlRef = React.createRef();
    }

    setFocus() : void {
        let inputModel = GlobalModel.inputModel;
        if (inputModel.historyShow.get()) {
            this.historyInputRef.current.focus();
        }
        else {
            this.mainInputRef.current.focus();
        }
    }

    checkHeight(shouldFire : boolean) : void {
        let elem = this.controlRef.current;
        if (elem == null) {
            return;
        }
        let curHeight = elem.offsetHeight;
        if (this.lastHeight == curHeight) {
            return;
        }
        this.lastHeight = curHeight;
        if (shouldFire && this.props.onHeightChange != null) {
            this.props.onHeightChange();
        }
    }

    componentDidMount() {
        let activeScreen = GlobalModel.getActiveScreen();
        if (activeScreen != null) {
            let focusType = activeScreen.focusType.get();
            if (focusType == "input") {
                this.setFocus();
            }
            this.lastFocusType = focusType;
        }
        this.checkHeight(false);
    }

    componentDidUpdate() {
        let activeScreen = GlobalModel.getActiveScreen();
        if (activeScreen != null) {
            let focusType = activeScreen.focusType.get();
            if (this.lastFocusType != focusType && focusType == "input") {
                this.setFocus();
            }
            this.lastFocusType = focusType;
        }
        let inputModel = GlobalModel.inputModel;
        if (inputModel.forceCursorPos.get() != null) {
            if (this.mainInputRef.current != null) {
                this.mainInputRef.current.selectionStart = inputModel.forceCursorPos.get();
                this.mainInputRef.current.selectionEnd = inputModel.forceCursorPos.get();
            }
            mobx.action(() => inputModel.forceCursorPos.set(null))();
        }
        this.checkHeight(true);
    }

    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 (isModKeyPress(e)) {
                return;
            }
            let model = GlobalModel;
            let inputModel = model.inputModel;
            let win = model.getScreenLinesForActiveScreen();
            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) {
                    if (GlobalModel.inputModel.isEmpty()) {
                        let activeWindow = GlobalModel.getScreenLinesForActiveScreen();
                        let activeScreen = GlobalModel.getActiveScreen();
                        if (activeScreen != null && activeWindow != null && activeWindow.lines.length > 0) {
                            activeScreen.setSelectedLine(0);
                            GlobalCommandRunner.screenSelectLine("E");
                        }
                        return;
                    }
                    else {
                        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();
                e.stopPropagation();
                let inputModel = GlobalModel.inputModel;
                inputModel.toggleInfoMsg();
                if (inputModel.inputMode.get() != null) {
                    inputModel.resetInputMode();
                }
                return;
            }
            if (e.code == "KeyC" && e.getModifierState("Control")) {
                e.preventDefault();
                inputModel.resetInput();
                return;
            }
            if (e.code == "KeyU" && e.getModifierState("Control")) {
                e.preventDefault();
                this.controlU();
                return;
            }
            if (e.code == "KeyP" && e.getModifierState("Control")) {
                e.preventDefault();
                this.controlP();
                return;
            }
            if (e.code == "KeyN" && e.getModifierState("Control")) {
                e.preventDefault();
                this.controlN();
                return;
            }
            if (e.code == "KeyW" && e.getModifierState("Control")) {
                e.preventDefault();
                this.controlW();
                return;
            }
            if (e.code == "KeyY" && e.getModifierState("Control")) {
                e.preventDefault();
                this.controlY();
                return;
            }
            if (e.code == "KeyR" && e.getModifierState("Control")) {
                e.preventDefault();
                inputModel.openHistory();
                return;
            }
            if (e.code == "ArrowUp" && e.getModifierState("Shift")) {
                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, "screen");
                    }
                    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 == "KeyG" && e.getModifierState("Control")) {
            e.preventDefault();
            inputModel.resetInput();
            return;
        }
        if (e.code == "KeyC" && e.getModifierState("Control")) {
            e.preventDefault();
            inputModel.resetInput();
            return;
        }
        if (e.code == "KeyR" && ((e.getModifierState("Meta") || e.getModifierState("Control")) && !e.getModifierState("Shift"))) {
            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 == "screen") {
                htype = "session";
            }
            else if (htype == "session") {
                htype = "global";
            }
            else {
                htype = "screen";
            }
            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;
        }
        if (e.code == "KeyP" && e.getModifierState("Control")) {
            e.preventDefault();
            inputModel.moveHistorySelection(1);
            return;
        }
        if (e.code == "KeyN" && e.getModifierState("Control")) {
            e.preventDefault();
            inputModel.moveHistorySelection(-1);
            return;
        }
    }

    @boundMethod
    controlU() {
        if (this.mainInputRef.current == null) {
            return;
        }
        let selStart = this.mainInputRef.current.selectionStart;
        let value = this.mainInputRef.current.value;
        if (selStart > value.length) {
            return;
        }
        let cutValue = value.substr(0, selStart);
        let restValue = value.substr(selStart);
        let cmdLineUpdate = {cmdline: restValue, cursorpos: 0};
        console.log("ss", selStart, value, "[" + cutValue + "]", "[" + restValue + "]");
        navigator.clipboard.writeText(cutValue);
        GlobalModel.inputModel.updateCmdLine(cmdLineUpdate);
    }

    @boundMethod
    controlP() {
        let inputModel = GlobalModel.inputModel;
        if (!inputModel.isHistoryLoaded()) {
            this.lastHistoryUpDown = true;
            inputModel.loadHistory(false, 1, "screen");
            return;
        }
        inputModel.moveHistorySelection(1);
        this.lastHistoryUpDown = true;
    }

    @boundMethod
    controlN() {
        let inputModel = GlobalModel.inputModel;
        inputModel.moveHistorySelection(-1);
        this.lastHistoryUpDown = true;
    }

    @boundMethod
    controlW() {
        if (this.mainInputRef.current == null) {
            return;
        }
        let selStart = this.mainInputRef.current.selectionStart;
        let value = this.mainInputRef.current.value;
        if (selStart > value.length) {
            return;
        }
        let cutSpot = selStart-1;
        let initial = true;
        for (;cutSpot>=0; cutSpot--) {
            let ch = value[cutSpot];
            console.log(cutSpot, "[" + ch + "]");
            if (ch == " " && initial) {
                continue;
            }
            initial = false;
            if (ch == " ") {
                cutSpot++;
                break;
            }
        }
        let cutValue = value.slice(cutSpot, selStart);
        let prevValue = value.slice(0, cutSpot);
        let restValue = value.slice(selStart);
        let cmdLineUpdate = {cmdline: prevValue + restValue, cursorpos: prevValue.length};
        console.log("ss", selStart, value, "prev[" + prevValue + "]", "cut[" + cutValue + "]", "rest[" + restValue + "]");
        console.log("  ", cmdLineUpdate);
        navigator.clipboard.writeText(cutValue);
        GlobalModel.inputModel.updateCmdLine(cmdLineUpdate);
    }

    @boundMethod
    controlY() {
        if (this.mainInputRef.current == null) {
            return;
        }
        let pastePromise = navigator.clipboard.readText();
        pastePromise.then((clipText) => {
            clipText = clipText ?? "";
            let selStart = this.mainInputRef.current.selectionStart;
            let selEnd = this.mainInputRef.current.selectionEnd;
            let value = this.mainInputRef.current.value;
            if (selStart > value.length || selEnd > value.length) {
                return;
            }
            let newValue = value.substr(0, selStart) + clipText + value.substr(selEnd);
            let cmdLineUpdate = {cmdline: newValue, cursorpos: selStart+clipText.length};
            GlobalModel.inputModel.updateCmdLine(cmdLineUpdate);
        });
    }

    @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();
            if (this.historyInputRef.current != null) {
                this.historyInputRef.current.focus();
            }
            return;
        }
        inputModel.setPhysicalInputFocused(true);
    }

    @boundMethod
    handleMainBlur(e : any) {
        if (document.activeElement == this.mainInputRef.current) {
            return;
        }
        GlobalModel.inputModel.setPhysicalInputFocused(false);
    }

    @boundMethod
    handleHistoryFocus(e : any) {
        let inputModel = GlobalModel.inputModel;
        if (!inputModel.historyShow.get()) {
            e.preventDefault();
            if (this.mainInputRef.current != null) {
                this.mainInputRef.current.focus();
            }
            return;
        }
        inputModel.setPhysicalInputFocused(true);
    }

    @boundMethod
    handleHistoryBlur(e : any) {
        if (document.activeElement == this.historyInputRef.current) {
            return;
        }
        GlobalModel.inputModel.setPhysicalInputFocused(false);
    }

    render() {
        let model = GlobalModel;
        let inputModel = model.inputModel;
        let curLine = inputModel.getCurLine();
        let fcp = inputModel.forceCursorPos.get(); // for reaction
        let numLines = curLine.split("\n").length;
        let displayLines = numLines;
        if (displayLines > 5) {
            displayLines = 5;
        }
        let disabled = inputModel.historyShow.get();
        if (disabled) {
            displayLines = 1;
        }
        let activeScreen = GlobalModel.getActiveScreen();
        if (activeScreen != null) {
            activeScreen.focusType.get(); // for reaction
        }
        return (
            <div className="control cmd-input-control is-expanded" ref={this.controlRef}>
                <textarea ref={this.mainInputRef} spellCheck="false" autoComplete="off" autoCorrect="off" id="main-cmd-input" onFocus={this.handleMainFocus} onBlur={this.handleMainBlur} rows={displayLines} value={curLine} onKeyDown={this.onKeyDown} onChange={this.onChange} className={cn("textarea", {"display-disabled": disabled})}></textarea>
                <input ref={this.historyInputRef} spellCheck="false" autoComplete="off" autoCorrect="off" className="history-input" type="text" onFocus={this.handleHistoryFocus} onKeyDown={this.onHistoryKeyDown} onChange={this.handleHistoryInput} value={inputModel.historyQueryOpts.get().queryStr}/>
            </div>
        );
    }
}

@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 (
            <div className="info-remote-showall">
                <div className="info-title">
                    show all remotes
                </div>
                <table className="remotes-table">
                    <thead>
                        <tr>
                            <th>status</th>
                            <th>id</th>
                            <th>alias</th>
                            <th>user@host</th>
                            <th>connectmode</th>
                        </tr>
                    </thead>
                    <tbody>
                        <For each="remote" of={remotes}>
                            <tr key={remote.remoteid} onClick={() => this.clickRow(remote.remoteid)}>
                                <td className="status-cell">
                                    <div><RemoteStatusLight remote={remote}/>{remote.status}</div>
                                </td>
                                <td>
                                    {remote.remoteid.substr(0, 8)}
                                </td>
                                <td>
                                    {isBlank(remote.remotealias) ? "-" : remote.remotealias}
                                </td>
                                <td>
                                    {remote.remotecanonicalname}
                                </td>
                                <td>
                                    {remote.connectmode}
                                </td>
                            </tr>
                        </For>
                    </tbody>
                </table>
            </div>
        );
    }
}

@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 <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>
        }
    }

    renderEditButton(remote : RemoteType) : any {
        return <div onClick={() => this.editRemote(remote.remoteid)} className="text-button">[edit remote]</div>
    }

    renderInstallButton(remote : RemoteType) : any {
        if (remote.status == "connected" || remote.status == "connecting") {
            return null;
        }
        if (remote.installstatus == "disconnected" || remote.installstatus == "error") {
            return <div key="run-install" onClick={() => this.installRemote(remote.remoteid)} className="text-button connect-button">[run install]</div>
        }
        if (remote.installstatus == "connecting") {
            return <div key="cancel-install" onClick={() => this.cancelInstall(remote.remoteid)} className="text-button disconnect-button">[cancel install]</div>
        }
        return null;
    }

    renderInstallStatus(remote : RemoteType) : any {
        let statusStr : string = null;
        if (remote.installstatus == "disconnected") {
            if (remote.needsmshellupgrade) {
                statusStr = "mshell " + remote.mshellversion + " (needs upgrade)";
            }
            else if (isBlank(remote.mshellversion)) {
                statusStr = "mshell unknown";
            }
            else {
                statusStr = "mshell " + remote.mshellversion + " (current)";
            }
        }
        else {
            statusStr = remote.installstatus;
        }
        if (statusStr == null) {
            return null;
        }
        let installButton = this.renderInstallButton(remote);
        return (
            <div key="install-status" className="remote-field">
                <div className="remote-field-def"> install-status</div>
                <div className="remote-field-val">
                    {statusStr}<If condition={installButton != null}> | {this.renderInstallButton(remote)}</If>
                </div>
            </div>
        );
    }

    @boundMethod
    clickTermBlock(e : any) {
        let inputModel = GlobalModel.inputModel;
        if (inputModel.remoteTermWrap != null) {
            inputModel.remoteTermWrap.giveFocus();
        }
    }

    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.remoteTermWrapFocus.get());
        let remote : RemoteType;
        if (ptyRemoteId != null) {
            remote = GlobalModel.getRemote(ptyRemoteId);
        }
        if (ptyRemoteId == null || remote == null) {
            return (
                <>
                    <div key="term" className="terminal-wrapper" style={{display: "none"}}>
                        <div key="terminal" className="terminal-connectelem" id="term-remote"></div>
                    </div>
                </>
            );
        }
        let termFontSize = GlobalModel.termFontSize.get();
        return (
            <>
                <div key="info" className="info-remote">
                    <div key="title" className="info-title">
                        show remote [{remote.remotecanonicalname}]
                    </div>
                    <div key="remoteid" className="remote-field">
                        <div className="remote-field-def"> remoteid</div>
                        <div className="remote-field-val">{remote.remoteid} | {this.renderEditButton(remote)}</div>
                    </div>
                    <div key="type" className="remote-field">
                        <div className="remote-field-def"> type</div>
                        <div className="remote-field-val">{this.getRemoteTypeStr(remote)}</div>
                    </div>
                    
                    <div key="cname" className="remote-field">
                        <div className="remote-field-def"> canonicalname</div>
                        <div className="remote-field-val">{this.getCanonicalNameDisplayWithPort(remote)}</div>
                    </div>
                    <div key="alias" className="remote-field">
                        <div className="remote-field-def"> alias</div>
                        <div className="remote-field-val">{isBlank(remote.remotealias) ? "-" : remote.remotealias}</div>
                    </div>
                    <div key="cm" className="remote-field">
                        <div className="remote-field-def"> connectmode</div>
                        <div className="remote-field-val">{remote.connectmode}</div>
                    </div>
                    <div key="status" className="remote-field">
                        <div className="remote-field-def"> status</div>
                        <div className="remote-field-val"><RemoteStatusLight remote={remote}/>{remote.status} | {this.renderConnectButton(remote)}</div>
                    </div>
                    <If condition={!isBlank(remote.errorstr)}>
                        <div key="error" className="remote-field">
                            <div className="remote-field-def"> error</div>
                            <div className="remote-field-val">{remote.errorstr}</div>
                        </div>
                    </If>
                    {this.renderInstallStatus(remote)}
                    <If condition={!isBlank(remote.installerrorstr)}>
                        <div key="ierror" className="remote-field">
                            <div className="remote-field-def"> install error</div>
                            <div className="remote-field-val">{remote.installerrorstr}</div>
                        </div>
                    </If>
                </div>
                <div key="term" className={cn("terminal-wrapper", {"focus": isTermFocused}, (remote != null ? "status-" + remote.status : null))} style={{display: (ptyRemoteId == null ? "none" : "block"), width: termWidthFromCols(RemotePtyCols, termFontSize)}}>
                    <If condition={!isTermFocused}>
                        <div key="termblock" className="term-block" onClick={this.clickTermBlock}></div>
                    </If>
                    <If condition={inputModel.showNoInputMsg.get()}>
                        <div key="termtag" className="term-tag">input is only allowed while status is 'connecting'</div>
                    </If>
                    <div key="terminal" className="terminal-connectelem" id="term-remote" data-remoteid={ptyRemoteId} style={{height: termHeightFromRows(RemotePtyRows, termFontSize)}}></div>
                </div>
            </>
        );
    }
}

@mobxReact.observer
class InfoRemoteEdit extends React.Component<{}, {}> {
    alias : mobx.IObservableValue<string>;
    hostName : mobx.IObservableValue<string>;
    keyStr : mobx.IObservableValue<string>;
    portStr : mobx.IObservableValue<string>;
    passwordStr : mobx.IObservableValue<string>;
    colorStr : mobx.IObservableValue<string>;
    connectMode : mobx.IObservableValue<string>;
    sudoBool : mobx.IObservableValue<boolean>;
    autoInstallBool : mobx.IObservableValue<boolean>;
    authMode : mobx.IObservableValue<string>;
    archiveConfirm : mobx.IObservableValue<boolean> = 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<string, string> = {};
        let authMode = this.authMode.get();
        if (!isEditing) {
            if (this.sudoBool.get()) {
                kwargs["sudo"] = "1";
            }
            if (this.portStr.get() != "" && this.portStr.get() != "22") {
                kwargs["port"] = this.portStr.get();
            }
        }
        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";
        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 (
                <div className="info-title">cannot edit, remote {redit.remoteid} not found</div>
            );
        }
        let colorStr : string = null;
        return (
            <form className="info-remote">
                <div key="title" className="info-title">
                    <If condition={!isEditMode}>
                        add new remote <If condition={this.hostName.get() != ""}>'{this.remoteCName()}'</If>
                    </If>
                    <If condition={isEditMode}>
                        edit remote '{this.remoteCName()}'
                    </If>
                </div>
                <div key="type" className="remote-input-field">
                    <div className="remote-field-label">type</div>
                    <div className="remote-field-control text-control">
                        ssh
                    </div>
                </div>
                <If condition={!isEditMode}>
                    <div key="hostname" className="remote-input-field">
                        <div className="remote-field-label">user@host</div>
                        <div className="remote-field-control text-input">
                            <input type="text" autoFocus={!isEditMode ? true : null} onChange={this.onChangeHostName} value={this.hostName.get()}/>
                        </div>
                    </div>
                    <div key="port" className="remote-input-field">
                        <div className="remote-field-label">port</div>
                        <div className="remote-field-control text-input">
                            <input type="number" placeholder="22" onChange={this.onChangePortStr} value={this.portStr.get()}/>
                        </div>
                    </div>
                </If>
                <If condition={isEditMode}>
                    <div key="hostname" className="remote-input-field">
                        <div className="remote-field-label">user@host</div>
                        <div className="remote-field-control text-control">
                            {remote.remotecanonicalname}
                            <If condition={remote.remotevars.port != "22"}>
            &nbsp;(port {remote.remotevars.port})
                            </If>
                        </div>
                    </div>
                </If>
                <div key="alias" className="remote-input-field">
                    <div className="remote-field-label">alias</div>
                    <div className="remote-field-control text-input">
                        <input type="text" autoFocus={isEditMode ? true : null} onChange={this.onChangeAlias} value={this.alias.get()}/>
                    </div>
                </div>
                <div key="auth" className="remote-input-field">
                    <div className="remote-field-label">authmode</div>
                    <div className="remote-field-control select-input">
                        <select onChange={this.onChangeAuthMode} value={this.authMode.get()}>
                            <option value="none">none</option>
                            <option value="key">keyfile</option>
                            <option value="pw">password</option>
                            <option value="key+pw">keyfile and password</option>
                        </select>
                    </div>
                </div>
                <If condition={this.authMode.get() == "key" || this.authMode.get() == "key+pw"}>
                    <div key="keyfile" className="remote-input-field">
                        <div className="remote-field-label">ssh keyfile</div>
                        <div className="remote-field-control text-input">
                            <input type="text" onChange={this.onChangeKeyStr} value={this.keyStr.get()}/>
                        </div>
                    </div>
                </If>
                <If condition={this.authMode.get() == "pw" || this.authMode.get() == "key+pw"}>
                    <div key="pw" className="remote-input-field">
                        <div className="remote-field-label">ssh password</div>
                        <div className="remote-field-control text-input">
                            <input type="password" onFocus={this.onFocusPasswordStr} onChange={this.onChangePasswordStr} value={this.passwordStr.get()}/>
                            <If condition={this.canResetPw()}>
                                <i onClick={this.resetPw} title="restore to original password" className="icon fa-sharp fa-solid fa-rotate-left undo-icon"/>
                            </If>
                        </div>
                    </div>
                </If>
                <div key="sudo" className="remote-input-field" style={{display: "none"}}>
                    <div className="remote-field-label">sudo</div>
                    <div className="remote-field-control checkbox-input">
                        <input type="checkbox" onChange={this.onChangeSudo} checked={this.sudoBool.get()}/>
                    </div>
                </div>
                <div key="cm" className="remote-input-field">
                    <div className="remote-field-label">connectmode</div>
                    <div className="remote-field-control select-input">
                        <select onChange={this.onChangeConnectMode} value={this.connectMode.get()}>
                            <option>startup</option>
                            <option>auto</option>
                            <option>manual</option>
                        </select>
                    </div>
                </div>
                <div key="ai" className="remote-input-field">
                    <div className="remote-field-label">autoinstall</div>
                    <div className="remote-field-control checkbox-input">
                        <input type="checkbox" onChange={this.onChangeAutoInstall} checked={this.autoInstallBool.get()}/>
                    </div>
                </div>
                <div key="color" className="remote-input-field" style={{display: "none"}}>
                    <div className="remote-field-label">color</div>
                    <div className="remote-field-control select-input">
                        <select onChange={this.onChangeColorStr} value={this.colorStr.get()}>
                            <option value="">(default)</option>
                            <For each="colorStr" of={RemoteColors}>
                                <option key={colorStr} value={colorStr}>{colorStr}</option>
                            </For>
                        </select>
                    </div>
                </div>
                <If condition={!isBlank(redit.errorstr)}>
                    <div key="error" className="info-error">
                        {redit.errorstr}
                    </div>
                </If>
                <If condition={!isBlank(redit.infostr)}>
                    <div key="msg" className="info-msg">
                        {redit.infostr}
                    </div>
                </If>
                <div key="controls" style={{marginTop: 15, marginBottom: 10}} className="remote-input-field">
                    <a tabIndex={0} style={{marginRight: 20}} onClick={this.doSubmitRemote} onKeyDown={this.keyDownCreateRemote} className="text-button success-button">[{isEditMode ? "update" : "create"} remote]</a>
                    {"|"}
                    <If condition={isEditMode}>
                        <a tabIndex={0} style={{marginLeft: 20, marginRight: 5}} onClick={this.doArchiveRemote} onKeyDown={this.keyDownCreateRemote} className={cn("text-button", (this.archiveConfirm.get() ? "error-button" : "disabled-button"))}>[archive remote]</a>
                        <input onChange={this.updateArchiveConfirm} checked={this.archiveConfirm.get()} style={{marginRight: 20}} type="checkbox"/>
                        {"|"}
                    </If>
                    <a tabIndex={0} style={{marginLeft: 20}} onClick={this.doCancel} onKeyDown={this.keyDownCancel} className="text-button grey-button">[cancel (ESC)]</a>
                </div>
            </form>
        );
    }
}

@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);
    }

    hasSpace(s : string) : boolean {
        return s.indexOf(" ") != -1;
    }

    handleCompClick(s : string) : void {
        // TODO -> complete to this completion
    }

    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);
            }
        }
        let activeScreen = model.getActiveScreen();
        return (
            <div className="cmd-input-info" style={{display: (infoShow ? "block" : "none")}}>
                <If condition={infoMsg && infoMsg.infotitle != null}>
                    <div key="infotitle" className="info-title">
                        {titleStr}
                    </div>
                </If>
                <If condition={infoMsg && infoMsg.infomsg != null}>
                    <div key="infomsg" className="info-msg">
                        <If condition={infoMsg.infomsghtml}>
                            <span dangerouslySetInnerHTML={{__html: infoMsg.infomsg}}/>
                        </If>
                        <If condition={!infoMsg.infomsghtml}>
                            {infoMsg.infomsg}
                        </If>
                    </div>
                </If>
                <If condition={infoMsg && infoMsg.websharelink && activeScreen != null}>
                    <div key="infomsg" className="info-msg">
                        started sharing screen at <a target="_blank" href={makeExternLink(activeScreen.getWebShareUrl())}>[link]</a>
                    </div>
                </If>
                <If condition={infoMsg && infoMsg.infolines != null}>
                    <div key="infolines" className="info-lines">
                        <For index="idx" each="line" of={infoMsg.infolines}>
                            <div key={idx}>{line == "" ? " " : line}</div>
                        </For>
                    </div>
                </If>
                <If condition={infoMsg && infoMsg.remoteedit}>
                    <InfoRemoteEdit key={"inforemoteedit"} />
                </If>
                <InfoRemoteShow key="inforemoteshow"/>
                <InfoRemoteShowAll key="inforemoteshowall"/>
                <If condition={infoMsg && infoMsg.infocomps != null && infoMsg.infocomps.length > 0}>
                    <div key="infocomps" className="info-comps">
                        <For each="istr" index="idx" of={infoMsg.infocomps}>
                            <div onClick={() => this.handleCompClick(istr)} key={idx} className={cn("info-comp", {"has-space": this.hasSpace(istr)}, {"metacmd-comp": istr.startsWith("^")})}>
                                {this.getAfterSlash(istr)}
                            </div>
                        </For>
                        <If condition={infoMsg.infocompsmore}>
                            <div key="more" className="info-comp no-select">
                                ...
                            </div>
                        </If>
                    </div>
                </If>
                <If condition={infoMsg && infoMsg.infoerror != null}>
                    <div key="infoerror" className="info-error">
                        [error] {infoMsg.infoerror}
                    </div>
                </If>
            </div>
        );
    }
}

@mobxReact.observer
class HistoryInfo extends React.Component<{}, {}> {
    lastClickHNum : string = null;
    lastClickTs : number = 0;
    containingText : mobx.IObservableValue<string> = 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 ", "[" + truncateWithTDots(rname, 13) + "]")
        return rtn;
    }

    renderHInfoText(hitem : HistoryItem, opts : HistoryQueryOpts, isSelected : boolean, snames : Record<string, string>, scrNames : Record<string, string>) : string {
        let remoteStr = "";
        if (!opts.limitRemote) {
            remoteStr = this.renderRemote(hitem);
        }
        let selectedStr = (isSelected ? "*" : " ");
        let lineNumStr = (hitem.linenum > 0 ? "(" + hitem.linenum + ")" : "");
        if (isBlank(opts.queryType) || opts.queryType == "screen") {
            return selectedStr + sprintf("%7s", lineNumStr) + " " + remoteStr;
        }
        if (opts.queryType == "session") {
            let screenStr = "";
            if (!isBlank(hitem.screenid)) {
                let scrName = scrNames[hitem.screenid];
                if (scrName != null) {
                    screenStr = "[" + truncateWithTDots(scrName, 15) + "]";
                }
            }
            return selectedStr + sprintf("%17s", screenStr) + sprintf("%7s", lineNumStr) + " " + remoteStr;
        }
        if (opts.queryType == "global") {
            let sessionStr = "";
            if (!isBlank(hitem.sessionid)) {
                let sessionName = snames[hitem.sessionid];
                if (sessionName != null) {
                    sessionStr = "#" + truncateWithTDots(sessionName, 15);
                }
            }
            let screenStr = "";
            if (!isBlank(hitem.screenid)) {
                let scrName = scrNames[hitem.screenid];
                if (scrName != null) {
                    screenStr = "[" + truncateWithTDots(scrName, 13) + "]";
                }
            }
            let ssStr = sessionStr + screenStr;
            return selectedStr + sprintf("%15s ", sessionStr) + " " + sprintf("%15s", screenStr) + sprintf("%7s", lineNumStr) + " " + remoteStr;
        }
        return "-";
    }

    renderHItem(hitem : HistoryItem, opts : HistoryQueryOpts, isSelected : boolean, snames : Record<string, string>, scrNames : Record<string, string>) : any {
        let lines = hitem.cmdstr.split("\n");
        let line : string = "";
        let idx = 0;
        let infoText = this.renderHInfoText(hitem, opts, isSelected, snames, scrNames);
        let infoTextSpacer = sprintf("%" + infoText.length + "s", "");
        return (
            <div key={hitem.historynum} className={cn("history-item", {"is-selected": isSelected}, {"history-haderror": hitem.haderror}, "hnum-" + hitem.historynum)} onClick={() => this.handleItemClick(hitem)}>
                <div className="history-line">{infoText} {lines[0]}</div>
                <For each="line" index="idx" of={lines.slice(1)}>
                    <div key={idx} className="history-line">{infoTextSpacer} {line}</div>
                </For>
            </div>
        );
    }

    @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();
        let snames : Record<string, string> = {};
        let scrNames : Record<string, string> = {};
        if (opts.queryType == "global") {
            scrNames = GlobalModel.getScreenNames();
            snames = GlobalModel.getSessionNames();
        }
        else if (opts.queryType == "session") {
            scrNames = GlobalModel.getScreenNames();
        }
        return (
            <div className="cmd-history">
                <div className="history-title">
                    <div>history</div>
                    <div className="spacer"></div>
                    <div className="history-opt">[for {opts.queryType} &#x2318;S]</div>
                    <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="grow-spacer"></div>
                    <div className="history-clickable-opt" onClick={this.handleClose}>(ESC)</div>
                    <div className="spacer"></div>
                </div>
                <div className={cn("history-items", {"show-remotes": !opts.limitRemote}, {"show-sessions": opts.queryType == "global"})}>
                    <If condition={hitems.length == 0}>
                        [no history]
                    </If>
                    <If condition={hitems.length > 0}>
                        <For each="hitem" index="idx" of={hitems}>
                            {this.renderHItem(hitem, opts, (hitem == selItem), snames, scrNames)}
                        </For>
                    </If>
                </div>
            </div>
        );
    }
}

@mobxReact.observer
class CmdInput extends React.Component<{}, {}> {
    cmdInputRef : React.RefObject<any> = React.createRef();
    
    @boundMethod
    onInfoToggle() : void {
        GlobalModel.inputModel.toggleInfoMsg();
        return;
    }

    componentDidMount() {
        this.updateCmdInputHeight();
    }

    updateCmdInputHeight() {
        let elem = this.cmdInputRef.current;
        if (elem == null) {
            return;
        }
        let height = elem.offsetHeight;
        if (height == GlobalModel.inputModel.cmdInputHeight) {
            return;
        }
        mobx.action(() => {
            GlobalModel.inputModel.cmdInputHeight.set(height);
        })();
    }

    componentDidUpdate(prevProps, prevState, snapshot : {}) : void {
        this.updateCmdInputHeight();
    }

    @boundMethod
    handleInnerHeightUpdate() : void {
        this.updateCmdInputHeight();
    }

    @boundMethod
    clickFocusInputHint() : void {
        GlobalModel.inputModel.giveFocus();
    }

    @boundMethod
    clickHistoryHint(e : any) : void {
        e.preventDefault();
        e.stopPropagation();
        
        let inputModel = GlobalModel.inputModel;
        if (inputModel.historyShow.get()) {
            inputModel.resetHistory();
        }
        else {
            inputModel.openHistory();
        }
    }

    @boundMethod
    clickConnectRemote(remoteId : string) : void {
        GlobalCommandRunner.connectRemote(remoteId);
    }
    
    render() {
        let model = GlobalModel;
        let inputModel = model.inputModel;
        let screen = GlobalModel.getActiveScreen();
        let ri : RemoteInstanceType = null;
        let rptr : RemotePtrType = null;
        if (screen != null) {
            ri = screen.getCurRemoteInstance();
            rptr = screen.curRemote.get();
        }
        let remote : RemoteType = null;
        let remoteState : FeStateType = null;
        if (ri != null) {
            remote = GlobalModel.getRemote(ri.remoteid);
            remoteState = ri.festate;
        }
        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();
        let inputMode : string = inputModel.inputMode.get();
        return (
            <div ref={this.cmdInputRef} className={cn("cmd-input has-background-black", {"has-info": infoShow}, {"has-history": historyShow}, {"has-remote": remoteShow})}>
                <div key="focus" className={cn("focus-indicator", {"active": focusVal})}/>
                <div key="minmax" onClick={this.onInfoToggle} className="input-minmax-control">
                    <If condition={infoShow || historyShow}>
                        <i className="fa-sharp fa-solid fa-chevron-down"/>
                    </If>
                    <If condition={!(infoShow || historyShow) && hasInfo}>
                        <i className="fa-sharp fa-solid fa-chevron-up"/>
                    </If>
                </div>
                <If condition={historyShow}>
                    <div className="cmd-input-grow-spacer"></div>
                    <HistoryInfo/>
                </If>
                <InfoMsg key="infomsg"/>
                <If condition={remote && remote.status != "connected"}>
                    <div className="remote-status-warning">
                        WARNING:&nbsp;<span className="remote-name">[{GlobalModel.resolveRemoteIdToFullRef(remote.remoteid)}]</span>&nbsp;is {remote.status}
                        <If condition={remote.status != "connecting"}><div className="button is-prompt-green is-outlined is-small" onClick={() => this.clickConnectRemote(remote.remoteid)}>connect now</div></If>
                    </div>
                </If>
                <div key="prompt" className="cmd-input-context">
                    <div className="has-text-white">
                        <Prompt rptr={rptr} festate={remoteState}/>
                    </div>
                </div>
                <div key="input" className={cn("cmd-input-field field has-addons", (inputMode != null ? "inputmode-" + inputMode : null))}>
                    <If condition={inputMode != null}>
                        <div className="control cmd-quick-context">
                            <div className="button is-static">{inputMode}</div>
                        </div>
                    </If>
                    <TextAreaInput onHeightChange={this.handleInnerHeightUpdate}/>
                    <div className="control cmd-exec">
                        <div onClick={GlobalModel.inputModel.uiSubmitCommand} className="button" title="Run Command">
                            <span className="icon">
                                <i className="fa-sharp fa-solid fa-rocket"/>
                            </span>
                        </div>
                    </div>
                    <div className="cmd-hints">
                        <If condition={!focusVal}><div onClick={this.clickFocusInputHint} className="hint-item color-white">focus input ({renderCmdText("I")})</div></If>
                        <If condition={focusVal}><div onMouseDown={this.clickHistoryHint} className="hint-item color-green"><i className={cn("fa-sharp fa-solid", (historyShow ? "fa-angle-down" : "fa-angle-up"))}/> {historyShow ? "close history (esc)" : "show history (ctrl-r)"}</div></If>
                    </div>
                </div>
            </div>
        );
    }
}

// screen is not null
@mobxReact.observer
class ScreenWindowView extends React.Component<{screen : Screen}, {}> {
    rszObs : any;
    windowViewRef : React.RefObject<any>;

    width : mobx.IObservableValue<number> = mobx.observable.box(0, {name: "sw-view-width"});
    height : mobx.IObservableValue<number> = mobx.observable.box(0, {name: "sw-view-height"});
    setSize_debounced : (width : number, height : number) => void;

    renderMode : OV<RenderModeType> = mobx.observable.box("normal", {name: "renderMode"});
    shareCopied : OV<boolean> = mobx.observable.box(false, {name: "sw-shareCopied"});

    constructor(props : any) {
        super(props);
        this.setSize_debounced = debounce(1000, this.setSize.bind(this));
        this.windowViewRef = React.createRef();
    }

    setSize(width : number, height : number) : void {
        let {screen} = this.props;
        if (screen == null) {
            return;
        }
        if (width == null || height == null || width == 0 || height == 0) {
            return;
        }
        mobx.action(() => {
            this.width.set(width);
            this.height.set(height);
            screen.screenSizeCallback({height: height, width: width});
        })();
    }

    componentDidMount() {
        let wvElem = this.windowViewRef.current;
        if (wvElem != null) {
            let width = wvElem.offsetWidth;
            let height = wvElem.offsetHeight;
            mobx.action(() => {
                this.setSize(width, height);
            })();
            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;
        let height = entry.target.offsetHeight;
        mobx.action(() => {
            this.setSize_debounced(width, height);
        })();
    }

    getScreenLines() : ScreenLines {
        let {screen} = this.props;
        let win = GlobalModel.getScreenLinesById(screen.screenId);
        if (win == null) {
            win = GlobalModel.loadScreenLines(screen.screenId);
        }
        return win;
    }

    getWindowViewStyle() : any {
        return {position: "absolute", width: "100%", height: "100%", overflowX: "hidden"};
    }

    @boundMethod
    toggleRenderMode() {
        let renderMode = this.renderMode.get();
        mobx.action(() => {
            this.renderMode.set(renderMode == "normal" ? "collapsed" : "normal");
        })();
    }

    renderError(message : string, fade : boolean) {
        let {screen} = this.props;
        return (
            <div className="window-view" style={this.getWindowViewStyle()} ref={this.windowViewRef} data-screenid={screen.screenId}>
                <div key="lines" className="lines"></div>
                <div key="window-empty" className={cn("window-empty", {"should-fade": fade})}>
                    <div>{message}</div>
                </div>
            </div>
        );
    }

    @boundMethod
    copyShareLink() : void {
        let {screen} = this.props;
        let shareLink = screen.getWebShareUrl();
        if (shareLink == null) {
            return;
        }
        navigator.clipboard.writeText(shareLink);
        mobx.action(() => {
            this.shareCopied.set(true);
        })();
        setTimeout(() => {
            mobx.action(() => {
                this.shareCopied.set(false);
            })();
        }, 600)
    }

    @boundMethod
    buildLineComponent(lineProps : LineFactoryProps) : JSX.Element {
        let {screen} = this.props;
        let {line, ...restProps} = lineProps;
        let realLine : LineType = (line as LineType);
        return (
            <Line key={realLine.lineid} screen={screen} line={realLine} {...restProps}/>
        );
    }

    render() {
        let {screen} = this.props;
        let win = this.getScreenLines();
        if (win == null || !win.loaded.get()) {
            return this.renderError("...", true);
        }
        if (win.loadError.get() != null) {
            return this.renderError(sprintf("(%s)", win.loadError.get()), false);
        }
        if (this.width.get() == 0) {
            return this.renderError("", false);
        }
        let cdata = GlobalModel.clientData.get();
        if (cdata == null) {
            return this.renderError("loading client data", true);
        }
        let idx = 0;
        let line : LineType = null;
        let session = GlobalModel.getSessionById(screen.sessionId);
        let isActive = screen.isActive();
        let selectedLine = screen.getSelectedLine();
        let lines = win.getNonArchivedLines();
        let renderMode = this.renderMode.get();
        return (
            <div className="window-view" style={this.getWindowViewStyle()} ref={this.windowViewRef}>
                <div key="rendermode-tag" className={cn("rendermode-tag", {"is-active": isActive})}>
                    <div className="render-mode" onClick={this.toggleRenderMode}>
                        <If condition={renderMode == "normal"}>
                            <i title="collapse" className="fa-sharp fa-solid fa-arrows-to-line"/>
                        </If>
                        <If condition={renderMode == "collapsed"}>
                            <i title="expand" className="fa-sharp fa-solid fa-arrows-from-line"/>
                        </If>
                    </div>
                </div>
                <If condition={screen.isWebShare()}>
                    <div key="share-tag" className="share-tag">
                        <If condition={this.shareCopied.get()}>
                            <div className="copied-indicator"/>
                        </If>
                        <div><i title="archived" className="fa-sharp fa-solid fa-share-nodes"/> web shared</div>
                        <div className="share-tag-link">
                            <div className="button is-prompt-green is-outlined is-small" onClick={this.copyShareLink}>
                                <span>copy link</span>
                                <span className="icon">
                                    <i className="fa-sharp fa-solid fa-copy"/>
                                </span>
                            </div>
                        </div>
                    </div>
                </If>
                <If condition={lines.length > 0}>
                    <LinesView screen={screen} width={this.width.get()} lines={lines} renderMode={renderMode} lineFactory={this.buildLineComponent}/>
                </If>
                <If condition={lines.length == 0}>
                    <div key="window-empty" className="window-empty">
                        <div><code>[session="{session.name.get()}" screen="{screen.name.get()}"]</code></div>
                    </div>
                </If>
            </div>
        );
    }
}

@mobxReact.observer
class ScreenView extends React.Component<{screen : Screen}, {}> {
    render() {
        let {screen} = this.props;
        if (screen == null) {
            return (
                <div className="screen-view">
                    (no screen found)
                </div>
            );
        }
        let fontSize = GlobalModel.termFontSize.get();
        return (
            <div className="screen-view" data-screenid={screen.screenId}>
                <ScreenWindowView key={screen.screenId + ":" + fontSize} screen={screen}/>
            </div>
        );
    }
}

@mobxReact.observer
class ScreenTabs extends React.Component<{session : Session}, {}> {
    tabsRef : React.RefObject<any> = React.createRef();
    lastActiveScreenId : string = null;
    scrolling : OV<boolean> = mobx.observable.box(false, {name: "screentabs-scrolling"});

    stopScrolling_debounced : () => void;

    constructor(props : any) {
        super(props);
        this.stopScrolling_debounced = debounce(1500, this.stopScrolling.bind(this));
    }
    
    @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);
    }

    componentDidMount() : void {
        this.componentDidUpdate();
    }

    componentDidUpdate() : void {
        let {session} = this.props;
        let activeScreenId = session.activeScreenId.get();
        if (activeScreenId != this.lastActiveScreenId && this.tabsRef.current) {
            let tabElem = this.tabsRef.current.querySelector(sprintf(".screen-tab[data-screenid=\"%s\"]", activeScreenId));
            if (tabElem != null) {
                tabElem.scrollIntoView();
            }
        }
        this.lastActiveScreenId = activeScreenId;
    }

    stopScrolling() : void {
        mobx.action(() => {
            this.scrolling.set(false);
        })();
    }

    @boundMethod
    handleScroll() {
        if (!this.scrolling.get()) {
            mobx.action(() => {
                this.scrolling.set(true);
            })();
        }
        this.stopScrolling_debounced();
    }

    @boundMethod
    openScreenSettings(e : any, screen : Screen) : void {
        e.preventDefault();
        e.stopPropagation();
        mobx.action(() => {
            GlobalModel.screenSettingsModal.set({sessionId: screen.sessionId, screenId: screen.screenId});
        })();
    }

    render() {
        let {session} = this.props;
        if (session == null) {
            return null;
        }
        let screen : Screen = null;
        let index = 0;
        let showingScreens = [];
        let activeScreenId = session.activeScreenId.get();
        let screens = GlobalModel.getSessionScreens(session.sessionId);
        for (let screen of screens) {
            if (!screen.archived.get() || activeScreenId == screen.screenId) {
                showingScreens.push(screen);
            }
        }
        showingScreens.sort((a, b) => {
            let aidx = a.screenIdx.get();
            let bidx = b.screenIdx.get();
            if (aidx < bidx) {
                return -1;
            }
            if (aidx > bidx) {
                return 1;
            }
            return 0;
        });
        return (
            <div className="screen-tabs-container">
                <div className={cn("screen-tabs", {"scrolling": this.scrolling.get()})} ref={this.tabsRef} onScroll={this.handleScroll}>
                    <For each="screen" index="index" of={showingScreens}>
                        <div key={screen.screenId} data-screenid={screen.screenId} className={cn("screen-tab", {"is-active": activeScreenId == screen.screenId, "is-archived": screen.archived.get()}, "color-" + screen.getTabColor())} onClick={() => this.handleSwitchScreen(screen.screenId)} onContextMenu={(event) => this.openScreenSettings(event, screen)}>
                            <If condition={screen.archived.get()}><i title="archived" className="fa-sharp fa-solid fa-box-archive"/></If>
                            <If condition={screen.isWebShare()}><i title="archived" className="fa-sharp fa-solid fa-share-nodes web-share-icon"/></If>
                            {screen.name.get()}
                            <If condition={index+1 <= 9}>
                                <div className="tab-index">{renderCmdText(String(index+1))}</div>
                                <div onClick={(e) => this.openScreenSettings(e, screen)} title="Settings" className="tab-gear"><i className="fa-sharp fa-solid fa-gear"/></div>
                            </If>
                            <If condition={index+1 > 9}>
                                <div onClick={(e) => this.openScreenSettings(e, screen)} title="Settings" className="tab-gear"><i className="fa-sharp fa-solid fa-gear"/></div>
                            </If>
                        </div>
                    </For>
                    <div key="new-screen" className="screen-tab new-screen" onClick={this.handleNewScreen}>
                        <i className="fa-sharp fa-solid fa-plus"/>
                    </div>
                    
                </div>
                <div className="cmd-hints">
                    <div className="hint-item color-green">move left {renderCmdText("[")}</div>
                    <div className="hint-item color-green">move right {renderCmdText("]")}</div>
                    <div className="hint-item color-green">new tab {renderCmdText("T")}</div>
                </div>
            </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>;
        }
        let activeScreen = session.getActiveScreen();
        let cmdInputHeight = model.inputModel.cmdInputHeight.get();
        if (cmdInputHeight == 0) {
            cmdInputHeight = 110;
        }
        let isHidden = (GlobalModel.activeMainView.get() != "session");
        return (
            <div className={cn("session-view", {"is-hidden": isHidden})} data-sessionid={session.sessionId}>
                <ScreenView screen={activeScreen}/>
                <ScreenTabs session={session}/>
                <div style={{height: cmdInputHeight}}></div>
                <CmdInput/>
            </div>
        );
    }
}

function getConnVal(r : RemoteType) : number {
    if (r.status == "connected") {
        return 1;
    }
    if (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-sharp fa-solid fa-circle"
        if (status == "connecting") {
            icon = (wfp ? "fa-sharp fa-solid fa-key" : "fa-sharp fa-solid fa-rotate");
        }
        return (
            <i className={cn("remote-status", icon, "status-" + status)}/>
        );
    }
}

@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) {
        GlobalCommandRunner.switchSession(sessionId);
    }

    handleNewSession() {
        GlobalCommandRunner.createNewSession();
    }

    handleNewSharedSession() {
        GlobalCommandRunner.openSharedSession();
    }

    clickRemotes() {
        GlobalCommandRunner.showAllRemotes();
    }

    clickLinks() {
        mobx.action(() => {
            GlobalModel.showLinks.set(!GlobalModel.showLinks.get());
        })();
    }

    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>);
    }

    clickRemote(remote : RemoteType) {
        GlobalCommandRunner.showRemote(remote.remoteid);
    }

    @boundMethod
    handleAddRemote() : void {
        GlobalCommandRunner.openCreateRemote();
    }

    @boundMethod
    handleHistoryClick() : void {
        if (GlobalModel.activeMainView.get() == "history") {
            mobx.action(() => {
                GlobalModel.activeMainView.set("session");
            })();
            return;
        }
        GlobalModel.historyViewModel.reSearch();
    }

    @boundMethod
    handlePlaybookClick() : void {
        console.log("playbook click");
        return;
    }

    @boundMethod
    handleBookmarksClick() : void {
        if (GlobalModel.activeMainView.get() == "bookmarks") {
            mobx.action(() => {
                GlobalModel.activeMainView.set("session");
            })();
            return;
        }
        GlobalCommandRunner.bookmarksView();
    }

    @boundMethod
    handleWelcomeClick() : void {
        mobx.action(() => {
            GlobalModel.welcomeModalOpen.set(true);
        })();
    }

    @boundMethod
    handleSettingsClick() : void {
        mobx.action(() => {
            GlobalModel.clientSettingsModal.set(true);
        })();
    }

    @boundMethod
    openSessionSettings(e : any, session : Session) : void {
        e.preventDefault();
        e.stopPropagation();
        mobx.action(() => {
            GlobalModel.sessionSettingsModal.set(session.sessionId);
        })();
    }

    render() {
        let model = GlobalModel;
        let activeSessionId = model.activeSessionId.get();
        let activeWindow = model.getScreenLinesForActiveScreen();
        let activeScreen = model.getActiveScreen();
        let activeRemoteId : string = null;
        if (activeScreen != null) {
            let rptr = activeScreen.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);
        let sessionList = [];
        for (let session of model.sessionList) {
            if (!session.archived.get() || session.sessionId == activeSessionId) {
                sessionList.push(session);
            }
        }
        let isCollapsed = this.collapsed.get();
        let mainView = GlobalModel.activeMainView.get();
        let activePlaybookId : string = null;
        return (
            <div className={cn("main-sidebar", {"collapsed": isCollapsed}, {"is-dev": GlobalModel.isDev})}>
                <div className="logo-header">
                    <h1 className={cn("title", "prompt-logo-small", {"collapsed": isCollapsed}, {"is-dev": GlobalModel.isDev})}>
                        {(isCollapsed ? "[p]" : "[prompt]")}
                    </h1>
                </div>
                <div className="collapse-container">
                    <div className="arrow-container" onClick={this.toggleCollapsed}>
                        <If condition={!isCollapsed}><i className="fa-sharp fa-solid fa-angle-left"/></If>
                        <If condition={isCollapsed}><i className="fa-sharp fa-solid fa-angle-right"/></If>
                    </div>
                </div>
                <div className="menu">
                    <p className="menu-label">
                        Private Sessions
                    </p>
                    <ul className="menu-list session-menu-list">
                        <If condition={!model.sessionListLoaded.get()}>
                            <li className="menu-loading-message"><a>...</a></li>
                        </If>
                        <If condition={model.sessionListLoaded.get()}>
                            <For each="session" index="idx" of={sessionList}>
                                <li key={session.sessionId}><a className={cn({"is-active": mainView == "session" && activeSessionId == session.sessionId})} onClick={() => this.handleSessionClick(session.sessionId)}>
                                    <If condition={!session.archived.get()}>
                                        <div className="session-num">{idx+1}</div>
                                    </If>
                                    <If condition={session.archived.get()}>
                                        <div className="session-num"><i title="archived" className="fa-sharp fa-solid fa-box-archive"/></div>
                                    </If>
                                    <div>
                                        {session.name.get()}
                                    </div>
                                    <div className="flex-spacer"/>
                                    <div className="session-gear" onClick={(e) => this.openSessionSettings(e, session)}>
                                        <i className="fa-sharp fa-solid fa-gear"/>
                                    </div>
                                </a></li>
                            </For>
                            <li className="new-session"><a onClick={() => this.handleNewSession()}><i className="fa-sharp fa-solid fa-plus"/> New Session</a></li>
                        </If>
                    </ul>
                    <p className="menu-label">
                        Shared Sessions
                    </p>
                    <ul className="menu-list">
                        <li className="new-session"><a onClick={() => this.handleNewSharedSession()}><i className="fa-sharp fa-solid fa-plus"/> New Session</a></li>
                    </ul>
                    <ul className="menu-list" style={{marginTop: 20}}>
                        <li className="menu-history"><a onClick={this.handleHistoryClick} className={cn({"is-active": (mainView == "history")})}><i className="fa-sharp fa-solid fa-clock"/> HISTORY <span className="hotkey">&#x2318;H</span></a></li>
                    </ul>
                    <ul className="menu-list">
                        <li className="menu-bookmarks"><a onClick={this.handleBookmarksClick} className={cn({"is-active": (mainView == "bookmarks")})}><i className="fa-sharp fa-solid fa-bookmark"/> BOOKMARKS <span className="hotkey">&#x2318;B</span></a></li>
                    </ul>
                    <p className="menu-label display-none">
                        Playbooks
                    </p>
                    <ul className="menu-list display-none">
                        <li key="default"><a onClick={this.handlePlaybookClick}><i className="fa-sharp fa-solid fa-file-lines"/> default</a></li>
                        <li key="prompt-dev"><a onClick={this.handlePlaybookClick}><i className="fa-sharp fa-solid fa-file-lines"/> prompt-dev</a></li>
                    </ul>
                    <div className="spacer"></div>
                    <If condition={GlobalModel.debugScreen.get() && activeScreen != null}>
                        <div>
                            focus={activeScreen.focusType.get()}<br/>
            sline={activeScreen.getSelectedLine()}<br/>
            termfocus={activeScreen.termLineNumFocus.get()}<br/>
                        </div>
                    </If>
                    <ul className="menu-list" style={{display: "none"}}>
                        <li className="menu-bookmarks"><a onClick={this.handleWelcomeClick} className={cn({"is-active": GlobalModel.welcomeModalOpen.get()})}><i className="fa-sharp fa-solid fa-door-open"/> WELCOME</a></li>
                    </ul>
                    <ul className="menu-list">
                        <li className="menu-bookmarks"><a onClick={this.handleSettingsClick}><i className="fa-sharp fa-solid fa-cog"/> SETTINGS</a></li>
                    </ul>
                    <p className="menu-label">
                        <a onClick={() => this.clickLinks()}>LINKS <i className={cn("fa-sharp fa-solid", (GlobalModel.showLinks.get() ? "fa-angle-down" : "fa-angle-right"))}/></a>
                    </p>
                    <ul className="menu-list" style={{display: (GlobalModel.showLinks.get() ? null : "none")}}>
                        <li>
                            <a target="_blank" href="https://docs.getprompt.dev/releasenotes"><i style={{width: 20}} className="fa-sharp fa-solid fa-notes"/> release notes</a>
                        </li>
                        <li>
                            <a target="_blank" href="https://docs.getprompt.dev/"><i style={{width: 20}} className="fa-sharp fa-solid fa-book"/> documentation</a>
                        </li>
                        <li>
                            <a target="_blank" href="https://discord.gg/XfvZ334gwU"><i style={{width: 20}} className="fa-brands fa-discord"/> discord</a>
                        </li>
                    </ul>
                    <p className="menu-label">
                        <a onClick={() => this.clickRemotes()}>Connections</a>
                    </p>
                    <ul className="menu-list remotes-menu-list">
                        <For each="remote" of={remotes}>
                            <li key={remote.remoteid} className={cn("remote-menu-item")}><a className={cn({"is-active": (remote.remoteid == activeRemoteId)})} onClick={() => this.clickRemote(remote)}>
                                <RemoteStatusLight remote={remote}/>
                                {this.remoteDisplayName(remote)}
                            </a></li>
                        </For>
                        <li key="add-remote" className="add-remote">
                            <a onClick={() => this.handleAddRemote()}><i className="fa-sharp fa-solid fa-plus"/> Add Connection</a>
                        </li>
                    </ul>
                    <div className="bottom-spacer"></div>
                </div>
            </div>
        );
    }
}

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 DisconnectedModal extends React.Component<{}, {}> {
    logRef : any = React.createRef();
    showLog : mobx.IObservableValue<boolean> = mobx.observable.box(false)
    
    @boundMethod
    restartServer() {
        GlobalModel.restartLocalServer();
    }

    @boundMethod
    tryReconnect() {
        GlobalModel.ws.connectNow("manual");
    }

    componentDidMount() {
        if (this.logRef.current != null) {
            this.logRef.current.scrollTop = this.logRef.current.scrollHeight;
        }
    }

    componentDidUpdate() {
        if (this.logRef.current != null) {
            this.logRef.current.scrollTop = this.logRef.current.scrollHeight;
        }
    }

    @boundMethod
    handleShowLog() : void {
        mobx.action(() => {
            this.showLog.set(!this.showLog.get());
        })();
    }
    
    render() {
        let model = GlobalModel;
        let logLine : string = null;
        let idx : number = 0;
        return (
            <div className="prompt-modal disconnected-modal modal is-active">
                <div className="modal-background"></div>
                <div className="modal-content">
                    <div className="message-header">
                        <div className="modal-title">Prompt Client Disconnected</div>
                    </div>
                    <If condition={this.showLog.get()}>
                        <div className="inner-content">
                            <div className="ws-log" ref={this.logRef}>
                                <For each="logLine" index="idx" of={GlobalModel.ws.wsLog}>
                                    <div key={idx} className="ws-logline">{logLine}</div>
                                </For>
                            </div>
                        </div>
                    </If>
                    <footer>
                        <div className="footer-text-link" style={{marginLeft: 10}} onClick={this.handleShowLog}>
                            <If condition={!this.showLog.get()}>
                                <i className="fa-sharp fa-solid fa-plus"/> Show Log
                            </If>
                            <If condition={this.showLog.get()}>
                                <i className="fa-sharp fa-solid fa-minus"/> Hide Log
                            </If>
                        </div>
                        <div className="flex-spacer"/>
                        <button onClick={this.tryReconnect} className="button">
                            <span className="icon">
                                <i className="fa-sharp fa-solid fa-rotate"/>
                            </span>
                            <span>Try Reconnect</span>
                        </button>
                        <button onClick={this.restartServer} className="button is-danger" style={{marginLeft: 10}}>
                            <span className="icon">
                                <i className="fa-sharp fa-solid fa-triangle-exclamation"/>
                            </span>
                            <span>Restart Server</span>
                        </button>
                    </footer>
                </div>
            </div>
        );
    }
}

@mobxReact.observer
class ClientStopModal extends React.Component<{}, {}> {
    @boundMethod
    refreshClient() {
        GlobalModel.refreshClient();
    }

    render() {
        let model = GlobalModel;
        let cdata = model.clientData.get();
        let mdata : ClientMigrationInfo = (cdata != null ? cdata.migration : null);
        let title = "Client Not Ready";
        if (mdata != null) {
            title = "Migrating Data";
        }
        return (
            <div className="prompt-modal client-stop-modal modal is-active">
                <div className="modal-background"></div>
                <div className="modal-content">
                    <div className="message-header">
                        <div className="modal-title">[prompt] {title}</div>
                    </div>
                    <div className="inner-content">
                        <If condition={cdata == null}>
                            <div>Cannot get client data.</div>
                        </If>
                        <If condition={cdata != null && cdata.cmdstoretype == "session"}>
                            <div>Client database is being migrated to the latest version, please wait.</div>
                            <If condition={mdata != null}>
                                <div className="progress-container">
                                    <progress className="progress is-primary" value={mdata.migrationpos} max={mdata.migrationtotal}>{mdata.migrationpos}</progress>
                                </div>
                                <div className="progress-text">{mdata.migrationpos}/{mdata.migrationtotal}</div>
                            </If>
                        </If>
                    </div>
                    <footer>
                        <button onClick={this.refreshClient} className="button">
                            <span className="icon">
                                <i className="fa-sharp fa-solid fa-rotate"/>
                            </span>
                            <span>Hard Refresh Client</span>
                        </button>
                    </footer>
                </div>
            </div>
        );
    }
}

@mobxReact.observer
class LoadingSpinner extends React.Component<{}, {}> {
    render() {
        return (
            <div className="loading-spinner"><div></div><div></div><div></div><div></div></div>
        );
    }
}

@mobxReact.observer
class AlertModal extends React.Component<{}, {}> {
    @boundMethod
    closeModal() : void {
        GlobalModel.cancelAlert();
    }

    @boundMethod
    handleOK() : void {
        GlobalModel.confirmAlert();
    }
    
    render() {
        let message = GlobalModel.alertMessage.get();
        if (message == null) {
            return null;
        }
        let title = message.title ?? "Alert";
        let isConfirm = message.confirm;
        return (
            <div className="modal prompt-modal is-active alert-modal">
                <div className="modal-background"/>
                <div className="modal-content">
                    <header>
                        <p className="modal-title"><i className="fa-sharp fa-solid fa-triangle-exclamation"/> {title}</p>
                        <div className="close-icon">
                            <i onClick={this.closeModal} className="fa-sharp fa-solid fa-times"/>
                        </div>
                    </header>
                    <div className="inner-content content">
                        <p>{message.message}</p>
                    </div>
                    <footer>
                        <If condition={isConfirm}>
                            <div onClick={this.closeModal} className="button is-prompt-cancel is-outlined is-small">Cancel</div>
                            <div onClick={this.handleOK} className="button is-prompt-green is-outlined is-small">OK</div>
                        </If>
                        <If condition={!isConfirm}>
                            <div onClick={this.handleOK} className="button is-prompt-green is-small">OK</div>
                        </If>
                    </footer>
                </div>
            </div>
        );
    }
}

@mobxReact.observer
class WelcomeModal extends React.Component<{}, {}> {
    totalPages : number = 3;
    pageNum : OV<number> = mobx.observable.box(1, {name: "welcome-pagenum"});
    
    @boundMethod
    closeModal() : void {
        mobx.action(() => {
            GlobalModel.welcomeModalOpen.set(false);
        })();
    }

    @boundMethod
    goNext() : void {
        mobx.action(() => {
            this.pageNum.set(this.pageNum.get() + 1);
        })();
    }

    @boundMethod
    goPrev() : void {
        mobx.action(() => {
            this.pageNum.set(this.pageNum.get() - 1);
        })();
    }

    renderDot(num : number) : any {
        if (num == this.pageNum.get()) {
            return <i key={String(num)} className="fa-sharp fa-solid fa-circle"/>;
        }
        return <i key={String(num)} className="fa-sharp fa-regular fa-circle"/>;
    }

    renderDots() : any {
        let elems : any = [];
        for (let i=1; i<=this.totalPages; i++) {
            let elem = this.renderDot(i);
            elems.push(elem);
        }
        return elems;
    }

    render() {
        let pageNum = this.pageNum.get();
        return (
            <div className={cn("modal welcome-modal prompt-modal is-active")}>
                <div className="modal-background"/>
                <div className="modal-content">
                    <header>
                        <div className="modal-title">welcome to [prompt]</div>
                        <div className="close-icon">
                            <i onClick={this.closeModal} className="fa-sharp fa-solid fa-times"/>
                        </div>
                    </header>
                    <div className={cn("inner-content content", {"is-hidden": pageNum != 1})}>
                        <p>
                            Prompt is a new terminal to help save you time and keep your command-line life organized.
                            Here's a couple quick tips to get your started!
                        </p>
                    </div>
                    <footer>
                        <If condition={pageNum > 1}>
                            <button className={cn("button is-dark prev-button is-small")} onClick={this.goPrev}>
                                <span className="icon is-small">
                                    <i className="fa-sharp fa-regular fa-angle-left"/>
                                </span>
                                <span>Prev</span>
                            </button>
                        </If>
                        <If condition={pageNum == 1}>
                            <div className="prev-spacer"/>
                        </If>
                        <div className="flex-spacer"/>
                        <div className="dots">
                            {this.renderDots()}
                        </div>
                        <div className="flex-spacer"/>
                        <If condition={pageNum < this.totalPages}>
                            <button className="button is-dark next-button is-small" onClick={this.goNext}>
                                <span>Next</span>
                                <span className="icon is-small">
                                    <i className="fa-sharp fa-regular fa-angle-right"/>
                                </span>
                            </button>
                        </If>
                        <If condition={pageNum == this.totalPages}>
                            <button className="button is-dark next-button is-small" onClick={this.closeModal}>
                                <span>Done</span>
                            </button>
                        </If>
                    </footer>
                </div>
            </div>
        );
    }
}

@mobxReact.observer
class Main extends React.Component<{}, {}> {
    dcWait : OV<boolean> = mobx.observable.box(false, {name: "dcWait"});
    
    constructor(props : any) {
        super(props);
    }

    @boundMethod
    handleContextMenu(e : any) {
        let isInNonTermInput = false;
        let activeElem = document.activeElement;
        if (activeElem != null && activeElem.nodeName == "TEXTAREA") {
            if (!activeElem.classList.contains("xterm-helper-textarea")) {
                isInNonTermInput = true;
            }
        }
        if (activeElem != null && activeElem.nodeName == "INPUT" && activeElem.getAttribute("type") == "text") {
            isInNonTermInput = true;
        }
        let opts : ContextMenuOpts = {};
        if (isInNonTermInput) {
            opts.showCut = true;
        }
        let sel = window.getSelection();
        if (!isBlank(sel.toString())) {
            GlobalModel.contextEditMenu(e, opts);
        }
        else {
            if (isInNonTermInput) {
                GlobalModel.contextEditMenu(e, opts);
            }
        }
    }

    @boundMethod
    updateDcWait(val : boolean) : void {
        mobx.action(() => {
            this.dcWait.set(val);
        })();
    }

    render() {
        let screenSettingsModal = GlobalModel.screenSettingsModal.get();
        let sessionSettingsModal = GlobalModel.sessionSettingsModal.get();
        let lineSettingsModal = GlobalModel.lineSettingsModal.get();
        let clientSettingsModal = GlobalModel.clientSettingsModal.get();
        let disconnected = !GlobalModel.ws.open.get() || !GlobalModel.localServerRunning.get();
        let hasClientStop = GlobalModel.getHasClientStop();
        let dcWait = this.dcWait.get();
        if (disconnected || hasClientStop) {
            if (!dcWait) {
                setTimeout(() => this.updateDcWait(true), 1500);
            }
            return (
                <div id="main" onContextMenu={this.handleContextMenu}>
                    <div className="main-content">
                        <MainSideBar/>
                        <div className="session-view"/>
                    </div>
                    <If condition={dcWait}>
                        <If condition={disconnected}>
                            <DisconnectedModal/>
                        </If>
                        <If condition={!disconnected && hasClientStop}>
                            <ClientStopModal/>
                        </If>
                    </If>
                </div>
            );
        }
        if (dcWait) {
            setTimeout(() => this.updateDcWait(false), 0);
        }
        return (
            <div id="main" onContextMenu={this.handleContextMenu}>
                <div className="main-content">
                    <MainSideBar/>
                    <SessionView/>
                    <HistoryView/>
                    <BookmarksView/>
                </div>
                <AlertModal/>
                <If condition={GlobalModel.welcomeModalOpen.get()}>
                    <WelcomeModal/>
                </If>
                <If condition={screenSettingsModal != null}>
                    <ScreenSettingsModal key={screenSettingsModal.sessionId + ":" + screenSettingsModal.screenId} sessionId={screenSettingsModal.sessionId} screenId={screenSettingsModal.screenId}/>
                </If>
                <If condition={sessionSettingsModal != null}>
                    <SessionSettingsModal key={sessionSettingsModal} sessionId={sessionSettingsModal}/>
                </If>
                <If condition={lineSettingsModal != null}>
                    <LineSettingsModal key={lineSettingsModal.lineid} line={lineSettingsModal}/>
                </If>
                <If condition={clientSettingsModal}>
                    <ClientSettingsModal/>
                </If>
            </div>
        );
    }
}

export {Main};

