From 4744eaa49169fe2a0f27ad62adecd941dce818ec Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 13 Oct 2022 18:58:21 -0700 Subject: [PATCH] handle non-running terminal keypresses --- src/main.tsx | 7 ++---- src/model.ts | 62 ++++++++++++++++++++++++++++++++++++++++++---------- src/term.ts | 50 +++++++++++++++++++++++++++++------------- src/util.ts | 6 ++++- 4 files changed, 93 insertions(+), 32 deletions(-) diff --git a/src/main.tsx b/src/main.tsx index f24d5afb..80f4b199 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -12,6 +12,7 @@ import {TermWrap} from "./term"; import type {SessionDataType, LineType, CmdDataType, RemoteType, RemoteStateType, RemoteInstanceType, RemotePtrType, HistoryItem, HistoryQueryOpts, RemoteEditType} from "./types"; import localizedFormat from 'dayjs/plugin/localizedFormat'; import {GlobalModel, GlobalCommandRunner, Session, Cmd, Window, Screen, ScreenWindow, riToRPtr, widthToCols, termWidthFromCols, termHeightFromRows} from "./model"; +import {isModKeyPress} from "./util"; dayjs.extend(localizedFormat) @@ -439,10 +440,6 @@ class TextAreaInput extends React.Component<{}, {}> { } } - isModKeyPress(e : any) { - return e.code.match(/^(Control|Meta|Alt|Shift)(Left|Right)$/); - } - 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; @@ -452,7 +449,7 @@ class TextAreaInput extends React.Component<{}, {}> { @mobx.action @boundMethod onKeyDown(e : any) { mobx.action(() => { - if (this.isModKeyPress(e)) { + if (isModKeyPress(e)) { return; } let model = GlobalModel; diff --git a/src/model.ts b/src/model.ts index 1ccc6432..bf6aaa00 100644 --- a/src/model.ts +++ b/src/model.ts @@ -2,7 +2,7 @@ import * as mobx from "mobx"; import {sprintf} from "sprintf-js"; import {boundMethod} from "autobind-decorator"; import {debounce} from "throttle-debounce"; -import {handleJsonFetchResponse, base64ToArray, genMergeData, genMergeSimpleData, boundInt} from "./util"; +import {handleJsonFetchResponse, base64ToArray, genMergeData, genMergeSimpleData, boundInt, isModKeyPress} from "./util"; import {TermWrap} from "./term"; import {v4 as uuidv4} from "uuid"; import type {SessionDataType, WindowDataType, LineType, RemoteType, HistoryItem, RemoteInstanceType, RemotePtrType, CmdDataType, FeCmdPacketType, TermOptsType, RemoteStateType, ScreenDataType, ScreenWindowType, ScreenOptsType, LayoutType, PtyDataUpdateType, ModelUpdateType, UpdateMessage, InfoType, CmdLineUpdateType, UIContextType, HistoryInfoType, HistoryQueryOpts, FeInputPacketType, TermWinSize, RemoteInputPacketType} from "./types"; @@ -171,7 +171,8 @@ class Cmd { return cmdStatusIsRunning(data.status); } - handleKey(event : any) { + handleData(data : string, termWrap : TermWrap) : void { + console.log("handle data", {data: data}); if (!this.isRunning()) { return; } @@ -179,7 +180,7 @@ class Cmd { type: "feinput", ck: this.sessionId + "/" + this.cmdId, remote: this.remote, - inputdata64: btoa(event.key), + inputdata64: btoa(data), }; GlobalModel.sendInputPacket(inputPacket); } @@ -460,6 +461,33 @@ class ScreenWindow { } } + termCustomKeyHandlerInternal(e : any, termWrap : TermWrap) : void { + if (e.code == "ArrowUp") { + termWrap.terminal.scrollLines(-1); + return; + } + if (e.code == "ArrowDown") { + termWrap.terminal.scrollLines(1); + return; + } + if (e.code == "PageUp") { + termWrap.terminal.scrollPages(-1); + return; + } + if (e.code == "PageDown") { + termWrap.terminal.scrollPages(1); + return; + } + } + + termCustomKeyHandler(e : any, termWrap : TermWrap) : boolean { + if (e.type != "keydown" || isModKeyPress(e)) { + return termWrap.isRunning; + } + this.termCustomKeyHandlerInternal(e, termWrap); + return termWrap.isRunning; + } + connectElem(elem : Element, line : LineType, cmd : Cmd, width : number) { let cmdId = cmd.cmdId; let termWrap = this.getTermWrap(cmdId); @@ -469,9 +497,16 @@ class ScreenWindow { } let cols = widthToCols(width); let usedRows = GlobalModel.getTUR(this.sessionId, cmdId, cols); - termWrap = new TermWrap( - elem, {sessionId: this.sessionId, cmdId: cmdId}, usedRows, cmd.getTermOpts(), {height: 0, width: width}, - cmd.handleKey.bind(cmd), (focus : boolean) => this.setTermFocus(line.linenum, focus), cmd.isRunning()); + termWrap = new TermWrap(elem, { + termContext: {sessionId: this.sessionId, cmdId: cmdId}, + usedRows: usedRows, + termOpts: cmd.getTermOpts(), + winSize: {height: 0, width: width}, + dataHandler: cmd.handleData.bind(cmd), + focusHandler: (focus : boolean) => this.setTermFocus(line.linenum, focus), + isRunning: cmd.isRunning(), + customKeyHandler: this.termCustomKeyHandler.bind(this), + }); this.terms[cmdId] = termWrap; if ((this.focusType.get() == "cmd" || this.focusType.get() == "cmd-fg") && this.selectedLine.get() == line.linenum) { termWrap.focusTerminal(); @@ -1331,7 +1366,7 @@ class InputModel { })(); } - termKeyHandler(remoteId : string, event : any) : void { + termKeyHandler(remoteId : string, event : any, termWrap : TermWrap) : void { let remote = GlobalModel.getRemote(remoteId); if (remote == null) { return; @@ -1366,10 +1401,15 @@ class InputModel { } else { let termOpts = {rows: RemotePtyRows, cols: RemotePtyCols, flexrows: false, maxptysize: 64*1024}; - this.remoteTermWrap = new TermWrap( - elem, {remoteId: remoteId}, RemotePtyRows, termOpts, null, - (e) => { this.termKeyHandler(remoteId, e)}, - this.setRemoteTermWrapFocus.bind(this), true); + this.remoteTermWrap = new TermWrap(elem, { + termContext: {remoteId: remoteId}, + usedRows: RemotePtyRows, + termOpts: termOpts, + winSize: null, + keyHandler: (e, termWrap) => { this.termKeyHandler(remoteId, e, termWrap)}, + focusHandler: this.setRemoteTermWrapFocus.bind(this), + isRunning: true, + }); } } } diff --git a/src/term.ts b/src/term.ts index a96aa44e..0b1a2a85 100644 --- a/src/term.ts +++ b/src/term.ts @@ -22,6 +22,18 @@ const MaxTermCols = 1024; type TermContext = {sessionId? : string, cmdId? : string, remoteId? : string}; +type TermWrapOpts = { + termContext : TermContext, + usedRows? : number, + termOpts : TermOptsType, + winSize : WindowSize, + keyHandler? : (event : any, termWrap : TermWrap) => void, + focusHandler? : (focus : boolean) => void, + dataHandler? : (data : string, termWrap : TermWrap) => void, + isRunning : boolean, + customKeyHandler? : (event : any, termWrap : TermWrap) => boolean, +}; + // cmd-instance class TermWrap { terminal : any; @@ -40,27 +52,28 @@ class TermWrap { focusHandler : (focus : boolean) => void; isRunning : boolean; - constructor(elem : Element, termContext : TermContext, usedRows : number, termOpts : TermOptsType, winSize : WindowSize, keyHandler : (event : any) => void, focusHandler : (focus : boolean) => void, isRunning : boolean) { - this.termContext = termContext; + constructor(elem : Element, opts : TermWrapOpts) { + opts = opts ?? ({} as any); + this.termContext = opts.termContext; this.connectedElem = elem; - this.flexRows = termOpts.flexrows ?? false; - this.winSize = winSize; - this.focusHandler = focusHandler; - this.isRunning = isRunning; + this.flexRows = opts.termOpts.flexrows ?? false; + this.winSize = opts.winSize; + this.focusHandler = opts.focusHandler; + this.isRunning = opts.isRunning; if (this.flexRows) { this.atRowMax = false; - this.usedRows = mobx.observable.box(usedRows ?? (isRunning ? 2 : 0)); + this.usedRows = mobx.observable.box(opts.usedRows ?? (opts.isRunning ? 2 : 0)); } else { this.atRowMax = true; - this.usedRows = mobx.observable.box(termOpts.rows); + this.usedRows = mobx.observable.box(opts.termOpts.rows); } - if (winSize == null) { - this.termSize = {rows: termOpts.rows, cols: termOpts.cols}; + if (opts.winSize == null) { + this.termSize = {rows: opts.termOpts.rows, cols: opts.termOpts.cols}; } else { - let cols = widthToCols(winSize.width); - this.termSize = {rows: termOpts.rows, cols: cols}; + let cols = widthToCols(opts.winSize.width); + this.termSize = {rows: opts.termOpts.rows, cols: cols}; } let theme = { foreground: "#d3d7cf", @@ -71,8 +84,11 @@ class TermWrap { return state; }); this.terminal.open(elem); - if (keyHandler != null) { - this.terminal.onKey(keyHandler); + if (opts.keyHandler != null) { + this.terminal.onKey((e) => opts.keyHandler(e, this)); + } + if (opts.dataHandler != null) { + this.terminal.onData((e) => opts.dataHandler(e, this)); } this.terminal.textarea.addEventListener("focus", () => { if (this.focusHandler != null) { @@ -88,6 +104,9 @@ class TermWrap { } }); elem.addEventListener("scroll", this.elemScrollHandler); + if (opts.customKeyHandler != null) { + this.terminal.attachCustomKeyEventHandler((e) => opts.customKeyHandler(e, this)); + } this.reloadTerminal(0); } @@ -97,7 +116,8 @@ class TermWrap { // xterm.js renders a textarea that handles focus. when it focuses and a space is typed the browser // will scroll to make it visible (even though our terminal element has overflow hidden) // this will undo that scroll. - if (e.target.scrollTop == 0) { + console.log("scroll", this.atRowMax, e.target.scrollTop); + if (this.atRowMax || e.target.scrollTop == 0) { return; } e.target.scrollTop = 0; diff --git a/src/util.ts b/src/util.ts index 1fb92d73..de13fc73 100644 --- a/src/util.ts +++ b/src/util.ts @@ -172,4 +172,8 @@ function boundInt(ival : number, minVal : number, maxVal : number) : number { return ival; } -export {handleJsonFetchResponse, base64ToArray, genMergeData, genMergeSimpleData, parseEnv0, boundInt}; +function isModKeyPress(e : any) { + return e.code.match(/^(Control|Meta|Alt|Shift)(Left|Right)$/); +} + +export {handleJsonFetchResponse, base64ToArray, genMergeData, genMergeSimpleData, parseEnv0, boundInt, isModKeyPress};