diff --git a/src/emain.ts b/src/emain.ts index 7b3e4bb3d..84c934ab6 100644 --- a/src/emain.ts +++ b/src/emain.ts @@ -40,6 +40,10 @@ electron.Menu.setApplicationMenu(menu); let MainWindow = null; +function getMods(input : any) { + return {meta: input.meta, shift: input.shift, ctrl: input.ctrl, alt: input.alt}; +} + function createWindow() { let win = new electron.BrowserWindow({ width: 1800, @@ -53,21 +57,33 @@ function createWindow() { if (input.type != "keyDown") { return; } + let mods = getMods(input); if (input.meta) { console.log("before-input", input.code, input.modifiers); } if (input.code == "KeyT" && input.meta) { - win.webContents.send("cmd-t"); + win.webContents.send("t-cmd", mods); e.preventDefault(); return; } - if (input.code == "BracketRight" && input.meta) { - win.webContents.send("switch-screen", {relative: 1}); - e.preventDefault(); + if (input.code == "KeyI" && input.meta) { + if (!input.alt) { + win.webContents.send("i-cmd", mods); + e.preventDefault(); + } return; } - if (input.code == "BracketLeft" && input.meta) { - win.webContents.send("switch-screen", {relative: -1}); + if (input.code.startsWith("Digit") && input.meta) { + let digitNum = parseInt(input.code.substr(5)); + if (isNaN(digitNum) || digitNum < 1 || digitNum > 9) { + return; + } + e.preventDefault(); + win.webContents.send("digit-cmd", {digit: digitNum}, mods); + } + if ((input.code == "BracketRight" || input.code == "BracketLeft") && input.meta) { + let rel = (input.code == "BracketRight" ? 1 : -1); + win.webContents.send("bracket-cmd", {relative: rel}, mods); e.preventDefault(); return; } diff --git a/src/main.tsx b/src/main.tsx index 14fbe307c..56a3149a8 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -187,7 +187,6 @@ class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType, width
{line.userid}
{formattedTime}
- width={this.props.width}, cellwidth={termWidth}
@@ -417,7 +416,14 @@ class ScreenWindowView extends React.Component<{sw : ScreenWindow}, {}> { getWindow() : Window { let {sw} = this.props; - return GlobalModel.getWindowById(sw.sessionId, sw.windowId); + if (sw == null) { + return null; + } + let win = GlobalModel.getWindowById(sw.sessionId, sw.windowId); + if (win == null) { + win = GlobalModel.loadWindow(sw.sessionId, sw.windowId); + } + return win; } getLinesDOMId() { @@ -475,12 +481,12 @@ class ScreenWindowView extends React.Component<{sw : ScreenWindow}, {}> { return this.renderError("(no screen window)"); } let win = this.getWindow(); - if (win == null) { - return this.renderError("(no window)"); - } - if (!win.linesLoaded.get()) { + if (win == null || !win.loaded.get()) { return this.renderError("(loading)"); } + if (win.loadError.get() != null) { + return this.renderError(sprintf("(%s)", win.loadError.get())); + } if (this.width.get() == 0) { return this.renderError(""); } @@ -562,11 +568,15 @@ class ScreenTabs extends React.Component<{session : Session}, {}> { return null; } let screen : Screen = null; + let index = 0; return (
- +
this.handleSwitchScreen(screen.screenId)}> {screen.name.get()} + +
⌘{index+1}
+
diff --git a/src/model.ts b/src/model.ts index d309ef1ab..95389c1cc 100644 --- a/src/model.ts +++ b/src/model.ts @@ -1,25 +1,35 @@ import * as mobx from "mobx"; import {sprintf} from "sprintf-js"; import {boundMethod} from "autobind-decorator"; -import {handleJsonFetchResponse, base64ToArray} from "./util"; +import {handleJsonFetchResponse, base64ToArray, genMergeData} 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} from "./types"; +import type {SessionDataType, WindowDataType, LineType, RemoteType, HistoryItem, RemoteInstanceType, CmdDataType, FeCmdPacketType, TermOptsType, RemoteStateType, ScreenDataType, ScreenWindowType, ScreenOptsType, LayoutType, PtyDataUpdateType, SessionUpdateType, WindowUpdateType} from "./types"; import {WSControl} from "./ws"; var GlobalUser = "sawka"; type OV = mobx.IObservableValue; type OArr = mobx.IObservableArray; +type OMap = mobx.ObservableMap; function isBlank(s : string) { return (s == null || s == ""); } +type KeyModsType = { + meta? : boolean, + ctrl? : boolean, + alt? : boolean, + shift? : boolean, +}; + type ElectronApi = { getId : () => string, - onCmdT : (callback : () => void) => void, - onSwitchScreen : (callback : (event : any, arg : {relative? : number, absolute? : number}) => void) => void, + onTCmd : (callback : (mods : KeyModsType) => void) => void, + onICmd : (callback : (mods : KeyModsType) => void) => void, + onBracketCmd : (callback : (event : any, arg : {relative : number}, mods : KeyModsType) => void) => void, + onDigitCmd : (callback : (event : any, arg : {digit : number}, mods : KeyModsType) => void) => void, }; function getApi() : ElectronApi { @@ -163,6 +173,7 @@ class Cmd { class Screen { sessionId : string; screenId : string; + screenIdx : OV; opts : OV; name : OV; activeWindowId : OV; @@ -172,6 +183,7 @@ class Screen { this.sessionId = sdata.sessionid; this.screenId = sdata.screenid; this.name = mobx.observable.box(sdata.name); + this.screenIdx = mobx.observable.box(sdata.screenidx); this.opts = mobx.observable.box(sdata.screenopts); this.activeWindowId = mobx.observable.box(ces(sdata.activewindowid)); let swArr : ScreenWindow[] = []; @@ -183,12 +195,10 @@ class Screen { this.windows = mobx.observable.array(swArr, {deep: false}) } - getActiveWindow() : Window { - let session = GlobalModel.getSessionById(this.sessionId); - if (session == null) { - return null; - } - return session.getWindowById(this.activeWindowId.get()); + dispose() { + } + + mergeData(data : ScreenDataType) { } updatePtyData(ptyMsg : PtyDataUpdateType) { @@ -216,34 +226,6 @@ class Screen { } return null; } - - deactivate() { - for (let i=0; i = {}; - let activeWindowId = this.activeWindowId.get(); - if (activeWindowId != null) { - GlobalModel.loadWindow(this.sessionId, activeWindowId, false); - loadedMap[activeWindowId] = true; - } - for (let i=0; i; + curRemote : OV = mobx.observable.box(null); loaded : OV = mobx.observable.box(false); + loadError : OV = mobx.observable.box(null); lines : OArr = mobx.observable.array([], {deep: false}); - linesLoaded : OV = mobx.observable.box(false); history : any[] = []; cmds : Record = {}; remoteInstances : OArr = mobx.observable.array([]); - constructor(wdata : WindowDataType) { - this.sessionId = wdata.sessionid; - this.windowId = wdata.windowid; - this.curRemote = mobx.observable.box(wdata.curremote); + constructor(sessionId : string, windowId : string) { + this.sessionId = sessionId; + this.windowId = windowId; } getNumHistoryItems() : number { @@ -307,15 +287,14 @@ class Window { cmd.updatePtyData(ptyMsg); } - updateWindow(win : WindowDataType, isActive : boolean) { + updateWindow(win : WindowDataType, load : boolean) { mobx.action(() => { if (!isBlank(win.curremote)) { this.curRemote.set(win.curremote); } - if (!isActive) { - return; + if (load) { + this.loaded.set(true); } - this.linesLoaded.set(true); this.lines.replace(win.lines || []); this.history = win.history || []; let cmds = win.cmds || []; @@ -325,15 +304,16 @@ class Window { })(); } - deactivate() { + setWindowLoadError(errStr : string) { mobx.action(() => { - this.linesLoaded.set(false); - this.lines.replace([]); - this.history = []; - this.cmds = {}; + this.loaded.set(true); + this.loadError.set(errStr); })(); } + dispose() { + } + getCmd(cmdId : string) { return this.cmds[cmdId]; } @@ -369,7 +349,7 @@ class Window { } addLineCmd(line : LineType, cmd : CmdDataType, interactive : boolean) { - if (!this.linesLoaded.get()) { + if (!this.loaded.get()) { return; } mobx.action(() => { @@ -401,21 +381,15 @@ class Session { sessionId : string; name : OV; activeScreenId : OV; + sessionIdx : OV; screens : OArr; - windows : OArr; notifyNum : OV = mobx.observable.box(0); remoteInstances : OArr = mobx.observable.array([]); constructor(sdata : SessionDataType) { this.sessionId = sdata.sessionid; this.name = mobx.observable.box(sdata.name); - let winData = sdata.windows || []; - let wins : Window[] = []; - for (let i=0; i { - for (let i=0; i { + if (!isBlank(sdata.name)) { + this.name.set(sdata.name); } - } - return null; + if (sdata.sessionidx > 0) { + this.sessionIdx.set(sdata.sessionidx); + } + if (sdata.notifynum >= 0) { + this.notifyNum.set(sdata.notifynum); + } + genMergeData(this.screens, sdata.screens, (s : Screen) => s.screenId, (s : ScreenDataType) => s.screenid, (data : ScreenDataType) => new Screen(data), (s : Screen) => s.screenIdx.get()); + if (!isBlank(sdata.activescreenid)) { + let screen = this.getScreenById(sdata.activescreenid); + if (screen == null) { + console.log(sprintf("got session update, activescreenid=%s, screen not found", sdata.activescreenid)); + } + else { + this.activeScreenId.set(sdata.activescreenid); + } + } + })(); } getActiveScreen() : Screen { @@ -492,13 +465,6 @@ class Session { } return null; } - - addLineCmd(line : LineType, cmd : CmdDataType, interactive : boolean) { - let win = this.getWindowById(line.windowid); - if (win != null) { - win.addLineCmd(line, cmd, interactive); - } - } } class Model { @@ -509,6 +475,7 @@ class Model { ws : WSControl; remotes : OArr = mobx.observable.array([], {deep: false}); remotesLoaded : OV = mobx.observable.box(false); + windows : OMap = mobx.observable.map({}, {deep: false}); constructor() { this.clientId = getApi().getId(); @@ -516,16 +483,29 @@ class Model { this.loadSessionList(); this.ws = new WSControl(this.clientId, this.onWSMessage.bind(this)) this.ws.reconnect(); - getApi().onCmdT(this.onCmdT.bind(this)); - getApi().onSwitchScreen(this.onSwitchScreen.bind(this)); + getApi().onTCmd(this.onTCmd.bind(this)); + getApi().onICmd(this.onICmd.bind(this)); + getApi().onBracketCmd(this.onBracketCmd.bind(this)); + getApi().onDigitCmd(this.onDigitCmd.bind(this)); } - onCmdT() { - console.log("got cmd-t"); + onTCmd(mods : KeyModsType) { + console.log("got cmd-t", mods); } - onSwitchScreen(e : any, arg : {relative? : number, absolute? : number}) { - console.log("switch screen", arg); + onICmd(mods : KeyModsType) { + let elem = document.getElementById("main-cmd-input"); + if (elem != null) { + elem.focus(); + } + } + + onBracketCmd(e : any, arg : {relative: number}, mods : KeyModsType) { + console.log("switch screen (bracket)", arg, mods); + } + + onDigitCmd(e : any, arg : {digit: number}, mods : KeyModsType) { + console.log("switch screen (digit)", arg, mods); } isConnected() : boolean { @@ -542,9 +522,26 @@ class Model { activeScreen.updatePtyData(ptyMsg); return; } + if ("sessions" in message) { + let sessionUpdateMsg : SessionUpdateType = message; + console.log("update-sessions", sessionUpdateMsg.sessions); + mobx.action(() => { + let oldActiveScreen = this.getActiveScreen(); + genMergeData(this.sessionList, sessionUpdateMsg.sessions, (s : Session) => s.sessionId, (sdata : SessionDataType) => sdata.sessionid, (sdata : SessionDataType) => new Session(sdata), (s : Session) => s.sessionIdx.get()); + let newActiveScreen = this.getActiveScreen(); + if (oldActiveScreen != newActiveScreen) { + this.activateScreen(newActiveScreen.sessionId, newActiveScreen.screenId, oldActiveScreen); + } + })(); + + } console.log("ws-message", message); } + removeSession(sessionId : string) { + console.log("removeSession not implemented"); + } + getActiveSession() : Session { return this.getSessionById(this.activeSessionId.get()); } @@ -561,12 +558,39 @@ class Model { return null; } + deactivateWindows() { + mobx.action(() => { + this.windows.clear(); + })(); + } + getWindowById(sessionId : string, windowId : string) : Window { - let session = this.getSessionById(sessionId); - if (session == null) { - return null; - } - return session.getWindowById(windowId); + return this.windows.get(sessionId + "/" + windowId); + } + + updateWindow(win : WindowDataType, load : boolean) { + mobx.action(() => { + let winKey = win.sessionid + "/" + win.windowid; + if (win.remove) { + this.windows.delete(winKey); + return; + } + let existingWin = this.windows.get(winKey); + if (existingWin == null) { + if (!load) { + console.log("cannot update window that does not exist"); + return; + } + let newWindow = new Window(win.sessionid, win.windowid); + this.windows.set(winKey, newWindow); + newWindow.updateWindow(win, load); + return; + } + else { + existingWin.updateWindow(win, load); + existingWin.loaded.set(true); + } + })(); } getScreenById(sessionId : string, screenId : string) : Screen { @@ -582,7 +606,8 @@ class Model { if (screen == null) { return null; } - return screen.getActiveWindow(); + let activeWindowId = screen.activeWindowId.get(); + return this.windows.get(screen.sessionId + "/" + activeWindowId); } getActiveScreen() : Screen { @@ -594,10 +619,11 @@ class Model { } addLineCmd(line : LineType, cmd : CmdDataType, interactive : boolean) { - let session = this.getSessionById(line.sessionid); - if (session != null) { - session.addLineCmd(line, cmd, interactive); + let win = this.getWindowById(line.sessionid, line.windowid); + if (win == null) { + return; } + win.addLineCmd(line, cmd, interactive); } submitCommand(cmdStr : string) { @@ -607,7 +633,20 @@ class Model { this.errorHandler("cannot submit command, no active window", null) return; } - let data : FeCmdPacketType = {type: "fecmd", sessionid: win.sessionId, windowid: win.windowId, cmdstr: cmdStr, userid: GlobalUser, remotestate: null}; + let screen = this.getActiveScreen(); + if (screen == null) { + this.errorHandler("cannot submit command, no active screen", null) + return; + } + let data : FeCmdPacketType = { + type: "fecmd", + sessionid: win.sessionId, + screenid: screen.screenId, + windowid: win.windowId, + cmdstr: cmdStr, + userid: GlobalUser, + remotestate: null + }; let rstate = win.getCurRemoteInstance(); if (rstate == null) { this.errorHandler("cannot submit command, no remote state found", null); @@ -626,15 +665,6 @@ class Model { }); } - updateWindow(win : WindowDataType) { - let session = this.getSessionById(win.sessionid); - if (session == null) { - return; - } - let isActive = (win.sessionid == this.activeSessionId.get()); - session.updateWindow(win, isActive); - } - loadSessionList() { let url = new URL("http://localhost:8080/api/get-all-sessions"); fetch(url).then((resp) => handleJsonFetchResponse(url, resp)).then((data) => { @@ -663,15 +693,15 @@ class Model { }); } - activateScreen(sessionId : string, screenId : string) { - let oldActiveScreen = this.getActiveScreen(); + activateScreen(sessionId : string, screenId : string, oldActiveScreen? : Screen) { + if (!oldActiveScreen) { + oldActiveScreen = this.getActiveScreen(); + } if (oldActiveScreen && oldActiveScreen.sessionId == sessionId && oldActiveScreen.screenId == screenId) { return; } mobx.action(() => { - if (oldActiveScreen != null) { - oldActiveScreen.deactivate(); - } + this.deactivateWindows(); this.activeSessionId.set(sessionId); this.getActiveSession().activeScreenId.set(screenId); })(); @@ -680,7 +710,6 @@ class Model { return; } this.ws.pushMessage({type: "watchscreen", sessionid: curScreen.sessionId, screenid: curScreen.screenId}); - curScreen.loadWindows(false); } createNewScreen(session : Session, name : string, activate : boolean) { @@ -700,7 +729,9 @@ class Model { }); } - loadWindow(sessionId : string, windowId : string, force : boolean) { + loadWindow(sessionId : string, windowId : string) : Window { + let newWin = new Window(sessionId, windowId); + this.windows.set(sessionId + "/" + windowId, newWin); let usp = new URLSearchParams({sessionid: sessionId, windowid: windowId}); let url = new URL(sprintf("http://localhost:8080/api/get-window?") + usp.toString()); fetch(url).then((resp) => handleJsonFetchResponse(url, resp)).then((data) => { @@ -708,11 +739,12 @@ class Model { console.log("null window returned from get-window"); return; } - this.updateWindow(data.data); + this.updateWindow(data.data, true); return; }).catch((err) => { this.errorHandler(sprintf("getting window=%s", windowId), err); }); + return newWin; } loadRemotes() { @@ -750,7 +782,7 @@ class Model { if (session == null) { return null; } - let window = session.getWindowById(line.windowid); + let window = this.getWindowById(line.sessionid, line.windowid); if (window == null) { return null; } diff --git a/src/preload.js b/src/preload.js index b8efa84b6..cabd11790 100644 --- a/src/preload.js +++ b/src/preload.js @@ -2,6 +2,8 @@ let {contextBridge, ipcRenderer} = require("electron"); contextBridge.exposeInMainWorld("api", { getId: () => ipcRenderer.sendSync("get-id"), - onCmdT: (callback) => ipcRenderer.on("cmd-t", callback), - onSwitchScreen: (callback) => ipcRenderer.on("switch-screen", callback), + onTCmd: (callback) => ipcRenderer.on("t-cmd", callback), + onICmd: (callback) => ipcRenderer.on("i-cmd", callback), + onBracketCmd: (callback) => ipcRenderer.on("bracket-cmd", callback), + onDigitCmd: (callback) => ipcRenderer.on("digit-cmd", callback), }); diff --git a/src/sh2.less b/src/sh2.less index 7fb767af5..51e5c5706 100644 --- a/src/sh2.less +++ b/src/sh2.less @@ -33,7 +33,7 @@ html, body, #main { min-width: 80px; width: 150px; flex-shrink: 1; - background-color: darken(#4e9a06, 15%); + background-color: darken(#4e9a06, 10%); display: flex; justify-content: center; align-items: center; @@ -43,16 +43,28 @@ html, body, #main { padding-left: 10px; padding-right: 10px; cursor: pointer; + position: relative; &:hover { background-color: #4e9a06; } &.is-active { - background-color: #4e9a06; + background-color: lighten(#4e9a06, 10%); } border-right: 1px solid #ccc; + + .tab-index { + position: absolute; + right: 5px; + font-weight: normal; + display: none; + } + } + + &:hover .screen-tab .tab-index { + display: block; } .screen-tab.new-screen { diff --git a/src/types.ts b/src/types.ts index 567c28c72..381885ef2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,12 +3,14 @@ import * as mobx from "mobx"; type SessionDataType = { sessionid : string, name : string, + notifynum : number, activescreenid : string, - windows : WindowDataType[], + sessionidx : number, screens : ScreenDataType[], - screenwindows : ScreenWindowType[], - cmds : CmdDataType[], - remove : boolean, + + // for updates + remove? : boolean, + full? : boolean, }; type LineType = { @@ -34,6 +36,10 @@ type ScreenDataType = { name : string, windows : ScreenWindowType[], screenopts : ScreenOptsType, + + // for updates + remove? : boolean, + full? : boolean, }; type LayoutType = { @@ -55,6 +61,9 @@ type ScreenWindowType = { windowid : string, name : string, layout : LayoutType, + + // for updates + remove? : boolean, }; type RemoteType = { @@ -88,7 +97,9 @@ type WindowDataType = { history : HistoryItem[], cmds : CmdDataType[], remotes : RemoteInstanceType[], - remove : boolean, + + // for updates + remove? : boolean, }; type HistoryItem = { @@ -104,6 +115,7 @@ type CmdRemoteStateType = { type FeCmdPacketType = { type : string, sessionid : string, + screenid : string, windowid : string, userid : string, cmdstr : string, @@ -161,4 +173,13 @@ type PtyDataUpdateType = { ptydatalen : number, }; -export type {SessionDataType, LineType, RemoteType, RemoteStateType, RemoteInstanceType, WindowDataType, HistoryItem, CmdRemoteStateType, FeCmdPacketType, TermOptsType, CmdStartPacketType, CmdDonePacketType, CmdDataType, ScreenDataType, ScreenOptsType, ScreenWindowType, LayoutType, PtyDataUpdateType}; +type SessionUpdateType = { + sessions: SessionDataType[], +}; + +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}; diff --git a/src/util.ts b/src/util.ts index c43f0b817..bfb7306b8 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,3 +1,4 @@ +import * as mobx from "mobx"; import {sprintf} from "sprintf-js"; function fetchJsonData(resp : any, ctErr : boolean) : Promise { @@ -42,4 +43,62 @@ function base64ToArray(b64 : string) : Uint8Array { return rtnArr; } -export {handleJsonFetchResponse, base64ToArray}; +interface IDataType { + remove? : boolean; + full? : boolean; +} + +interface IObjType { + dispose : () => void; + mergeData : (data : DataType) => void, +} + +function genMergeData, DataType extends IDataType>( + objs : mobx.IObservableArray, + dataArr : DataType[], + objIdFn : (obj : ObjType) => string, + dataIdFn : (data : DataType) => string, + ctorFn : (data : DataType) => ObjType, + sortIdxFn : (obj : ObjType) => number, +) { + if (dataArr == null || dataArr.length == 0) { + return; + } + let objMap : Record = {}; + for (let i=0; i { + return sortIdxFn(a) - sortIdxFn(b); + }); + } + objs.replace(newObjs); +} + +export {handleJsonFetchResponse, base64ToArray, genMergeData};