implement info panel, more control in cmd-input, completions, errors

This commit is contained in:
sawka 2022-08-10 18:35:18 -07:00
parent 1f4ac87a9a
commit 5082330dcf
5 changed files with 379 additions and 106 deletions

View File

@ -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
</div>
);
}
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 (
<div className="metapart-mono cmdtext">
<span className="term-bright-green">[{promptStr} {cwd}]</span> {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<number> = mobx.observable.box(0, {name: "history-index"});
modHistory : mobx.IObservableArray<string> = mobx.observable.array([""], {name: "mod-history"});
lastTabCurLine : mobx.IObservableValue<string> = 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 (
<div className="box cmd-input has-background-black">
<div className={cn("box cmd-input has-background-black", {"has-info": infoShow})}>
<div className="cmd-input-info" style={{display: (infoShow ? "block" : "none")}}>
<If condition={infoMsg && infoMsg.infotitle != null}>
<div className="info-title">
{infoMsg.infotitle}
</div>
</If>
<If condition={infoMsg && infoMsg.infomsg != null}>
<div className="info-msg">
{infoMsg.infomsg}
</div>
</If>
<If condition={infoMsg && infoMsg.infostrings != null && infoMsg.infostrings.length > 0}>
<div className="info-strings">
<For each="istr" index="istrIdx" of={infoMsg.infostrings}>
<div key={istrIdx} className="info-string">
{istr}
</div>
</For>
<If condition={infoMsg.infostringsmore}>
<div key="more" className="info-string">
...
</div>
</If>
</div>
</If>
<If condition={infoMsg && infoMsg.infoerror != null}>
<div className="info-error">
{infoMsg.infoerror}
</div>
</If>
</div>
<div className="cmd-input-context">
<div className="has-text-white">
<span className="bold term-bright-green">[mike@local ~]</span>
<span className="bold term-bright-green">[{promptStr} {cwdStr}]</span>
</div>
</div>
<div className="cmd-input-field field has-addons">
<div className="control cmd-quick-context">
<div className="button is-static">mike@local</div>
<div className="button is-static">{promptStr}</div>
</div>
<div className="control cmd-input-control is-expanded">
<textarea id="main-cmd-input" value={curLine} onKeyDown={this.onKeyDown} onChange={this.onChange} className="input"></textarea>

View File

@ -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<cmds.length; i++) {
this.cmds[cmds[i].cmdid] = new Cmd(cmds[i]);
}
genMergeSimpleData(this.remoteInstances, win.remotes, (r) => 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<number>;
screens : OArr<Screen>;
notifyNum : OV<number> = mobx.observable.box(0);
remoteInstances : OArr<RemoteInstanceType> = mobx.observable.array([]);
remoteInstances : OArr<RemoteInstanceType>;
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<number> = mobx.observable.box(0, {name: "history-index"});
modHistory : mobx.IObservableArray<string> = 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<string> = mobx.observable.box(null);
@ -501,6 +592,10 @@ class Model {
remotes : OArr<RemoteType> = mobx.observable.array([], {deep: false});
remotesLoaded : OV<boolean> = mobx.observable.box(false);
windows : OMap<string, Window> = mobx.observable.map({}, {deep: false});
infoShow : OV<boolean> = mobx.observable.box(false);
infoMsg : OV<InfoType> = 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) {

View File

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

View File

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

View File

@ -58,6 +58,40 @@ interface IObjType<DataType> {
mergeData : (data : DataType) => void,
}
interface ISimpleDataType {
remove? : boolean;
}
function genMergeSimpleData<T extends ISimpleDataType>(objs : mobx.IObservableArray<T>, dataArr : T, idFn : (obj : T) => string, sortIdxFn : (obj : T) => number) {
if (dataArr == null || dataArr.length == 0) {
return;
}
let objMap : Record<string, T> = {};
for (let i=0; i<objs.length; i++) {
let obj = objs[i];
let id = idFn(obj);
objMap[id] = obj;
}
for (let i=0; i<dataArr.length; i++) {
let dataItem = dataArr[i];
let id = idFn(dataItem);
if (dataItem.remove) {
delete objMap[id];
continue;
}
else {
objMap[id] = dataItem;
}
}
let newObjs = Object.values(objMap);
if (sortIdxFn) {
newObjs.sort((a, b) => {
return sortIdxFn(a) - sortIdxFn(b);
});
}
objs.replace(newObjs);
}
function genMergeData<ObjType extends IObjType<DataType>, DataType extends IDataType>(
objs : mobx.IObservableArray<ObjType>,
dataArr : DataType[],
@ -106,4 +140,4 @@ function genMergeData<ObjType extends IObjType<DataType>, DataType extends IData
objs.replace(newObjs);
}
export {handleJsonFetchResponse, base64ToArray, genMergeData};
export {handleJsonFetchResponse, base64ToArray, genMergeData, genMergeSimpleData};