From 087c0c4f1f075b522ae30843c43a8be0b734c078 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 30 Aug 2022 17:05:35 -0700 Subject: [PATCH] history checkpoint --- src/emain.ts | 9 ++- src/main.tsx | 75 ++++++++++++++++----- src/model.ts | 178 +++++++++++++++++++++++++++++++++++++++---------- src/preload.js | 1 + src/sh2.less | 7 +- src/types.ts | 1 + 6 files changed, 217 insertions(+), 54 deletions(-) diff --git a/src/emain.ts b/src/emain.ts index 5d5f3dd0..bf0f61ac 100644 --- a/src/emain.ts +++ b/src/emain.ts @@ -104,8 +104,13 @@ function createWindow(size : {width : number, height : number}) { } return; } - if (input.code == "KeyR" && input.meta && input.alt) { - createRemotesWindow(); + //if (input.code == "KeyR" && input.meta && input.alt) { + // createRemotesWindow(); + // e.preventDefault(); + // return; + //} + if (input.code == "KeyH" && input.meta) { + win.webContents.send("h-cmd", mods); e.preventDefault(); return; } diff --git a/src/main.tsx b/src/main.tsx index afd1cea6..d9962186 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -8,9 +8,9 @@ import dayjs from 'dayjs' import {If, For, When, Otherwise, Choose} from "tsx-control-statements/components"; import cn from "classnames" import {TermWrap} from "./term"; -import type {SessionDataType, LineType, CmdDataType, RemoteType, RemoteStateType, RemoteInstanceType, RemotePtrType} from "./types"; +import type {SessionDataType, LineType, CmdDataType, RemoteType, RemoteStateType, RemoteInstanceType, RemotePtrType, HistoryItem} from "./types"; import localizedFormat from 'dayjs/plugin/localizedFormat'; -import {GlobalModel, GlobalInput, Session, Cmd, Window, Screen, ScreenWindow, riToRPtr} from "./model"; +import {GlobalModel, GlobalCommandRunner, Session, Cmd, Window, Screen, ScreenWindow, riToRPtr} from "./model"; dayjs.extend(localizedFormat) @@ -441,6 +441,10 @@ class TextAreaInput extends React.Component<{}, {}> { } if (e.code == "Enter") { e.preventDefault(); + if (inputModel.historyShow.get()) { + inputModel.grabSelectedHistoryItem(); + return; + } if (!ctrlMod) { setTimeout(() => GlobalModel.inputModel.uiSubmitCommand(), 0); return; @@ -459,7 +463,16 @@ class TextAreaInput extends React.Component<{}, {}> { inputModel.clearCurLine(); return; } + if (e.code == "KeyR" && e.getModifierState("Control")) { + e.preventDefault(); + GlobalCommandRunner.openHistory(); + return; + } if (e.code == "ArrowUp" || e.code == "ArrowDown") { + if (inputModel.historyShow.get()) { + inputModel.moveHistorySelection(e.code == "ArrowUp" ? -1 : 1); + return; + } let linePos = this.getLinePos(e.target); if (e.code == "ArrowUp") { if (!lastHist && linePos.linePos > 1) { @@ -484,7 +497,11 @@ class TextAreaInput extends React.Component<{}, {}> { } if (e.code == "PageUp" || e.code == "PageDown") { e.preventDefault(); - let infoScroll = GlobalModel.inputModel.hasScrollingInfoMsg(); + if (inputModel.historyShow.get()) { + inputModel.moveHistorySelection(e.code == "PageUp" ? -10 : 10); + return; + } + let infoScroll = inputModel.hasScrollingInfoMsg(); if (infoScroll) { let div = document.querySelector(".cmd-input-info"); let amt = pageSize(div); @@ -529,23 +546,44 @@ class TextAreaInput extends React.Component<{}, {}> { @mobxReact.observer class HistoryInfo extends React.Component<{}, {}> { + lastClickHNum : string = null; + lastClickTs : number = 0; + componentDidMount() { let inputModel = GlobalModel.inputModel; let selNum = inputModel.historySelectedNum.get(); if (selNum != null) { - let elem = document.querySelector(".cmd-history .hnum-" + selNum); - if (elem != null) { - elem.scrollIntoView({block: "nearest"}); - } + inputModel.scrollHistoryItemIntoView(selNum); } } - renderHItem(hitem : HistoryItem, selNum : number) : any { + @boundMethod + handleItemClick(hitem : HistoryItem) { + let inputModel = GlobalModel.inputModel; + let selNum = inputModel.historySelectedNum.get(); + if (this.lastClickHNum == hitem.historynum && selNum == hitem.historynum) { + inputModel.grabSelectedHistoryItem(); + return; + } + inputModel.focusCmdInput(); + 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); + } + + renderHItem(hitem : HistoryItem, selNum : string) : any { let lines = hitem.cmdstr.split("\n"); let line : string = ""; let idx = 0; return ( -
+
this.handleItemClick(hitem)}>
{(selNum == hitem.historynum ? "*" : " ")}{sprintf("%5s", hitem.historynum)} {lines[0]}
{line}
@@ -553,17 +591,23 @@ class HistoryInfo extends React.Component<{}, {}> {
); } + + @boundMethod + handleClose() { + GlobalModel.inputModel.toggleInfoMsg(); + } render() { let inputModel = GlobalModel.inputModel; let idx : number = 0; - let hitems : HistoryItem[] = inputModel.historyItems.get() ?? []; let selNum = inputModel.historySelectedNum.get(); + let hitems = inputModel.getFilteredHistoryItems(); hitems = hitems.slice().reverse(); + let hitem : HistoryItem = null; return (
- showing history for + history {" "} [containing ''] {" "} @@ -574,6 +618,7 @@ class HistoryInfo extends React.Component<{}, {}> { [this remote ⌘R] {" "} [including metacmds ⌘M] + {" "} (close ESC)
@@ -937,7 +982,7 @@ class ScreenTabs extends React.Component<{session : Session}, {}> { @boundMethod handleNewScreen() { let {session} = this.props; - GlobalInput.createNewScreen(); + GlobalCommandRunner.createNewScreen(); } @boundMethod @@ -953,7 +998,7 @@ class ScreenTabs extends React.Component<{session : Session}, {}> { if (screen == null) { return; } - GlobalInput.switchScreen(screenId); + GlobalCommandRunner.switchScreen(screenId); } handleContextMenu(e : any, screenId : string) : void { @@ -1019,11 +1064,11 @@ class MainSideBar extends React.Component<{}, {}> { } handleSessionClick(sessionId : string) { - GlobalInput.switchSession(sessionId); + GlobalCommandRunner.switchSession(sessionId); } handleNewSession() { - GlobalInput.createNewSession(); + GlobalCommandRunner.createNewSession(); } clickRemotes() { diff --git a/src/model.ts b/src/model.ts index e05050c1..1194d8f8 100644 --- a/src/model.ts +++ b/src/model.ts @@ -51,6 +51,7 @@ type ElectronApi = { getId : () => string, onTCmd : (callback : (mods : KeyModsType) => void) => void, onICmd : (callback : (mods : KeyModsType) => void) => void, + onHCmd : (callback : (mods : KeyModsType) => void) => void, onMetaArrowUp : (callback : () => void) => void, onMetaArrowDown : (callback : () => void) => void, onBracketCmd : (callback : (event : any, arg : {relative : number}, mods : KeyModsType) => void) => void, @@ -573,10 +574,10 @@ type HistoryQueryOpts = { class InputModel { historyShow : OV = mobx.observable.box(false); infoShow : OV = mobx.observable.box(false); - + + loadId : string = null; historyLoading : mobx.IObservableValue = mobx.observable.box(false); - historySessionId : string = null; - historyItems : mobx.IObservableValue = mobx.observable.box(null, {name: "history-items", deep: false}); + historyItems : mobx.IObservableValue = mobx.observable.box(null, {name: "history-items", deep: false}); // sorted in reverse (most recent is index 0) historyIndex : mobx.IObservableValue = mobx.observable.box(0, {name: "history-index"}); // 1-indexed (because 0 is current) modHistory : mobx.IObservableArray = mobx.observable.array([""], {name: "mod-history"}); setHIdx : number = 0; @@ -586,6 +587,13 @@ class InputModel { infoTimeoutId : any = null; historySelectedNum : OV = mobx.observable.box(null); + focusCmdInput() : void { + let elem = document.getElementById("main-cmd-input"); + if (elem != null) { + elem.focus(); + } + } + updateCmdLine(cmdLine : CmdLineUpdateType) : void { mobx.action(() => { let curLine = this.getCurLine(); @@ -615,6 +623,103 @@ class InputModel { })(); } + getFilteredHistoryItems() : HistoryItem[] { + let hitems : HistoryItem[] = this.historyItems.get() ?? []; + return hitems; + } + + findCurrentHistoryIndex() : number { + let hitems = this.historyItems.get(); + let selNum = this.historySelectedNum.get(); + for (let i=0; i endPos) { + if (elemHeight + buffer > historyDiv.clientHeight - titleHeight) { + historyDiv.scrollTop = elemOffset - titleHeight; + return; + } + historyDiv.scrollTop = elemOffset - historyDiv.clientHeight + elemHeight + buffer; + return; + } + if (elemOffset < topPos + titleHeight) { + if (elemHeight + buffer > historyDiv.clientHeight - titleHeight) { + historyDiv.scrollTop = elemOffset - titleHeight; + return; + } + historyDiv.scrollTop = elemOffset - titleHeight - buffer; + return; + } + } + + setHistorySelectionNum(hnum : string) : void { + mobx.action(() => { + this.historySelectedNum.set(hnum); + this.scrollHistoryItemIntoView(hnum); + })(); + } + + grabSelectedHistoryItem() : void { + let idx = this.findCurrentHistoryIndex(); + if (idx == -1) { + return; + } + let hitem = this.historyItems.get()[idx]; + mobx.action(() => { + this.clearCurLine(); + this.setCurLine(hitem.cmdstr); + this.historyShow.set(false); + })(); + } + + moveHistorySelection(amt : number) : void { + let hitems : HistoryItem[] = this.historyItems.get() ?? []; + if (hitems.length == 0 || amt == 0) { + return; + } + let idx = this.findCurrentHistoryIndex(); + if (idx == -1) { + if (amt < 0) { + let hitem = hitems[hitems.length-1]; + this.setHistorySelectionNum(hitem.historynum); + } + else { + let hitem = hitems[0]; + this.setHistorySelectionNum(hitem.historynum); + } + return; + } + idx += -amt; // negate because history array is sorted in reverse + if (idx < 0) { + idx = 0; + } + if (idx >= hitems.length) { + idx = hitems.length-1; + } + let hitem = hitems[idx]; + this.setHistorySelectionNum(hitem.historynum); + } + flashInfoMsg(info : InfoType, timeoutMs : number) : void { if (this.infoTimeoutId != null) { clearTimeout(this.infoTimeoutId); @@ -655,17 +760,6 @@ class InputModel { return div.scrollHeight > div.clientHeight; } - hasScrollingHistory() : boolean { - if (!this.historyShow.get()) { - return false; - } - let div = document.querySelector(".cmd-history"); - if (div == null) { - return false; - } - return div.scrollHeight > div.clientHeight; - } - clearInfoMsg(setNull : boolean) : void { this.infoTimeoutId = null; mobx.action(() => { @@ -732,14 +826,18 @@ class InputModel { this.setHIdx = 0; return; } + let loadId = uuidv4(); mobx.action(() => { - this.historySessionId = sessionId; + this.loadId = loadId; this.historyItems.set(null); this.historyLoading.set(true); })(); let usp = new URLSearchParams({sessionid: sessionId, windowid: win.windowId}); let url = new URL("http://localhost:8080/api/get-history?" + usp.toString()); fetch(url).then((resp) => handleJsonFetchResponse(url, resp)).then((data) => { + if (loadId != this.loadId) { + return; // stale load + } mobx.action(() => { if (!this.historyLoading.get()) { return; @@ -750,7 +848,6 @@ class InputModel { } if (data.data && data.data.history) { let hitems : HistoryItem[] = data.data.history || []; - this.historySessionId = sessionId; this.historyItems.set(hitems); this.historyLoading.set(false); let hlen = hitems.length; @@ -763,6 +860,10 @@ class InputModel { } })(); }).catch((err) => { + let isStale = (loadId != this.loadId); + if (isStale) { + return; + } GlobalModel.errorHandler("getting history items", err, false); mobx.action(() => { this.historyLoading.set(false); @@ -873,6 +974,7 @@ class Model { this.inputModel = new InputModel(); getApi().onTCmd(this.onTCmd.bind(this)); getApi().onICmd(this.onICmd.bind(this)); + getApi().onHCmd(this.onHCmd.bind(this)); getApi().onMetaArrowUp(this.onMetaArrowUp.bind(this)); getApi().onMetaArrowDown(this.onMetaArrowDown.bind(this)); getApi().onBracketCmd(this.onBracketCmd.bind(this)); @@ -917,22 +1019,22 @@ class Model { return rtn; } - onTCmd(mods : KeyModsType) { + onTCmd(e : any, mods : KeyModsType) { console.log("got cmd-t", mods); - GlobalInput.createNewScreen(); + GlobalCommandRunner.createNewScreen(); } - focusCmdInput() : void { - let elem = document.getElementById("main-cmd-input"); - if (elem != null) { - elem.focus(); + onICmd(e : any, mods : KeyModsType) { + this.inputModel.focusCmdInput(); + } + + onHCmd(e : any, mods : KeyModsType) { + let focusedLine = this.getFocusedLine(); + if (focusedLine != null && focusedLine.cmdInputFocus) { + GlobalCommandRunner.openHistory(); } } - onICmd(mods : KeyModsType) { - this.focusCmdInput(); - } - getFocusedLine() : LineFocusType { let elem = document.getElementById("main-cmd-input"); if (document.activeElement == elem) { @@ -1019,7 +1121,7 @@ class Model { } let runningLines = win.getRunningCmdLines(); if (runningLines.length == 0) { - this.focusCmdInput(); + this.inputModel.focusCmdInput(); return; } let foundIdx = -1; @@ -1030,7 +1132,7 @@ class Model { } } if (foundIdx == -1 || foundIdx == runningLines.length - 1) { - this.focusCmdInput(); + this.inputModel.focusCmdInput(); return; } let switchLine = runningLines[foundIdx+1]; @@ -1048,15 +1150,15 @@ class Model { onBracketCmd(e : any, arg : {relative: number}, mods : KeyModsType) { if (arg.relative == 1) { - GlobalInput.switchScreen("+"); + GlobalCommandRunner.switchScreen("+"); } else if (arg.relative == -1) { - GlobalInput.switchScreen("-"); + GlobalCommandRunner.switchScreen("-"); } } onDigitCmd(e : any, arg : {digit: number}, mods : KeyModsType) { - GlobalInput.switchScreen(String(arg.digit)); + GlobalCommandRunner.switchScreen(String(arg.digit)); } isConnected() : boolean { @@ -1439,7 +1541,7 @@ class Model { } } -class InputClass { +class CommandRunner { constructor() { } @@ -1450,6 +1552,10 @@ class InputClass { })(); } + openHistory() { + GlobalModel.submitCommand("history", null, null, {"nohist": "1"}, true); + } + switchSession(session : string) { GlobalModel.submitCommand("session", null, [session], {"nohist": "1"}, false); this.clearCmdInput(); @@ -1477,14 +1583,14 @@ class InputClass { }; let GlobalModel : Model = null; -let GlobalInput : InputClass = null; +let GlobalCommandRunner : CommandRunner = null; if ((window as any).GlobalModal == null) { (window as any).GlobalModel = new Model(); - (window as any).GlobalInput = new InputClass(); + (window as any).GlobalCommandRunner = new CommandRunner(); } GlobalModel = (window as any).GlobalModel; -GlobalInput = (window as any).GlobalInput; +GlobalCommandRunner = (window as any).GlobalCommandRunner; -export {Model, Session, Window, GlobalModel, GlobalInput, Cmd, Screen, ScreenWindow, riToRPtr}; +export {Model, Session, Window, GlobalModel, GlobalCommandRunner, Cmd, Screen, ScreenWindow, riToRPtr}; diff --git a/src/preload.js b/src/preload.js index 61732952..427fc568 100644 --- a/src/preload.js +++ b/src/preload.js @@ -4,6 +4,7 @@ contextBridge.exposeInMainWorld("api", { getId: () => ipcRenderer.sendSync("get-id"), onTCmd: (callback) => ipcRenderer.on("t-cmd", callback), onICmd: (callback) => ipcRenderer.on("i-cmd", callback), + onHCmd: (callback) => ipcRenderer.on("h-cmd", callback), onMetaArrowUp: (callback) => ipcRenderer.on("meta-arrowup", callback), onMetaArrowDown: (callback) => ipcRenderer.on("meta-arrowdown", callback), onBracketCmd: (callback) => ipcRenderer.on("bracket-cmd", callback), diff --git a/src/sh2.less b/src/sh2.less index 2e56042d..8974e56b 100644 --- a/src/sh2.less +++ b/src/sh2.less @@ -617,11 +617,12 @@ body .xterm .xterm-viewport { .cmd-history { color: @term-white; margin-bottom: 5px; - overflow-y: auto; + overflow: auto; flex-shrink: 1; .history-title { position: absolute; + z-index: 2; top: 5px; left: 20px; background-color: black; @@ -630,6 +631,10 @@ body .xterm .xterm-viewport { font-size: 14px; color: #729fcf; padding-bottom: 4px; + + .history-clickable-opt { + cursor: pointer; + } } .history-items { diff --git a/src/types.ts b/src/types.ts index a8318b83..a339233d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -129,6 +129,7 @@ type HistoryItem = { remove : boolean, remote : RemotePtrType, ismetacmd : boolean, + historynum : string, }; type CmdRemoteStateType = {