history checkpoint

This commit is contained in:
sawka 2022-08-30 17:05:35 -07:00
parent d037666ad1
commit 087c0c4f1f
6 changed files with 217 additions and 54 deletions

View File

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

View File

@ -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 (
<div key={hitem.historynum} className={cn("history-item", {"is-selected": selNum == hitem.historynum}, "hnum-" + hitem.historynum)}>
<div key={hitem.historynum} className={cn("history-item", {"is-selected": selNum == hitem.historynum}, "hnum-" + hitem.historynum)} onClick={() => this.handleItemClick(hitem)}>
<div className="history-line">{(selNum == hitem.historynum ? "*" : " ")}{sprintf("%5s", hitem.historynum)} {lines[0]}</div>
<For each="line" index="index" of={lines.slice(1)}>
<div key={idx} className="history-line">{line}</div>
@ -553,17 +591,23 @@ class HistoryInfo extends React.Component<{}, {}> {
</div>
);
}
@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 (
<div className="cmd-history">
<div className="history-title">
showing history for
history
{" "}
<span className="term-bright-white">[containing '']</span>
{" "}
@ -574,6 +618,7 @@ class HistoryInfo extends React.Component<{}, {}> {
<span className="term-bright-white">[this remote &#x2318;R]</span>
{" "}
<span className="term-bright-white">[including metacmds &#x2318;M]</span>
{" "} <span className="history-clickable-opt" onClick={this.handleClose}>(close ESC)</span>
</div>
<div className="history-items">
<If condition={hitems.length == 0}>
@ -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() {

View File

@ -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<boolean> = mobx.observable.box(false);
infoShow : OV<boolean> = mobx.observable.box(false);
loadId : string = null;
historyLoading : mobx.IObservableValue<boolean> = mobx.observable.box(false);
historySessionId : string = null;
historyItems : mobx.IObservableValue<HistoryItem[]> = mobx.observable.box(null, {name: "history-items", deep: false});
historyItems : mobx.IObservableValue<HistoryItem[]> = mobx.observable.box(null, {name: "history-items", deep: false}); // sorted in reverse (most recent is index 0)
historyIndex : mobx.IObservableValue<number> = mobx.observable.box(0, {name: "history-index"}); // 1-indexed (because 0 is current)
modHistory : mobx.IObservableArray<string> = mobx.observable.array([""], {name: "mod-history"});
setHIdx : number = 0;
@ -586,6 +587,13 @@ class InputModel {
infoTimeoutId : any = null;
historySelectedNum : OV<string> = 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<hitems.length; i++) {
if (hitems[i].historynum == selNum) {
return i;
}
}
return -1;
}
scrollHistoryItemIntoView(hnum : string) : void {
let elem : HTMLElement = document.querySelector(".cmd-history .hnum-" + hnum);
if (elem == null) {
return;
}
let historyDiv = elem.closest(".cmd-history");
if (historyDiv == null) {
return;
}
let buffer = 15;
let titleHeight = 24;
let elemOffset = elem.offsetTop;
let elemHeight = elem.clientHeight;
let topPos = historyDiv.scrollTop;
let endPos = topPos + historyDiv.clientHeight;
if (elemOffset + elemHeight + buffer > 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};

View File

@ -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),

View File

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

View File

@ -129,6 +129,7 @@ type HistoryItem = {
remove : boolean,
remote : RemotePtrType,
ismetacmd : boolean,
historynum : string,
};
type CmdRemoteStateType = {