2023-03-29 02:02:05 +02:00
|
|
|
import * as mobx from "mobx";
|
2023-03-29 10:05:36 +02:00
|
|
|
import {sprintf} from "sprintf-js";
|
2023-03-29 02:02:05 +02:00
|
|
|
import {boundMethod} from "autobind-decorator";
|
2023-03-30 10:00:04 +02:00
|
|
|
import {handleJsonFetchResponse, isModKeyPress, base64ToArray} from "./util";
|
2023-03-29 07:53:18 +02:00
|
|
|
import * as T from "./types";
|
2023-03-29 10:05:36 +02:00
|
|
|
import {TermWrap} from "./term";
|
|
|
|
import * as lineutil from "./lineutil";
|
|
|
|
import {windowWidthToCols, windowHeightToRows, termWidthFromCols, termHeightFromRows} from "./textmeasure";
|
2023-03-30 02:51:42 +02:00
|
|
|
import {WebShareWSControl} from "./webshare-ws";
|
2023-03-29 02:02:05 +02:00
|
|
|
|
|
|
|
type OV<V> = mobx.IObservableValue<V>;
|
|
|
|
type OArr<V> = mobx.IObservableArray<V>;
|
|
|
|
type OMap<K,V> = mobx.ObservableMap<K,V>;
|
|
|
|
type CV<V> = mobx.IComputedValue<V>;
|
|
|
|
|
|
|
|
function isBlank(s : string) {
|
|
|
|
return (s == null || s == "");
|
|
|
|
}
|
|
|
|
|
|
|
|
function getBaseUrl() {
|
|
|
|
return "https://ot2e112zx5.execute-api.us-west-2.amazonaws.com/dev";
|
|
|
|
}
|
|
|
|
|
2023-03-30 02:51:42 +02:00
|
|
|
function getBaseWSUrl() {
|
|
|
|
return "wss://5lfzlg5crl.execute-api.us-west-2.amazonaws.com/dev";
|
|
|
|
}
|
|
|
|
|
2023-03-29 02:02:05 +02:00
|
|
|
class WebShareModelClass {
|
|
|
|
viewKey : string;
|
|
|
|
screenId : string;
|
|
|
|
errMessage : OV<string> = mobx.observable.box(null, {name: "errMessage"});
|
2023-03-29 07:53:18 +02:00
|
|
|
screen : OV<T.WebFullScreen> = mobx.observable.box(null, {name: "webScreen"});
|
2023-03-29 10:05:36 +02:00
|
|
|
terminals : Record<string, TermWrap> = {}; // lineid => TermWrap
|
|
|
|
renderers : Record<string, T.RendererModel> = {}; // lineid => RendererModel
|
|
|
|
contentHeightCache : Record<string, number> = {}; // lineid => height
|
2023-03-30 02:51:42 +02:00
|
|
|
wsControl : WebShareWSControl;
|
2023-03-31 03:33:37 +02:00
|
|
|
anchor : {anchorLine : number, anchorOffset : number} = {anchorLine: 0, anchorOffset: 0};
|
2023-03-29 02:02:05 +02:00
|
|
|
|
|
|
|
constructor() {
|
|
|
|
let urlParams = new URLSearchParams(window.location.search);
|
|
|
|
this.viewKey = urlParams.get("viewkey");
|
|
|
|
this.screenId = urlParams.get("screenid");
|
|
|
|
setTimeout(() => this.loadFullScreenData(), 10);
|
2023-03-30 02:51:42 +02:00
|
|
|
this.wsControl = new WebShareWSControl(getBaseWSUrl(), this.screenId, this.viewKey, this.wsMessageCallback.bind(this));
|
2023-03-29 02:02:05 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
setErrMessage(msg : string) : void {
|
|
|
|
mobx.action(() => {
|
|
|
|
this.errMessage.set(msg);
|
|
|
|
})();
|
|
|
|
}
|
|
|
|
|
2023-03-29 07:53:18 +02:00
|
|
|
getSelectedLine() : number {
|
2023-03-31 03:14:52 +02:00
|
|
|
let fullScreen = this.screen.get();
|
|
|
|
if (fullScreen != null) {
|
|
|
|
return fullScreen.screen.selectedline;
|
|
|
|
}
|
|
|
|
return 0;
|
2023-03-29 10:05:36 +02:00
|
|
|
}
|
|
|
|
|
2023-03-31 03:33:37 +02:00
|
|
|
setAnchorFields(anchorLine : number, anchorOffset : number, reason : string) : void {
|
|
|
|
this.anchor.anchorLine = anchorLine;
|
|
|
|
this.anchor.anchorOffset = anchorOffset;
|
|
|
|
}
|
|
|
|
|
|
|
|
getAnchor() : {anchorLine : number, anchorOffset : number} {
|
|
|
|
return this.anchor;
|
|
|
|
}
|
|
|
|
|
2023-03-29 10:05:36 +02:00
|
|
|
getTermFontSize() : number {
|
|
|
|
return 12;
|
|
|
|
}
|
|
|
|
|
2023-03-30 22:00:54 +02:00
|
|
|
resizeWindow(winSize : T.WindowSize) : void {
|
|
|
|
let cols = windowWidthToCols(winSize.width, this.getTermFontSize());
|
|
|
|
for (let lineId in this.terminals) {
|
|
|
|
let termWrap = this.terminals[lineId];
|
|
|
|
termWrap.resizeCols(cols);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-30 10:00:04 +02:00
|
|
|
mergeLine(fullScreen : T.WebFullScreen, newLine : T.WebLine) {
|
|
|
|
for (let i=0; i<fullScreen.lines.length; i++) {
|
|
|
|
let line = fullScreen.lines[i];
|
|
|
|
if (line.lineid == newLine.lineid) {
|
|
|
|
fullScreen.lines[i] = newLine;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (line.linenum > newLine.linenum) {
|
|
|
|
fullScreen.lines.splice(i, 0, newLine);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fullScreen.lines.push(newLine);
|
|
|
|
}
|
|
|
|
|
2023-03-30 22:00:54 +02:00
|
|
|
removeLine(fullScreen : T.WebFullScreen, lineId : string) {
|
|
|
|
for (let i=0; i<fullScreen.lines.length; i++) {
|
|
|
|
let line = fullScreen.lines[i];
|
|
|
|
if (line.lineid == lineId) {
|
|
|
|
fullScreen.lines.splice(i, 1);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (let i=0; i<fullScreen.cmds.length; i++) {
|
|
|
|
let cmd = fullScreen.cmds[i];
|
|
|
|
if (cmd.lineid == lineId) {
|
|
|
|
fullScreen.cmds.splice(i, 1);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.unloadRenderer(lineId);
|
|
|
|
}
|
|
|
|
|
|
|
|
setCmdDone(lineId : string) : void {
|
|
|
|
let termWrap = this.getTermWrap(lineId);
|
|
|
|
if (termWrap != null) {
|
|
|
|
termWrap.cmdDone();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-30 10:00:04 +02:00
|
|
|
mergeCmd(fullScreen : T.WebFullScreen, newCmd : T.WebCmd) {
|
|
|
|
for (let i=0; i<fullScreen.cmds.length; i++) {
|
|
|
|
let cmd = fullScreen.cmds[i];
|
|
|
|
if (cmd.lineid == newCmd.lineid) {
|
2023-03-30 22:00:54 +02:00
|
|
|
let wasRunning = lineutil.cmdStatusIsRunning(cmd.status);
|
|
|
|
let isRunning = lineutil.cmdStatusIsRunning(newCmd.status);
|
|
|
|
if (wasRunning && !isRunning) {
|
|
|
|
setTimeout(() => this.setCmdDone(cmd.lineid), 300);
|
|
|
|
}
|
2023-03-30 10:00:04 +02:00
|
|
|
fullScreen.cmds[i] = newCmd;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fullScreen.cmds.push(newCmd);
|
|
|
|
}
|
|
|
|
|
|
|
|
mergeUpdate(msg : T.WebScreenUpdate) {
|
|
|
|
if (msg.screenid != this.screenId) {
|
|
|
|
console.log("bad WebScreenUpdate, wrong screenid", msg.screenid);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
console.log("merge", msg);
|
|
|
|
mobx.action(() => {
|
|
|
|
let fullScreen = this.screen.get();
|
|
|
|
if (msg.screen) {
|
|
|
|
fullScreen.screen = msg.screen;
|
|
|
|
}
|
|
|
|
if (msg.lines != null && msg.lines.length > 0) {
|
|
|
|
for (let line of msg.lines) {
|
|
|
|
this.mergeLine(fullScreen, line);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (msg.cmds != null && msg.cmds.length > 0) {
|
|
|
|
for (let cmd of msg.cmds) {
|
|
|
|
this.mergeCmd(fullScreen, cmd);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (msg.ptydata != null && msg.ptydata.length > 0) {
|
|
|
|
for (let data of msg.ptydata) {
|
|
|
|
let termWrap = this.getTermWrap(data.lineid);
|
|
|
|
if (termWrap == null) {
|
|
|
|
continue;
|
|
|
|
}
|
2023-03-30 22:00:54 +02:00
|
|
|
let dataArr = base64ToArray(data.data);
|
2023-03-31 03:14:52 +02:00
|
|
|
termWrap.receiveData(data.ptypos, dataArr, "ws:ptydata");
|
2023-03-30 22:00:54 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (msg.removedlines != null && msg.removedlines.length > 0) {
|
|
|
|
for (let lineid of msg.removedlines) {
|
|
|
|
this.removeLine(fullScreen, lineid);
|
2023-03-30 10:00:04 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
})();
|
|
|
|
}
|
|
|
|
|
2023-03-30 02:51:42 +02:00
|
|
|
wsMessageCallback(msg : any) {
|
2023-03-30 10:00:04 +02:00
|
|
|
if (msg.type == "webscreen:update") {
|
|
|
|
this.mergeUpdate(msg);
|
|
|
|
return;
|
|
|
|
}
|
2023-03-30 02:51:42 +02:00
|
|
|
console.log("ws message", msg);
|
|
|
|
}
|
|
|
|
|
|
|
|
setWebFullScreen(screen : T.WebFullScreen) {
|
|
|
|
mobx.action(() => {
|
2023-03-30 10:00:04 +02:00
|
|
|
if (screen.lines == null) {
|
|
|
|
screen.lines = [];
|
|
|
|
}
|
|
|
|
if (screen.cmds == null) {
|
|
|
|
screen.cmds = [];
|
|
|
|
}
|
2023-03-30 02:51:42 +02:00
|
|
|
this.screen.set(screen);
|
|
|
|
this.wsControl.reconnect(true);
|
|
|
|
})();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2023-03-29 10:05:36 +02:00
|
|
|
loadTerminalRenderer(elem : Element, line : T.WebLine, cmd : T.WebCmd, width : number) : void {
|
|
|
|
let lineId = cmd.lineid;
|
|
|
|
let termWrap = this.getTermWrap(lineId);
|
|
|
|
if (termWrap != null) {
|
|
|
|
console.log("term-wrap already exists for", lineId);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
let cols = windowWidthToCols(width, this.getTermFontSize());
|
|
|
|
let usedRows = this.getContentHeight(lineutil.getWebRendererContext(line));
|
|
|
|
if (line.contentheight != null && line.contentheight != -1) {
|
|
|
|
usedRows = line.contentheight;
|
|
|
|
}
|
|
|
|
let termContext = lineutil.getWebRendererContext(line);
|
|
|
|
termWrap = new TermWrap(elem, {
|
|
|
|
termContext: termContext,
|
|
|
|
usedRows: usedRows,
|
|
|
|
termOpts: cmd.termopts,
|
|
|
|
winSize: {height: 0, width: width},
|
|
|
|
dataHandler: null,
|
|
|
|
focusHandler: (focus : boolean) => this.setTermFocus(line.linenum, focus),
|
|
|
|
isRunning: lineutil.cmdStatusIsRunning(cmd.status),
|
|
|
|
customKeyHandler: this.termCustomKeyHandler.bind(this),
|
|
|
|
fontSize: this.getTermFontSize(),
|
|
|
|
ptyDataSource: getTermPtyData,
|
|
|
|
onUpdateContentHeight: (termContext : T.RendererContext, height : number) => { this.setContentHeight(termContext, height); },
|
|
|
|
});
|
|
|
|
this.terminals[lineId] = termWrap;
|
2023-03-31 03:14:52 +02:00
|
|
|
if (this.getSelectedLine() == line.linenum) {
|
2023-03-29 10:05:36 +02:00
|
|
|
termWrap.giveFocus();
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
termCustomKeyHandler(e : any, termWrap : TermWrap) : boolean {
|
|
|
|
if (e.type != "keydown" || isModKeyPress(e)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
e.stopPropagation();
|
|
|
|
e.preventDefault();
|
|
|
|
if (e.code == "ArrowUp") {
|
|
|
|
termWrap.terminal.scrollLines(-1);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (e.code == "ArrowDown") {
|
|
|
|
termWrap.terminal.scrollLines(1);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (e.code == "PageUp") {
|
|
|
|
termWrap.terminal.scrollPages(-1);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (e.code == "PageDown") {
|
|
|
|
termWrap.terminal.scrollPages(1);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
setTermFocus(lineNum : number, focus : boolean) : void {
|
|
|
|
}
|
|
|
|
|
|
|
|
getContentHeight(context : T.RendererContext) : number {
|
|
|
|
let key = context.lineId;
|
|
|
|
return this.contentHeightCache[key];
|
|
|
|
}
|
|
|
|
|
|
|
|
setContentHeight(context : T.RendererContext, height : number) : void {
|
|
|
|
let key = context.cmdId;
|
|
|
|
this.contentHeightCache[key] = height;
|
|
|
|
}
|
|
|
|
|
|
|
|
unloadRenderer(lineId : string) : void {
|
|
|
|
let rmodel = this.renderers[lineId];
|
|
|
|
if (rmodel != null) {
|
|
|
|
rmodel.dispose();
|
|
|
|
delete this.renderers[lineId];
|
|
|
|
}
|
|
|
|
let term = this.terminals[lineId];
|
|
|
|
if (term != null) {
|
|
|
|
term.dispose();
|
|
|
|
delete this.terminals[lineId];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
getUsedRows(context : T.RendererContext, line : T.WebLine, cmd : T.WebCmd, width : number) : number {
|
|
|
|
if (cmd == null) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
let termOpts = cmd.termopts;
|
|
|
|
if (!termOpts.flexrows) {
|
|
|
|
return termOpts.rows;
|
|
|
|
}
|
|
|
|
let termWrap = this.getTermWrap(cmd.lineid);
|
|
|
|
if (termWrap == null) {
|
|
|
|
let cols = windowWidthToCols(width, this.getTermFontSize());
|
|
|
|
let usedRows = this.getContentHeight(context);
|
|
|
|
if (usedRows != null) {
|
|
|
|
return usedRows;
|
|
|
|
}
|
|
|
|
if (line.contentheight != null && line.contentheight != -1) {
|
|
|
|
return line.contentheight;
|
|
|
|
}
|
|
|
|
return (lineutil.cmdStatusIsRunning(cmd.status) ? 1 : 0);
|
|
|
|
}
|
|
|
|
return termWrap.getUsedRows();
|
|
|
|
}
|
|
|
|
|
|
|
|
getTermWrap(lineId : string) : TermWrap {
|
|
|
|
return this.terminals[lineId];
|
|
|
|
}
|
|
|
|
|
|
|
|
getRenderer(lineId : string) : T.RendererModel {
|
|
|
|
return this.renderers[lineId];
|
2023-03-29 07:53:18 +02:00
|
|
|
}
|
|
|
|
|
2023-03-29 02:02:05 +02:00
|
|
|
loadFullScreenData() : void {
|
|
|
|
if (isBlank(this.screenId)) {
|
|
|
|
this.setErrMessage("No ScreenId Specified, Cannot Load.");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (isBlank(this.viewKey)) {
|
|
|
|
this.setErrMessage("No ViewKey Specified, Cannot Load.");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
let usp = new URLSearchParams({screenid: this.screenId, viewkey: this.viewKey});
|
|
|
|
let url = new URL(getBaseUrl() + "/webshare/screen?" + usp.toString());
|
|
|
|
fetch(url, {method: "GET", mode: "cors", cache: "no-cache"}).then((resp) => handleJsonFetchResponse(url, resp)).then((data) => {
|
2023-03-29 10:05:36 +02:00
|
|
|
mobx.action(() => {
|
|
|
|
let screen : T.WebFullScreen = data;
|
2023-03-30 02:51:42 +02:00
|
|
|
this.setWebFullScreen(screen);
|
2023-03-29 10:05:36 +02:00
|
|
|
})();
|
2023-03-29 02:02:05 +02:00
|
|
|
}).catch((err) => {
|
|
|
|
this.errMessage.set("Cannot get screen: " + err.message);
|
|
|
|
});
|
2023-03-31 03:14:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
getLineIndex(lineNum : number) : number {
|
|
|
|
let fullScreen = this.screen.get();
|
|
|
|
if (fullScreen == null) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
for (let i=0; i<fullScreen.lines.length; i++) {
|
|
|
|
let line = fullScreen.lines[i];
|
|
|
|
if (line.linenum == lineNum) {
|
|
|
|
return i;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
getNumLines() : number {
|
|
|
|
let fullScreen = this.screen.get();
|
|
|
|
if (fullScreen == null) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
return fullScreen.lines.length;
|
2023-03-29 02:02:05 +02:00
|
|
|
}
|
2023-03-31 03:33:37 +02:00
|
|
|
|
|
|
|
getCmdById(lineId : string) : T.WebCmd {
|
|
|
|
let fullScreen = this.screen.get();
|
|
|
|
if (fullScreen == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
for (let cmd of fullScreen.cmds) {
|
|
|
|
if (cmd.lineid == lineId) {
|
|
|
|
return cmd;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
2023-03-29 02:02:05 +02:00
|
|
|
}
|
|
|
|
|
2023-03-29 10:05:36 +02:00
|
|
|
function getTermPtyData(termContext : T.TermContextUnion) : Promise<T.PtyDataType> {
|
|
|
|
if ("remoteId" in termContext) {
|
|
|
|
throw new Error("remote term ptydata is not supported in webshare");
|
|
|
|
}
|
|
|
|
let ptyOffset = 0;
|
|
|
|
let viewKey = WebShareModel.viewKey;
|
|
|
|
let usp = new URLSearchParams({screenid: termContext.screenId, viewkey: viewKey, lineid: termContext.lineId});
|
|
|
|
let url = new URL(getBaseUrl() + "/webshare/ptydata?" + usp.toString());
|
|
|
|
return fetch(url, {method: "GET", mode: "cors", cache: "no-cache"}).then((resp) => {
|
|
|
|
if (!resp.ok) {
|
|
|
|
throw new Error(sprintf("Bad fetch response for /webshare/ptydata: %d %s", resp.status, resp.statusText));
|
|
|
|
}
|
|
|
|
let ptyOffsetStr = resp.headers.get("X-PtyDataOffset");
|
|
|
|
if (ptyOffsetStr != null && !isNaN(parseInt(ptyOffsetStr))) {
|
|
|
|
ptyOffset = parseInt(ptyOffsetStr);
|
|
|
|
}
|
|
|
|
return resp.arrayBuffer();
|
|
|
|
}).then((buf) => {
|
|
|
|
return {pos: ptyOffset, data: new Uint8Array(buf)};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-03-29 02:02:05 +02:00
|
|
|
let WebShareModel : WebShareModelClass = null;
|
|
|
|
if ((window as any).WebShareModel == null) {
|
|
|
|
WebShareModel = new WebShareModelClass();
|
|
|
|
(window as any).WebShareModel = WebShareModel;
|
|
|
|
}
|
|
|
|
|
|
|
|
export {WebShareModel};
|