From 5082330dcfcf5535498c207819be532d5f3759d1 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 10 Aug 2022 18:35:18 -0700 Subject: [PATCH] implement info panel, more control in cmd-input, completions, errors --- src/main.tsx | 226 +++++++++++++++++++++++++++++++-------------------- src/model.ts | 178 ++++++++++++++++++++++++++++++++++++---- src/sh2.less | 42 ++++++++++ src/types.ts | 3 +- src/util.ts | 36 +++++++- 5 files changed, 379 insertions(+), 106 deletions(-) diff --git a/src/main.tsx b/src/main.tsx index 2b96ecf07..fb3ea7f92 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -18,6 +18,51 @@ function getLineId(line : LineType) : string { return sprintf("%s-%s-%s", line.sessionid, line.windowid, line.lineid); } +function getRemoteStr(remote : RemoteType) : string { + if (remote == null) { + return "(no remote)"; + } + if (remote.remotevars.local) { + return sprintf("%s@%s", remote.remotevars.remoteuser, "local") + } + else if (remote.remotevars.remotehost) { + return sprintf("%s@%s", remote.remotevars.remoteuser, remote.remotevars.remotehost); + } + else { + let host = remote.remotevars.host || "unknown"; + if (remote.remotevars.user) { + return sprintf("%s@%s", remote.remotevars.user, host) + } + else { + return host; + } + } +} + +function replaceHomePath(path : string, homeDir : string) : string { + if (path == homeDir) { + return "~"; + } + if (path.startsWith(homeDir + "/")) { + return "~" + path.substr(homeDir.length); + } + return path; +} + +function getCwdStr(remote : RemoteType, state : RemoteStateType) : string { + if ((state == null || state.cwd == null) && remote != null) { + return "~"; + } + let cwd = "(unknown)"; + if (state && state.cwd) { + cwd = state.cwd; + } + if (remote && remote.remotevars.home) { + cwd = replaceHomePath(cwd, remote.remotevars.home) + } + return cwd; +} + function getLineDateStr(ts : number) : string { let lineDate = new Date(ts); let nowDate = new Date(); @@ -107,16 +152,6 @@ class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType, width } } - replaceHomePath(path : string, homeDir : string) : string { - if (path == homeDir) { - return "~"; - } - if (path.startsWith(homeDir + "/")) { - return "~" + path.substr(homeDir.length); - } - return path; - } - renderCmdText(cmd : Cmd, remote : RemoteType) : any { if (cmd == null) { return ( @@ -125,30 +160,8 @@ class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType, width ); } - let promptStr = ""; - if (remote.remotevars.local) { - promptStr = sprintf("%s@%s", remote.remotevars.remoteuser, "local") - } - else if (remote.remotevars.remotehost) { - promptStr = sprintf("%s@%s", remote.remotevars.remoteuser, remote.remotevars.remotehost) - } - else { - let host = remote.remotevars.host || "unknown"; - if (remote.remotevars.user) { - promptStr = sprintf("%s@%s", remote.remotevars.user, host) - } - else { - promptStr = host; - } - } - let cwd = "(unknown)"; - let remoteState = cmd.getRemoteState(); - if (remoteState && remoteState.cwd) { - cwd = remoteState.cwd; - } - if (remote.remotevars.home) { - cwd = this.replaceHomePath(cwd, remote.remotevars.home) - } + let promptStr = getRemoteStr(remote); + let cwd = getCwdStr(remote, cmd.getRemoteState()); return (
[{promptStr} {cwd}] {cmd.getSingleLineCmdText()} @@ -221,110 +234,143 @@ class Line extends React.Component<{sw : ScreenWindow, line : LineType, width : @mobxReact.observer class CmdInput extends React.Component<{}, {}> { - historyIndex : mobx.IObservableValue = mobx.observable.box(0, {name: "history-index"}); - modHistory : mobx.IObservableArray = mobx.observable.array([""], {name: "mod-history"}); + lastTabCurLine : mobx.IObservableValue = mobx.observable.box(null); @mobx.action @boundMethod onKeyDown(e : any) { mobx.action(() => { let model = GlobalModel; + let inputModel = model.inputModel; let win = model.getActiveWindow(); let ctrlMod = e.getModifierState("Control") || e.getModifierState("Meta") || e.getModifierState("Shift"); + let curLine = inputModel.getCurLine(); + let ltCurLine = this.lastTabCurLine.get(); + if (e.code == "Tab") { + e.preventDefault(); + let lastTab = (ltCurLine != null && curLine == ltCurLine); + if (lastTab) { + GlobalModel.submitCommand("compgen", null, [curLine], {"comppos": String(curLine.length), "compshow": "1"}); + return; + } + else { + this.lastTabCurLine.set(curLine); + GlobalModel.submitCommand("compgen", null, [curLine], {"comppos": String(curLine.length)}); + GlobalModel.clearInfoMsg(true); + return; + } + } + if (ltCurLine != null && curLine != ltCurLine) { + this.lastTabCurLine.set(null); + } if (e.code == "Enter" && !ctrlMod) { e.preventDefault(); setTimeout(() => this.doSubmitCmd(), 0); return; } - if (e.code == "Tab") { + if (e.code == "Escape") { e.preventDefault(); - this.setCurLine(this.getCurLine() + "[tab]"); + GlobalModel.toggleInfoMsg(); + return; + } + if (e.code == "KeyC" && e.getModifierState("Control")) { + e.preventDefault(); + inputModel.clearCurLine(); return; } if (e.code == "ArrowUp") { e.preventDefault(); - let hidx = this.historyIndex.get(); - hidx += 1; - if (hidx > win.getNumHistoryItems()) { - hidx = win.getNumHistoryItems(); - } - this.historyIndex.set(hidx); + inputModel.prevHistoryItem(); return; } if (e.code == "ArrowDown") { e.preventDefault(); - let hidx = this.historyIndex.get(); - hidx -= 1; - if (hidx < 0) { - hidx = 0; - } - this.historyIndex.set(hidx); + inputModel.nextHistoryItem(); return; } // console.log(e.code, e.keyCode, e.key, event.which, ctrlMod, e); })(); } - @boundMethod - clearCurLine() { - mobx.action(() => { - this.historyIndex.set(0); - this.modHistory.clear(); - this.modHistory[0] = ""; - })(); - } - - @boundMethod - getCurLine() : string { - let model = GlobalModel; - let hidx = this.historyIndex.get(); - if (hidx < this.modHistory.length && this.modHistory[hidx] != null) { - return this.modHistory[hidx]; - } - let win = model.getActiveWindow(); - if (win == null) { - return ""; - } - let hitem = win.getHistoryItem(-hidx); - if (hitem == null) { - return ""; - } - return hitem.cmdtext; - } - - @boundMethod - setCurLine(val : string) { - let hidx = this.historyIndex.get(); - this.modHistory[hidx] = val; - } - @boundMethod onChange(e : any) { mobx.action(() => { - this.setCurLine(e.target.value); + GlobalModel.inputModel.setCurLine(e.target.value); })(); } @boundMethod doSubmitCmd() { let model = GlobalModel; - let commandStr = this.getCurLine(); + let inputModel = model.inputModel; + let commandStr = inputModel.getCurLine(); let hitem = {cmdtext: commandStr}; - this.clearCurLine(); + inputModel.clearCurLine(); + GlobalModel.clearInfoMsg(true); model.submitRawCommand(commandStr); } render() { - let curLine = this.getCurLine(); + let model = GlobalModel; + let inputModel = model.inputModel; + let curLine = inputModel.getCurLine(); + let win = GlobalModel.getActiveWindow(); + let ri : RemoteInstanceType = null; + if (win != null) { + ri = win.getCurRemoteInstance(); + } + let remote : RemoteType = null; + let remoteState : RemoteStateType = null; + if (ri != null) { + remote = GlobalModel.getRemote(ri.remoteid); + remoteState = ri.state; + } + let promptStr = getRemoteStr(remote); + let cwdStr = getCwdStr(remote, remoteState); + let infoMsg = GlobalModel.infoMsg.get(); + let infoShow = GlobalModel.infoShow.get(); + let istr : string = null; + let istrIdx : number = 0; return ( -
+
+
+ +
+ {infoMsg.infotitle} +
+
+ +
+ {infoMsg.infomsg} +
+
+ 0}> +
+ +
+ {istr} +
+
+ +
+ ... +
+
+
+
+ +
+ {infoMsg.infoerror} +
+
+
- [mike@local ~] + [{promptStr} {cwdStr}]
-
mike@local
+
{promptStr}
diff --git a/src/model.ts b/src/model.ts index 597fd90eb..0135a0c08 100644 --- a/src/model.ts +++ b/src/model.ts @@ -1,7 +1,7 @@ import * as mobx from "mobx"; import {sprintf} from "sprintf-js"; import {boundMethod} from "autobind-decorator"; -import {handleJsonFetchResponse, base64ToArray, genMergeData} from "./util"; +import {handleJsonFetchResponse, base64ToArray, genMergeData, genMergeSimpleData} from "./util"; import {TermWrap} from "./term"; import {v4 as uuidv4} from "uuid"; import type {SessionDataType, WindowDataType, LineType, RemoteType, HistoryItem, RemoteInstanceType, CmdDataType, FeCmdPacketType, TermOptsType, RemoteStateType, ScreenDataType, ScreenWindowType, ScreenOptsType, LayoutType, PtyDataUpdateType, SessionUpdateType, WindowUpdateType, UpdateMessage, LineCmdUpdateType} from "./types"; @@ -296,12 +296,13 @@ class Window { if (load) { this.loaded.set(true); } - this.lines.replace(win.lines || []); + genMergeSimpleData(this.lines, win.lines, (l) => String(l.lineid), (l) => l.lineid); this.history = win.history || []; let cmds = win.cmds || []; for (let i=0; i r.riid, null); })(); } @@ -321,6 +322,9 @@ class Window { getCurRemoteInstance() : RemoteInstanceType { let rname = this.curRemote.get(); + if (rname == null) { + return null; + } let sessionScope = false; if (rname.startsWith("^")) { rname = rname.substr(1); @@ -385,7 +389,7 @@ class Session { sessionIdx : OV; screens : OArr; notifyNum : OV = mobx.observable.box(0); - remoteInstances : OArr = mobx.observable.array([]); + remoteInstances : OArr; constructor(sdata : SessionDataType) { this.sessionId = sdata.sessionid; @@ -399,6 +403,8 @@ class Session { } this.screens = mobx.observable.array(screens, {deep: false}); this.activeScreenId = mobx.observable.box(ces(sdata.activescreenid)); + let remotes = sdata.remotes || []; + this.remoteInstances = mobx.observable.array(remotes); } dispose() : void { @@ -492,6 +498,91 @@ class Session { } } +type InfoType = { + infotitle : string; + infomsg : string; + infoerror : string; + infostrings : string[]; +}; + +type CmdLineUpdateType = { + insertchars : string, + insertpos : number, +}; + +class InputModel { + historyIndex : mobx.IObservableValue = mobx.observable.box(0, {name: "history-index"}); + modHistory : mobx.IObservableArray = mobx.observable.array([""], {name: "mod-history"}); + + updateCmdLine(cmdLine : CmdLineUpdateType) { + mobx.action(() => { + let curLine = this.getCurLine(); + if (curLine.length < cmdLine.insertpos) { + return; + } + let pos = cmdLine.insertpos; + curLine = curLine.substr(0, pos) + cmdLine.insertchars + curLine.substr(pos); + this.setCurLine(curLine); + })(); + } + + setCurLine(val : string) { + let hidx = this.historyIndex.get(); + this.modHistory[hidx] = val; + } + + clearCurLine() { + mobx.action(() => { + this.historyIndex.set(0); + this.modHistory.clear(); + this.modHistory[0] = ""; + })(); + } + + getCurLine() : string { + let model = GlobalModel; + let hidx = this.historyIndex.get(); + if (hidx < this.modHistory.length && this.modHistory[hidx] != null) { + return this.modHistory[hidx]; + } + let win = model.getActiveWindow(); + if (win == null) { + return ""; + } + let hitem = win.getHistoryItem(-hidx); + if (hitem == null) { + return ""; + } + return hitem.cmdtext; + } + + prevHistoryItem() : void { + let model = GlobalModel; + let win = model.getActiveWindow(); + let hidx = this.historyIndex.get(); + hidx += 1; + if (hidx > win.getNumHistoryItems()) { + hidx = win.getNumHistoryItems(); + } + mobx.action(() => { + this.historyIndex.set(hidx); + })(); + return; + } + + nextHistoryItem() : void { + let hidx = this.historyIndex.get(); + hidx -= 1; + if (hidx < 0) { + hidx = 0; + } + mobx.action(() => { + this.historyIndex.set(hidx); + })(); + return; + } +}; + class Model { clientId : string; activeSessionId : OV = mobx.observable.box(null); @@ -501,6 +592,10 @@ class Model { remotes : OArr = mobx.observable.array([], {deep: false}); remotesLoaded : OV = mobx.observable.box(false); windows : OMap = mobx.observable.map({}, {deep: false}); + infoShow : OV = mobx.observable.box(false); + infoMsg : OV = mobx.observable.box(null); + infoTimeoutId : any = null; + inputModel : InputModel; constructor() { this.clientId = getApi().getId(); @@ -508,6 +603,7 @@ class Model { this.loadSessionList(); this.ws = new WSControl(this.clientId, (message : any) => this.runUpdate(message, false)); this.ws.reconnect(); + this.inputModel = new InputModel(); getApi().onTCmd(this.onTCmd.bind(this)); getApi().onICmd(this.onICmd.bind(this)); getApi().onBracketCmd(this.onBracketCmd.bind(this)); @@ -519,6 +615,47 @@ class Model { getApi().contextScreen({screenId: screenId}, {x: e.x, y: e.y}); } + flashInfoMsg(info : InfoType, timeoutMs : number) { + if (this.infoTimeoutId != null) { + clearTimeout(this.infoTimeoutId); + this.infoTimeoutId = null; + } + mobx.action(() => { + this.infoMsg.set(info); + this.infoShow.set(info != null); + })(); + if (info != null && timeoutMs) { + this.infoTimeoutId = setTimeout(() => { + this.clearInfoMsg(false); + }, timeoutMs); + } + } + + clearInfoMsg(setNull : boolean) { + this.infoTimeoutId = null; + mobx.action(() => { + this.infoShow.set(false); + if (setNull) { + this.infoMsg.set(null); + } + })(); + } + + toggleInfoMsg() { + this.infoTimeoutId = null; + mobx.action(() => { + let isShowing = this.infoShow.get(); + if (isShowing) { + this.infoShow.set(false); + } + else { + if (this.infoMsg.get() != null) { + this.infoShow.set(true); + } + } + })(); + } + onTCmd(mods : KeyModsType) { console.log("got cmd-t", mods); GlobalInput.createNewScreen(); @@ -561,7 +698,6 @@ class Model { return; } activeScreen.updatePtyData(ptyMsg); - return; } if ("sessions" in update) { let sessionUpdateMsg : SessionUpdateType = update; @@ -588,13 +724,20 @@ class Model { let lineMsg : LineCmdUpdateType = update; this.addLineCmd(lineMsg.line, lineMsg.cmd, interactive); } + if ("window" in update) { + let winMsg : WindowUpdateType = update; + this.updateWindow(winMsg.window, false); + } + if ("info" in update) { + let info = update.info; + this.flashInfoMsg(info, info.timeoutms); + } + if ("cmdline" in update) { + this.inputModel.updateCmdLine(update.cmdline); + } console.log("run-update>", interactive, update); } - removeSession(sessionId : string) { - console.log("removeSession not implemented"); - } - getActiveSession() : Session { return this.getSessionById(this.activeSessionId.get()); } @@ -631,7 +774,7 @@ class Model { let existingWin = this.windows.get(winKey); if (existingWin == null) { if (!load) { - console.log("cannot update window that does not exist"); + console.log("cannot update window that does not exist", winKey); return; } let newWindow = new Window(win.sessionid, win.windowid); @@ -707,7 +850,7 @@ class Model { } })(); }).catch((err) => { - this.errorHandler("calling run-command", err); + this.errorHandler("calling run-command", err, true); }); } @@ -756,7 +899,7 @@ class Model { } })(); }).catch((err) => { - this.errorHandler("getting session list", err); + this.errorHandler("getting session list", err, false); }); } @@ -805,7 +948,7 @@ class Model { this.updateWindow(data.data, true); return; }).catch((err) => { - this.errorHandler(sprintf("getting window=%s", windowId), err); + this.errorHandler(sprintf("getting window=%s", windowId), err, false); }); return newWin; } @@ -818,7 +961,7 @@ class Model { this.remotesLoaded.set(true); })(); }).catch((err) => { - this.errorHandler("calling get-remotes", err) + this.errorHandler("calling get-remotes", err, false) }); } @@ -852,8 +995,15 @@ class Model { return window.getCmd(line.cmdid); } - errorHandler(str : string, err : any) { + errorHandler(str : string, err : any, interactive : boolean) { console.log("[error]", str, err); + if (interactive) { + let errMsg = "error running command"; + if (err != null && err.message) { + errMsg = err.message; + } + this.flashInfoMsg({infoerror: errMsg}, null); + } } sendInputPacket(inputPacket : any) { diff --git a/src/sh2.less b/src/sh2.less index 51e5c5706..7ae70f9ed 100644 --- a/src/sh2.less +++ b/src/sh2.less @@ -464,6 +464,10 @@ body .xterm .xterm-viewport { border-bottom: 1px solid #ccc; border-bottom-right-radius: 10px; + &.has-info { + padding-top: 5px; + } + .cmd-input-context { color: #fff; font-family: 'JetBrains Mono', monospace; @@ -491,6 +495,44 @@ body .xterm .xterm-viewport { color: #d3d7cf; } } + + .cmd-input-info { + .info-msg { + font-family: 'JetBrains Mono', monospace; + font-weight: 400; + font-size: 14px; + color: #729fcf; + padding-bottom: 2px; + } + + .info-title { + font-family: 'JetBrains Mono', monospace; + font-weight: 600; + font-size: 14px; + color: #729fcf; + padding-bottom: 2px; + } + + .info-strings { + display: flex; + flex-direction: row; + flex-wrap: wrap; + padding-bottom: 5px; + + .info-string { + min-width: 200px; + color: #d3d7cf; + } + } + + .info-error { + font-family: 'JetBrains Mono', monospace; + font-weight: 600; + font-size: 14px; + color: #cc0000; + padding-bottom: 2px; + } + } } .bold { diff --git a/src/types.ts b/src/types.ts index 105d18764..8b3b54802 100644 --- a/src/types.ts +++ b/src/types.ts @@ -87,6 +87,8 @@ type RemoteInstanceType = { remoteid : string, sessionscope : boolean, state : RemoteStateType, + + remove? : boolean, } type WindowDataType = { @@ -185,7 +187,6 @@ type UpdateMessage = PtyDataUpdateType | SessionUpdateType | LineCmdUpdateType; type WindowUpdateType = { window: WindowDataType, - remove: boolean, } export type {SessionDataType, LineType, RemoteType, RemoteStateType, RemoteInstanceType, WindowDataType, HistoryItem, CmdRemoteStateType, FeCmdPacketType, TermOptsType, CmdStartPacketType, CmdDonePacketType, CmdDataType, ScreenDataType, ScreenOptsType, ScreenWindowType, LayoutType, PtyDataUpdateType, SessionUpdateType, WindowUpdateType, UpdateMessage, LineCmdUpdateType}; diff --git a/src/util.ts b/src/util.ts index d9f89cf3c..b2d3db232 100644 --- a/src/util.ts +++ b/src/util.ts @@ -58,6 +58,40 @@ interface IObjType { mergeData : (data : DataType) => void, } +interface ISimpleDataType { + remove? : boolean; +} + +function genMergeSimpleData(objs : mobx.IObservableArray, dataArr : T, idFn : (obj : T) => string, sortIdxFn : (obj : T) => number) { + if (dataArr == null || dataArr.length == 0) { + return; + } + let objMap : Record = {}; + for (let i=0; i { + return sortIdxFn(a) - sortIdxFn(b); + }); + } + objs.replace(newObjs); +} + function genMergeData, DataType extends IDataType>( objs : mobx.IObservableArray, dataArr : DataType[], @@ -106,4 +140,4 @@ function genMergeData, DataType extends IData objs.replace(newObjs); } -export {handleJsonFetchResponse, base64ToArray, genMergeData}; +export {handleJsonFetchResponse, base64ToArray, genMergeData, genMergeSimpleData};