{formattedTime}
diff --git a/src/lineutil.ts b/src/lineutil.ts
new file mode 100644
index 00000000..8f25210b
--- /dev/null
+++ b/src/lineutil.ts
@@ -0,0 +1,76 @@
+import dayjs from "dayjs";
+import localizedFormat from 'dayjs/plugin/localizedFormat';
+import {isBlank, getDateStr} from "./util";
+import {LineType, WebLine} from "./types";
+
+dayjs.extend(localizedFormat)
+
+function getRendererType(line : LineType|WebLine) : "terminal" | "plugin" {
+ if (isBlank(line.renderer) || line.renderer == "terminal") {
+ return "terminal";
+ }
+ return "plugin";
+}
+
+function getLineDateStr(todayDate : string, yesterdayDate : string, ts : number) : string {
+ let lineDate = new Date(ts);
+ let dateStr = getDateStr(lineDate);
+ if (dateStr == todayDate) {
+ return "today";
+ }
+ if (dateStr == yesterdayDate) {
+ return "yesterday";
+ }
+ return dateStr;
+}
+
+function getLineDateTimeStr(ts : number) : string {
+ let lineDate = new Date(ts);
+ let nowDate = new Date();
+
+ if (nowDate.getFullYear() != lineDate.getFullYear()) {
+ return dayjs(lineDate).format("ddd L LTS");
+ }
+ else if (nowDate.getMonth() != lineDate.getMonth() || nowDate.getDate() != lineDate.getDate()) {
+ let yesterdayDate = (new Date());
+ yesterdayDate.setDate(yesterdayDate.getDate()-1);
+ if (yesterdayDate.getMonth() == lineDate.getMonth() && yesterdayDate.getDate() == lineDate.getDate()) {
+ return "Yesterday " + dayjs(lineDate).format("LTS");;
+ }
+ return dayjs(lineDate).format("ddd L LTS");
+ }
+ else {
+ return dayjs(lineDate).format("LTS");
+ }
+}
+
+function isMultiLineCmdText(cmdText : string) : boolean {
+ if (cmdText == null) {
+ return false;
+ }
+ cmdText = cmdText.trim();
+ let nlIdx = cmdText.indexOf("\n");
+ return (nlIdx != -1);
+}
+
+function getFullCmdText(cmdText : string) {
+ if (cmdText == null) {
+ return "(none)";
+ }
+ cmdText = cmdText.trim();
+ return cmdText;
+}
+
+function getSingleLineCmdText(cmdText : string) {
+ if (cmdText == null) {
+ return "(none)";
+ }
+ cmdText = cmdText.trim();
+ let nlIdx = cmdText.indexOf("\n");
+ if (nlIdx != -1) {
+ cmdText = cmdText.substr(0, nlIdx);
+ }
+ return cmdText;
+}
+
+export {getRendererType, getLineDateStr, getLineDateTimeStr, isMultiLineCmdText, getFullCmdText, getSingleLineCmdText};
diff --git a/src/main.tsx b/src/main.tsx
index 6c1b692d..dc09315e 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -11,7 +11,8 @@ import dayjs from "dayjs";
import type {SessionDataType, LineType, CmdDataType, RemoteType, RemoteStateType, RemoteInstanceType, RemotePtrType, HistoryItem, HistoryQueryOpts, RemoteEditType, FeStateType, ContextMenuOpts, BookmarkType, RenderModeType, ClientMigrationInfo} from "./types";
import type * as T from "./types";
import localizedFormat from 'dayjs/plugin/localizedFormat';
-import {GlobalModel, GlobalCommandRunner, Session, Cmd, ScreenLines, Screen, riToRPtr, windowWidthToCols, windowHeightToRows, termHeightFromRows, termWidthFromCols, TabColors, RemoteColors} from "./model";
+import {GlobalModel, GlobalCommandRunner, Session, Cmd, ScreenLines, Screen, riToRPtr, TabColors, RemoteColors} from "./model";
+import {windowWidthToCols, windowHeightToRows, termHeightFromRows, termWidthFromCols} from "./textmeasure";
import {isModKeyPress, boundInt} from "./util";
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
diff --git a/src/model.ts b/src/model.ts
index 5a79adb1..e045baab 100644
--- a/src/model.ts
+++ b/src/model.ts
@@ -5,9 +5,9 @@ import {debounce} from "throttle-debounce";
import {handleJsonFetchResponse, base64ToArray, genMergeData, genMergeDataMap, genMergeSimpleData, boundInt, isModKeyPress} from "./util";
import {TermWrap} from "./term";
import {v4 as uuidv4} from "uuid";
-import type {SessionDataType, LineType, RemoteType, HistoryItem, RemoteInstanceType, RemotePtrType, CmdDataType, FeCmdPacketType, TermOptsType, RemoteStateType, ScreenDataType, ScreenOptsType, PtyDataUpdateType, ModelUpdateType, UpdateMessage, InfoType, CmdLineUpdateType, UIContextType, HistoryInfoType, HistoryQueryOpts, FeInputPacketType, TermWinSize, RemoteInputPacketType, FeStateType, ContextMenuOpts, RendererContext, RendererModel, PtyDataType, BookmarkType, ClientDataType, HistoryViewDataType, AlertMessageType, HistorySearchParams, FocusTypeStrs, ScreenLinesType, HistoryTypeStrs, RendererPluginType, WindowSize, ClientMigrationInfo, WebShareOpts} from "./types";
+import type {SessionDataType, LineType, RemoteType, HistoryItem, RemoteInstanceType, RemotePtrType, CmdDataType, FeCmdPacketType, TermOptsType, RemoteStateType, ScreenDataType, ScreenOptsType, PtyDataUpdateType, ModelUpdateType, UpdateMessage, InfoType, CmdLineUpdateType, UIContextType, HistoryInfoType, HistoryQueryOpts, FeInputPacketType, TermWinSize, RemoteInputPacketType, FeStateType, ContextMenuOpts, RendererContext, RendererModel, PtyDataType, BookmarkType, ClientDataType, HistoryViewDataType, AlertMessageType, HistorySearchParams, FocusTypeStrs, ScreenLinesType, HistoryTypeStrs, RendererPluginType, WindowSize, ClientMigrationInfo, WebShareOpts, TermContextUnion} from "./types";
import {WSControl} from "./ws";
-import {measureText, getMonoFontSize} from "./textmeasure";
+import {measureText, getMonoFontSize, windowWidthToCols, windowHeightToRows, termWidthFromCols, termHeightFromRows} from "./textmeasure";
import dayjs from "dayjs";
import localizedFormat from 'dayjs/plugin/localizedFormat';
import customParseFormat from 'dayjs/plugin/customParseFormat';
@@ -18,8 +18,6 @@ dayjs.extend(localizedFormat)
var GlobalUser = "sawka";
const RemotePtyRows = 8; // also in main.tsx
const RemotePtyCols = 80;
-const MinTermCols = 10;
-const MaxTermCols = 1024;
const ProdServerEndpoint = "http://localhost:1619";
const ProdServerWsEndpoint = "ws://localhost:1623";
const DevServerEndpoint = "http://localhost:8090";
@@ -60,13 +58,6 @@ type SWLinePtr = {
screen : Screen,
};
-function getRendererType(line : LineType) : "terminal" | "plugin" {
- if (isBlank(line.renderer) || line.renderer == "terminal") {
- return "terminal";
- }
- return "plugin";
-}
-
function getRendererContext(line : LineType) : RendererContext {
return {
screenId: line.screenid,
@@ -76,32 +67,6 @@ function getRendererContext(line : LineType) : RendererContext {
};
}
-function windowWidthToCols(width : number, fontSize : number) : number {
- let dr = getMonoFontSize(fontSize);
- let cols = Math.trunc((width - 50) / dr.width) - 1;
- cols = boundInt(cols, MinTermCols, MaxTermCols);
- return cols;
-}
-
-function windowHeightToRows(height : number, fontSize : number) : number {
- let dr = getMonoFontSize(fontSize);
- let rows = Math.floor((height - 80) / dr.height) - 1;
- if (rows <= 0) {
- rows = 1;
- }
- return rows;
-}
-
-function termWidthFromCols(cols : number, fontSize : number) : number {
- let dr = getMonoFontSize(fontSize);
- return Math.ceil(dr.width*cols) + 15;
-}
-
-function termHeightFromRows(rows : number, fontSize : number) : number {
- let dr = getMonoFontSize(fontSize);
- return Math.ceil(dr.height*rows);
-}
-
function cmdStatusIsRunning(status : string) : boolean {
return status == "running" || status == "detached";
}
@@ -227,38 +192,6 @@ class Cmd {
return this.data.get().festate;
}
- isMultiLineCmdText() : boolean {
- let cmdText = this.data.get().cmdstr;
- if (cmdText == null) {
- return false;
- }
- cmdText = cmdText.trim();
- let nlIdx = cmdText.indexOf("\n");
- return (nlIdx != -1);
- }
-
- getSingleLineCmdText() {
- let cmdText = this.data.get().cmdstr;
- if (cmdText == null) {
- return "(none)";
- }
- cmdText = cmdText.trim();
- let nlIdx = cmdText.indexOf("\n");
- if (nlIdx != -1) {
- cmdText = cmdText.substr(0, nlIdx);
- }
- return cmdText;
- }
-
- getFullCmdText() {
- let cmdText = this.data.get().cmdstr;
- if (cmdText == null) {
- return "(none)";
- }
- cmdText = cmdText.trim();
- return cmdText;
- }
-
isRunning() : boolean {
let data = this.data.get();
return cmdStatusIsRunning(data.status);
@@ -691,6 +624,8 @@ class Screen {
isRunning: cmd.isRunning(),
customKeyHandler: this.termCustomKeyHandler.bind(this),
fontSize: GlobalModel.termFontSize.get(),
+ ptyDataSource: getTermPtyData,
+ onUpdateContentHeight: (termContext : RendererContext, height : number) => { GlobalModel.setContentHeight(termContext, height); },
});
this.terminals[cmdId] = termWrap;
if ((this.focusType.get() == "cmd" || this.focusType.get() == "cmd-fg") && this.selectedLine.get() == line.linenum) {
@@ -1639,6 +1574,8 @@ class InputModel {
focusHandler: this.setRemoteTermWrapFocus.bind(this),
isRunning: true,
fontSize: GlobalModel.termFontSize.get(),
+ ptyDataSource: getTermPtyData,
+ onUpdateContentHeight: null,
});
}
}
@@ -1757,7 +1694,8 @@ class SpecialHistoryViewLineContainer {
isRunning: cmd.isRunning(),
customKeyHandler: null,
fontSize: GlobalModel.termFontSize.get(),
- noSetTUR: true,
+ ptyDataSource: getTermPtyData,
+ onUpdateContentHeight: null,
});
this.terminal = termWrap;
return;
@@ -2369,7 +2307,6 @@ class Model {
sessionSettingsModal : OV
= mobx.observable.box(null, {name: "sessionSettingsModal"});
clientSettingsModal : OV = mobx.observable.box(false, {name: "clientSettingsModal"});
lineSettingsModal : OV = mobx.observable.box(null, {name: "lineSettingsModal"});
- rendererPlugins : RendererPluginType[] = [];
inputModel : InputModel;
bookmarksModel : BookmarksModel;
@@ -2423,20 +2360,6 @@ class Model {
getApi().reloadWindow();
}
- registerRendererPlugin(plugin : RendererPluginType) {
- if (isBlank(plugin.name)) {
- throw new Error("invalid plugin, no name");
- }
- if (plugin.name == "terminal" || plugin.name == "none") {
- throw new Error(sprintf("invalid plugin, name '%s' is reserved", plugin.name));
- }
- let existingPlugin = this.getRendererPluginByName(plugin.name);
- if (existingPlugin != null) {
- throw new Error(sprintf("plugin with name %s already registered", plugin.name));
- }
- this.rendererPlugins.push(plugin);
- }
-
getHasClientStop() : boolean {
if (this.clientData.get() == null) {
return true;
@@ -2448,16 +2371,6 @@ class Model {
return false;
}
- getRendererPluginByName(name : string) : RendererPluginType {
- for (let i=0; i {
mobx.action(() => {
this.alertMessage.set(alertMessage);
@@ -3494,6 +3407,13 @@ function _getPtyDataFromUrl(url : string) : Promise {
});
}
+function getTermPtyData(termContext : TermContextUnion) : Promise {
+ if ("remoteId" in termContext) {
+ return getRemotePtyData(termContext.remoteId);
+ }
+ return getPtyData(termContext.screenId, termContext.cmdId, termContext.lineNum);
+}
+
function getPtyData(screenId : string, cmdId : string, lineNum : number) : Promise {
let url = sprintf(GlobalModel.getBaseHostPort() + "/api/ptyout?linenum=%d&screenid=%s&cmdid=%s", lineNum, screenId, cmdId);
return _getPtyDataFromUrl(url);
@@ -3514,7 +3434,7 @@ if ((window as any).GlobalModel == null) {
GlobalModel = (window as any).GlobalModel;
GlobalCommandRunner = (window as any).GlobalCommandRunner;
-export {Model, Session, ScreenLines, GlobalModel, GlobalCommandRunner, Cmd, Screen, riToRPtr, windowWidthToCols, windowHeightToRows, termWidthFromCols, termHeightFromRows, getPtyData, getRemotePtyData, TabColors, RemoteColors, getRendererType, getRendererContext};
+export {Model, Session, ScreenLines, GlobalModel, GlobalCommandRunner, Cmd, Screen, riToRPtr, TabColors, RemoteColors, getRendererContext, getTermPtyData};
export type {LineContainerModel};
diff --git a/src/plugins.ts b/src/plugins.ts
new file mode 100644
index 00000000..c32f3ba6
--- /dev/null
+++ b/src/plugins.ts
@@ -0,0 +1,67 @@
+import {RendererPluginType} from "./types";
+import {SimpleImageRenderer} from "./imagerenderer";
+import {SimpleMarkdownRenderer} from "./markdownrenderer";
+import {isBlank} from "./util";
+import {sprintf} from "sprintf-js";
+
+const ImagePlugin : RendererPluginType = {
+ name: "image",
+ rendererType: "simple",
+ heightType: "pixels",
+ dataType: "blob",
+ collapseType: "hide",
+ globalCss: null,
+ mimeTypes: ["image/*"],
+ component: SimpleImageRenderer,
+};
+
+const MarkdownPlugin : RendererPluginType = {
+ name: "markdown",
+ rendererType: "simple",
+ heightType: "pixels",
+ dataType: "blob",
+ collapseType: "hide",
+ globalCss: null,
+ mimeTypes: ["text/markdown"],
+ component: SimpleMarkdownRenderer,
+};
+
+let AllPlugins = [ImagePlugin, MarkdownPlugin];
+
+class PluginModelClass {
+ rendererPlugins : RendererPluginType[] = [];
+
+ registerRendererPlugin(plugin : RendererPluginType) {
+ if (isBlank(plugin.name)) {
+ throw new Error("invalid plugin, no name");
+ }
+ if (plugin.name == "terminal" || plugin.name == "none") {
+ throw new Error(sprintf("invalid plugin, name '%s' is reserved", plugin.name));
+ }
+ let existingPlugin = this.getRendererPluginByName(plugin.name);
+ if (existingPlugin != null) {
+ throw new Error(sprintf("plugin with name %s already registered", plugin.name));
+ }
+ this.rendererPlugins.push(plugin);
+ }
+
+ getRendererPluginByName(name : string) : RendererPluginType {
+ for (let i=0; i = mobx.IObservableValue;
type OArr = mobx.IObservableArray;
@@ -307,7 +308,7 @@ class LineSettingsModal extends React.Component<{line : LineType}, {}> {
renderRendererDropdown() : any {
let {line} = this.props;
let renderer = this.tempRenderer.get() ?? "terminal";
- let plugins = GlobalModel.rendererPlugins;
+ let plugins = PluginModel.rendererPlugins;
let plugin : RendererPluginType = null;
return (
diff --git a/src/sh2.less b/src/sh2.less
index a268ebbb..3bc80609 100644
--- a/src/sh2.less
+++ b/src/sh2.less
@@ -3185,7 +3185,15 @@ body.prompt-webshare #main {
.logo-text {
.mono-font(32px);
- color: @prompt-green;
+ a {
+ color: @prompt-green;
+ }
+ }
+
+ .screen-name {
+ color: white;
+ .mono-font(24px);
+ margin-left: 20px;
}
.download-button {
@@ -3195,7 +3203,7 @@ body.prompt-webshare #main {
.prompt-content {
flex-grow: 1;
- padding: 10px;
+ padding: 10px;
}
.prompt-footer {
@@ -3218,4 +3226,9 @@ body.prompt-webshare #main {
#app {
color: white;
}
+
+ .lines .line-sep {
+ margin-left: 0px;
+ margin-right: 10px;
+ }
}
diff --git a/src/sh2.ts b/src/sh2.ts
index 9c58f4e8..4c942c6e 100644
--- a/src/sh2.ts
+++ b/src/sh2.ts
@@ -6,53 +6,14 @@ import {Terminal} from 'xterm';
import {Main} from "./main";
import {GlobalModel} from "./model";
import {v4 as uuidv4} from "uuid";
-import {RendererPluginType} from "./types";
-import {SimpleImageRenderer} from "./imagerenderer";
-import {SimpleMarkdownRenderer} from "./markdownrenderer";
+import {loadFonts} from "./util";
// @ts-ignore
let VERSION = __PROMPT_VERSION__;
// @ts-ignore
let BUILD = __PROMPT_BUILD__;
-let jbmFontNormal = new FontFace("JetBrains Mono", "url('static/fonts/jetbrains-mono-v13-latin-regular.woff2')", {style: "normal", weight: "400"});
-let jbmFont200 = new FontFace("JetBrains Mono", "url('static/fonts/jetbrains-mono-v13-latin-200.woff2')", {style: "normal", weight: "200"});
-let jbmFont700 = new FontFace("JetBrains Mono", "url('static/fonts/jetbrains-mono-v13-latin-700.woff2')", {style: "normal", weight: "700"});
-let faFont = new FontFace("FontAwesome", "url(static/fonts/fontawesome-webfont-4.7.woff2)", {style: "normal", weight: "normal"});
-let docFonts : any = document.fonts; // work around ts typing issue
-docFonts.add(jbmFontNormal);
-docFonts.add(jbmFont200);
-docFonts.add(jbmFont700);
-docFonts.add(faFont);
-jbmFontNormal.load();
-jbmFont200.load();
-jbmFont700.load();
-faFont.load();
-
-const ImagePlugin : RendererPluginType = {
- name: "image",
- rendererType: "simple",
- heightType: "pixels",
- dataType: "blob",
- collapseType: "hide",
- globalCss: null,
- mimeTypes: ["image/*"],
- component: SimpleImageRenderer,
-};
-
-const MarkdownPlugin : RendererPluginType = {
- name: "markdown",
- rendererType: "simple",
- heightType: "pixels",
- dataType: "blob",
- collapseType: "hide",
- globalCss: null,
- mimeTypes: ["text/markdown"],
- component: SimpleMarkdownRenderer,
-};
-
-GlobalModel.registerRendererPlugin(ImagePlugin);
-GlobalModel.registerRendererPlugin(MarkdownPlugin);
+loadFonts();
document.addEventListener("DOMContentLoaded", () => {
let reactElem = React.createElement(Main, null, null);
diff --git a/src/simplerenderer.tsx b/src/simplerenderer.tsx
index ae4dba9c..1c008db9 100644
--- a/src/simplerenderer.tsx
+++ b/src/simplerenderer.tsx
@@ -5,7 +5,7 @@ import {sprintf} from "sprintf-js";
import {boundMethod} from "autobind-decorator";
import {If, For, When, Otherwise, Choose} from "tsx-control-statements/components";
import type {RendererModelInitializeParams, TermOptsType, RendererContext, RendererOpts, SimpleBlobRendererComponent, RendererModelContainerApi, RendererPluginType, PtyDataType, RendererModel, RendererOptsUpdate, LineType} from "./types";
-import {GlobalModel, LineContainerModel, getPtyData, Cmd} from "./model";
+import {LineContainerModel, getTermPtyData, Cmd} from "./model";
import {PtyDataBuffer} from "./ptydata";
import {debounce, throttle} from "throttle-debounce";
@@ -72,7 +72,7 @@ class SimpleBlobRendererModel {
mobx.action(() => {
this.loading.set(true);
})();
- let rtnp = getPtyData(this.context.screenId, this.context.cmdId, this.context.lineNum);
+ let rtnp = getTermPtyData(this.context);
rtnp.then((ptydata) => {
setTimeout(() => {
this.ptyData = ptydata;
@@ -120,14 +120,14 @@ function apiAdapter(lcm : LineContainerModel, line : LineType, cmd : Cmd) : Rend
}
@mobxReact.observer
-class SimpleBlobRenderer extends React.Component<{lcm : LineContainerModel, line : LineType, cmd : Cmd, plugin : RendererPluginType, onHeightChange : () => void}, {}> {
+class SimpleBlobRenderer extends React.Component<{lcm : LineContainerModel, line : LineType, cmd : Cmd, rendererOpts : RendererOpts, plugin : RendererPluginType, onHeightChange : () => void}, {}> {
model : SimpleBlobRendererModel;
wrapperDivRef : React.RefObject
= React.createRef();
rszObs : ResizeObserver;
constructor(props : any) {
super(props);
- let {lcm, line, cmd} = this.props;
+ let {lcm, line, cmd, rendererOpts} = this.props;
let context = contextFromLine(line);
let savedHeight = lcm.getContentHeight(context);
if (savedHeight == null) {
@@ -142,12 +142,7 @@ class SimpleBlobRenderer extends React.Component<{lcm : LineContainerModel, line
context: context,
isDone: !cmd.isRunning(),
savedHeight: savedHeight,
- opts: {
- maxSize: lcm.getMaxContentSize(),
- idealSize: lcm.getIdealContentSize(),
- termOpts: cmd.getTermOpts(),
- termFontSize: GlobalModel.termFontSize.get(),
- },
+ opts: rendererOpts,
api: apiAdapter(lcm, line, cmd),
};
this.model = new SimpleBlobRendererModel();
diff --git a/src/term.ts b/src/term.ts
index c166a320..a3f3ade5 100644
--- a/src/term.ts
+++ b/src/term.ts
@@ -3,9 +3,9 @@ import {Terminal} from 'xterm';
import {sprintf} from "sprintf-js";
import {boundMethod} from "autobind-decorator";
import {v4 as uuidv4} from "uuid";
-import {GlobalModel, GlobalCommandRunner, termHeightFromRows, windowWidthToCols, windowHeightToRows, getPtyData, getRemotePtyData} from "./model";
+import {termHeightFromRows, windowWidthToCols, windowHeightToRows} from "./textmeasure";
import {boundInt} from "./util";
-import type {TermOptsType, TermWinSize, RendererContext, WindowSize, PtyDataType} from "./types";
+import type {TermContextUnion, TermOptsType, TermWinSize, RendererContext, WindowSize, PtyDataType} from "./types";
type DataUpdate = {
data : Uint8Array,
@@ -15,12 +15,8 @@ type DataUpdate = {
const MinTermCols = 10;
const MaxTermCols = 1024;
-type RemoteTermContext = {remoteId : string};
-
-type TermContext = RendererContext | RemoteTermContext;
-
type TermWrapOpts = {
- termContext : TermContext,
+ termContext : TermContextUnion,
usedRows? : number,
termOpts : TermOptsType,
winSize : WindowSize,
@@ -30,13 +26,14 @@ type TermWrapOpts = {
isRunning : boolean,
customKeyHandler? : (event : any, termWrap : TermWrap) => boolean,
fontSize : number,
- noSetTUR? : boolean,
+ ptyDataSource : (termContext : TermContextUnion) => Promise,
+ onUpdateContentHeight : (termContext : RendererContext, height : number) => void,
};
// cmd-instance
class TermWrap {
terminal : any;
- termContext : TermContext;
+ termContext : TermContextUnion;
atRowMax : boolean;
usedRows : mobx.IObservableValue;
flexRows : boolean;
@@ -51,7 +48,8 @@ class TermWrap {
focusHandler : (focus : boolean) => void;
isRunning : boolean;
fontSize : number;
- noSetTUR : boolean;
+ onUpdateContentHeight : (termContext : RendererContext, height : number) => void;
+ ptyDataSource : (termContext : TermContextUnion) => Promise;
constructor(elem : Element, opts : TermWrapOpts) {
opts = opts ?? ({} as any);
@@ -62,7 +60,8 @@ class TermWrap {
this.focusHandler = opts.focusHandler;
this.isRunning = opts.isRunning;
this.fontSize = opts.fontSize;
- this.noSetTUR = !!opts.noSetTUR;
+ this.ptyDataSource = opts.ptyDataSource;
+ this.onUpdateContentHeight = opts.onUpdateContentHeight;
if (this.flexRows) {
this.atRowMax = false;
this.usedRows = mobx.observable.box(opts.usedRows ?? (opts.isRunning ? 1 : 0), {name: "term-usedrows"});
@@ -221,8 +220,8 @@ class TermWrap {
return;
}
this.usedRows.set(tur);
- if (!this.noSetTUR) {
- GlobalModel.setContentHeight(termContext, tur);
+ if (this.onUpdateContentHeight != null) {
+ this.onUpdateContentHeight(termContext, tur);
}
})();
}
@@ -272,14 +271,7 @@ class TermWrap {
}
this.reloading = true;
this.terminal.reset();
- let rtnp : Promise = null;
- if (this.getContextRemoteId() != null) {
- rtnp = getRemotePtyData(this.getContextRemoteId());
- }
- else {
- let termContext = this.getRendererContext();
- rtnp = getPtyData(termContext.screenId, termContext.cmdId, termContext.lineNum);
- }
+ let rtnp = this.ptyDataSource(this.termContext);
rtnp.then((ptydata) => {
setTimeout(() => {
this._reloadThenHandler(ptydata);
diff --git a/src/textmeasure.ts b/src/textmeasure.ts
index 7e6eb405..c4397d14 100644
--- a/src/textmeasure.ts
+++ b/src/textmeasure.ts
@@ -1,3 +1,8 @@
+import {boundInt} from "./util";
+
+const MinTermCols = 10;
+const MaxTermCols = 1024;
+
let MonoFontSizes : {height : number, width : number}[] = [];
MonoFontSizes[8] = {height: 11, width: 4.797};
@@ -41,4 +46,30 @@ function measureText(text : string, textOpts? : {pre? : boolean, mono? : boolean
return measureDiv.getBoundingClientRect()
}
-export {measureText, getMonoFontSize};
+function windowWidthToCols(width : number, fontSize : number) : number {
+ let dr = getMonoFontSize(fontSize);
+ let cols = Math.trunc((width - 50) / dr.width) - 1;
+ cols = boundInt(cols, MinTermCols, MaxTermCols);
+ return cols;
+}
+
+function windowHeightToRows(height : number, fontSize : number) : number {
+ let dr = getMonoFontSize(fontSize);
+ let rows = Math.floor((height - 80) / dr.height) - 1;
+ if (rows <= 0) {
+ rows = 1;
+ }
+ return rows;
+}
+
+function termWidthFromCols(cols : number, fontSize : number) : number {
+ let dr = getMonoFontSize(fontSize);
+ return Math.ceil(dr.width*cols) + 15;
+}
+
+function termHeightFromRows(rows : number, fontSize : number) : number {
+ let dr = getMonoFontSize(fontSize);
+ return Math.ceil(dr.height*rows);
+}
+
+export {measureText, getMonoFontSize, windowWidthToCols, windowHeightToRows, termWidthFromCols, termHeightFromRows};
diff --git a/src/types.ts b/src/types.ts
index f2712c98..d104fd3f 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -355,6 +355,10 @@ type RendererContext = {
lineNum : number,
};
+type RemoteTermContext = {remoteId : string};
+
+type TermContextUnion = RendererContext | RemoteTermContext;
+
type RendererOpts = {
maxSize : WindowSize,
idealSize : WindowSize,
@@ -482,4 +486,52 @@ type HistorySearchParams = {
type RenderModeType = "normal" | "collapsed";
-export type {SessionDataType, LineType, RemoteType, RemoteStateType, RemoteInstanceType, HistoryItem, CmdRemoteStateType, FeCmdPacketType, TermOptsType, CmdStartPacketType, CmdDataType, ScreenDataType, ScreenOptsType, PtyDataUpdateType, ModelUpdateType, UpdateMessage, InfoType, CmdLineUpdateType, RemotePtrType, UIContextType, HistoryInfoType, HistoryQueryOpts, WatchScreenPacketType, TermWinSize, FeInputPacketType, RemoteInputPacketType, RemoteEditType, FeStateType, ContextMenuOpts, RendererContext, WindowSize, RendererModel, PtyDataType, BookmarkType, ClientDataType, PlaybookType, PlaybookEntryType, HistoryViewDataType, RenderModeType, AlertMessageType, HistorySearchParams, ScreenLinesType, FocusTypeStrs, HistoryTypeStrs, RendererOpts, RendererPluginType, SimpleBlobRendererComponent, RendererModelContainerApi, RendererModelInitializeParams, RendererOptsUpdate, ClientMigrationInfo, WebShareOpts, RemoteStatusTypeStrs};
+type WebScreen = {
+ screenid : string,
+ sharename : string,
+ vts : number,
+};
+
+type WebLine = {
+ screenid : string,
+ lineid : string,
+ ts : number,
+ linenum : number,
+ linetype : string,
+ text : string,
+ contentheight : number,
+ renderer : string,
+ archived : boolean,
+ vts : number,
+};
+
+type WebRemote = {
+ alias : string,
+ canonicalname : string,
+ name : string,
+};
+
+type WebCmd = {
+ screeid : string,
+ lineid : string,
+ remote : WebRemote,
+ cmdstr : string,
+ rawcmdstr : string,
+ festate : FeStateType,
+ termopts : TermOptsType,
+ status : string,
+ startpk : CmdStartPacketType,
+ doneinfo : CmdDoneInfoType,
+ rtnstate : boolean,
+ rtnstatestr : string,
+ vts : number,
+};
+
+type WebFullScreen = {
+ screen : WebScreen,
+ lines : WebLine[],
+ cmds : WebCmd[],
+ vts : number,
+}
+
+export type {SessionDataType, LineType, RemoteType, RemoteStateType, RemoteInstanceType, HistoryItem, CmdRemoteStateType, FeCmdPacketType, TermOptsType, CmdStartPacketType, CmdDataType, ScreenDataType, ScreenOptsType, PtyDataUpdateType, ModelUpdateType, UpdateMessage, InfoType, CmdLineUpdateType, RemotePtrType, UIContextType, HistoryInfoType, HistoryQueryOpts, WatchScreenPacketType, TermWinSize, FeInputPacketType, RemoteInputPacketType, RemoteEditType, FeStateType, ContextMenuOpts, RendererContext, WindowSize, RendererModel, PtyDataType, BookmarkType, ClientDataType, PlaybookType, PlaybookEntryType, HistoryViewDataType, RenderModeType, AlertMessageType, HistorySearchParams, ScreenLinesType, FocusTypeStrs, HistoryTypeStrs, RendererOpts, RendererPluginType, SimpleBlobRendererComponent, RendererModelContainerApi, RendererModelInitializeParams, RendererOptsUpdate, ClientMigrationInfo, WebShareOpts, RemoteStatusTypeStrs, WebFullScreen, WebScreen, WebLine, WebCmd, RemoteTermContext, TermContextUnion};
diff --git a/src/util.ts b/src/util.ts
index 628618ef..b1435e45 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -1,5 +1,13 @@
import * as mobx from "mobx";
import {sprintf} from "sprintf-js";
+import dayjs from "dayjs";
+import localizedFormat from 'dayjs/plugin/localizedFormat';
+
+dayjs.extend(localizedFormat)
+
+function isBlank(s : string) : boolean {
+ return (s == null || s == "");
+}
function handleNotOkResp(resp : any, url : URL) : Promise {
let errMsg = sprintf("Bad status code response from fetch '%s': code=%d %s", url.toString(), resp.status, resp.statusText);
@@ -249,4 +257,46 @@ function incObs(inum : mobx.IObservableValue) {
})();
}
-export {handleJsonFetchResponse, base64ToArray, genMergeData, genMergeDataMap, genMergeSimpleData, parseEnv0, boundInt, isModKeyPress, incObs};
+function loadFonts() {
+ let jbmFontNormal = new FontFace("JetBrains Mono", "url('static/fonts/jetbrains-mono-v13-latin-regular.woff2')", {style: "normal", weight: "400"});
+ let jbmFont200 = new FontFace("JetBrains Mono", "url('static/fonts/jetbrains-mono-v13-latin-200.woff2')", {style: "normal", weight: "200"});
+ let jbmFont700 = new FontFace("JetBrains Mono", "url('static/fonts/jetbrains-mono-v13-latin-700.woff2')", {style: "normal", weight: "700"});
+ let faFont = new FontFace("FontAwesome", "url(static/fonts/fontawesome-webfont-4.7.woff2)", {style: "normal", weight: "normal"});
+ let docFonts : any = document.fonts; // work around ts typing issue
+ docFonts.add(jbmFontNormal);
+ docFonts.add(jbmFont200);
+ docFonts.add(jbmFont700);
+ docFonts.add(faFont);
+ jbmFontNormal.load();
+ jbmFont200.load();
+ jbmFont700.load();
+ faFont.load();
+}
+
+const DOW_STRS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+
+function getTodayStr() : string {
+ return getDateStr(new Date());
+}
+
+function getYesterdayStr() : string {
+ let d = new Date();
+ d.setDate(d.getDate()-1);
+ return getDateStr(d);
+}
+
+function getDateStr(d : Date) : string {
+ let yearStr = String(d.getFullYear());
+ let monthStr = String(d.getMonth()+1);
+ if (monthStr.length == 1) {
+ monthStr = "0" + monthStr;
+ }
+ let dayStr = String(d.getDate());
+ if (dayStr.length == 1) {
+ dayStr = "0" + dayStr;
+ }
+ let dowStr = DOW_STRS[d.getDay()];
+ return dowStr + " " + yearStr + "-" + monthStr + "-" + dayStr;
+}
+
+export {handleJsonFetchResponse, base64ToArray, genMergeData, genMergeDataMap, genMergeSimpleData, parseEnv0, boundInt, isModKeyPress, incObs, isBlank, loadFonts, getTodayStr, getYesterdayStr, getDateStr};
diff --git a/src/webshare-elems.tsx b/src/webshare-elems.tsx
index 8f7018dd..0305125a 100644
--- a/src/webshare-elems.tsx
+++ b/src/webshare-elems.tsx
@@ -6,6 +6,282 @@ import {boundMethod} from "autobind-decorator";
import {If, For, When, Otherwise, Choose} from "tsx-control-statements/components";
import cn from "classnames";
import {WebShareModel} from "./webshare-model";
+import * as T from "./types";
+import {isBlank} from "./util";
+import {PluginModel} from "./plugins";
+import * as lineutil from "./lineutil";
+import * as util from "./util";
+
+// TODO selection
+// TODO remotevars
+
+function makeFullRemoteRef(ownerName : string, remoteRef : string, name : string) : string {
+ if (isBlank(ownerName) && isBlank(name)) {
+ return remoteRef;
+ }
+ if (!isBlank(ownerName) && isBlank(name)) {
+ return ownerName + ":" + remoteRef;
+ }
+ if (isBlank(ownerName) && !isBlank(name)) {
+ return remoteRef + ":" + name;
+ }
+ return ownerName + ":" + remoteRef + ":" + name;
+}
+
+function replaceHomePath(path : string, homeDir : string) : string {
+ if (path == homeDir) {
+ return "~";
+ }
+ if (path.startsWith(homeDir + "/")) {
+ return "~" + path.substr(homeDir.length);
+ }
+ return path;
+}
+
+function getCwdStr(state : FeStateType) : string {
+ if ((state == null || state.cwd == null) && remote != null) {
+ return "~";
+ }
+ let cwd = "?";
+ if (state && state.cwd) {
+ cwd = state.cwd;
+ }
+ // if (remote && remote.remotevars.home) {
+ // cwd = replaceHomePath(cwd, remote.remotevars.cwd)
+ // }
+ return cwd;
+}
+
+function getRemoteStr(remote : WebRemote) : string {
+ if (remote == null) {
+ return "(invalid remote)";
+ }
+ let remoteRef = (!isBlank(remote.alias) ? remote.alias : remote.canonicalname);
+ let fullRef = makeFullRemoteRef(null, remoteRef, remote.name);
+ return fullRef;
+}
+
+@mobxReact.observer
+class Prompt extends React.Component<{remote : T.WebRemote, festate : T.FeStateType}, {}> {
+ render() {
+ let {remote, festate} = this.props;
+ let remoteStr = getRemoteStr(remote);
+ let cwd = getCwdStr(festate);
+ let isRoot = false;
+ // if (remote && remote.remotevars) {
+ // if (remote.remotevars["sudo"] || remote.remotevars["bestuser"] == "root") {
+ // isRoot = true;
+ // }
+ // }
+ let remoteColorClass = (isRoot ? "color-red" : "color-green");
+ if (remote && remote.remoteopts && remote.remoteopts.color) {
+ remoteColorClass = "color-" + remote.remoteopts.color;
+ }
+ let remoteTitle : string = null;
+ if (remote && remote.canonicalname) {
+ remoteTitle = remote.canonicalname;
+ }
+ return (
+ [{remoteStr}] {cwd} {isRoot ? "#" : "$"}
+ );
+ }
+}
+
+@mobxReact.observer
+class LineAvatar extends React.Component<{line : T.WebLine, cmd : T.WebCmd}, {}> {
+ render() {
+ let {line, cmd} = this.props;
+ let lineNumStr = String(line.linenum);
+ let status = (cmd != null ? cmd.status : "done");
+ let rtnstate = (cmd != null ? cmd.rtnstate : false);
+ let isComment = (line.linetype == "text");
+ return (
+
+ {lineNumStr}
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+@mobxReact.observer
+class WebLineCmdView extends React.Component<{line : T.WebLine, cmd : T.WebCmd}, {}> {
+ isCmdExpanded : OV = mobx.observable.box(false, {name: "cmd-expanded"});
+ isOverflow : OV = mobx.observable.box(false, {name: "line-overflow"});
+
+ renderSimple() {
+
+
+
+ }
+
+ renderCmdText(cmd : Cmd, remote : WebRemote) : any {
+ if (cmd == null) {
+ return (
+
+ (cmd not found)
+
+ );
+ }
+ if (this.isCmdExpanded.get()) {
+ return (
+
+
+
+
{getFullCmdText(cmd.cmdstr)}
+
+
+ );
+ }
+ let isMultiLine = lineutil.isMultiLineCmdText(cmd.cmdstr);
+ return (
+
+
+
+
+ {lineutil.getSingleLineCmdText(cmd.cmdstr)}
+
+
+ ...▼
+
+
+ );
+ }
+
+ renderMetaWrap() {
+ let {line, cmd} = this.props;
+ let formattedTime = lineutil.getLineDateTimeStr(line.ts);
+ let termOpts = cmd.termopts;
+ let remote = cmd.remote;
+ let renderer = line.renderer;
+ return (
+
+
+
{formattedTime}
+
+
+ {renderer}
+
+
+ ({termOpts.rows}x{termOpts.cols})
+
+
+ {this.renderCmdText(cmd, remote)}
+
+ );
+ }
+
+ render() {
+ let {line, cmd} = this.props;
+ let model = WebShareModel;
+ let isSelected = mobx.computed(() => (model.getSelectedLine() == line.linenum), {name: "computed-isSelected"}).get();
+ let rendererPlugin : RendererPluginType = null;
+ let isNoneRenderer = (line.renderer == "none");
+ if (!isBlank(line.renderer) && line.renderer != "terminal" && !isNoneRenderer) {
+ rendererPlugin = PluginModel.getRendererPluginByName(line.renderer);
+ }
+ let rendererType = lineutil.getRendererType(line);
+ return (
+
+
+
+
+ {this.renderMetaWrap()}
+
+
+ );
+ }
+}
+
+@mobxReact.observer
+class WebLineTextView extends React.Component<{line : T.WebLine, cmd : T.WebCmd}, {}> {
+ render() {
+ let {line} = this.props;
+ let isSelected = mobx.computed(() => (model.getSelectedLine() == line.linenum), {name: "computed-isSelected"}).get();
+ return (
+
+ );
+ }
+}
+
+@mobxReact.observer
+class WebLineView extends React.Component<{line : T.WebLine, cmd : T.WebCmd}, {}> {
+ render() {
+ let {line} = this.props;
+ if (line.linetype == "text") {
+ return
+ }
+ if (line.linetype == "cmd") {
+ return
+ }
+ return (
+ invalid linetype "{line.linetype}"
+ );
+ }
+}
+
+@mobxReact.observer
+class WebScreenView extends React.Component<{screen : T.WebFullScreen}, {}> {
+ render() {
+ let {screen} = this.props;
+ let lines = screen.lines ?? [];
+ let cmds = screen.cmds ?? [];
+ let cmdMap : Record = {};
+ for (let i=0; i{dateSepStr}
+ lineElements.push(sepElem);
+ }
+ let topBorder = (dateSepStr == null) && (idx != 0);
+ let lineElem = ;
+ lineElements.push(lineElem);
+ }
+ return (
+
+ );
+ }
+}
@mobxReact.observer
class WebShareMain extends React.Component<{}, {}> {
@@ -14,10 +290,17 @@ class WebShareMain extends React.Component<{}, {}> {
}
render() {
+ let screen = WebShareModel.screen.get();
+ let errMessage = WebShareModel.errMessage.get();
return (
-
screenid={WebShareModel.screenId}, viewkey={WebShareModel.viewKey}
-
{WebShareModel.errMessage.get()}
+
+
+
+
+ {WebShareModel.errMessage.get()}
+
{this.renderCopy()}
diff --git a/src/webshare-model.ts b/src/webshare-model.ts
index ce9a2f27..02d3c72d 100644
--- a/src/webshare-model.ts
+++ b/src/webshare-model.ts
@@ -1,6 +1,7 @@
import * as mobx from "mobx";
import {boundMethod} from "autobind-decorator";
import {handleJsonFetchResponse} from "./util";
+import * as T from "./types";
type OV = mobx.IObservableValue;
type OArr = mobx.IObservableArray;
@@ -19,6 +20,7 @@ class WebShareModelClass {
viewKey : string;
screenId : string;
errMessage : OV = mobx.observable.box(null, {name: "errMessage"});
+ screen : OV = mobx.observable.box(null, {name: "webScreen"});
constructor() {
let urlParams = new URLSearchParams(window.location.search);
@@ -34,6 +36,10 @@ class WebShareModelClass {
})();
}
+ getSelectedLine() : number {
+ return 10;
+ }
+
loadFullScreenData() : void {
if (isBlank(this.screenId)) {
this.setErrMessage("No ScreenId Specified, Cannot Load.");
@@ -46,7 +52,7 @@ class WebShareModelClass {
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) => {
- console.log("got data", data);
+ mobx.action(() => this.screen.set(data))();
}).catch((err) => {
this.errMessage.set("Cannot get screen: " + err.message);
});
diff --git a/src/webshare.ts b/src/webshare.ts
index 2cd45ce7..42dbf3e7 100644
--- a/src/webshare.ts
+++ b/src/webshare.ts
@@ -3,12 +3,24 @@ import * as React from "react";
import {createRoot} from 'react-dom/client';
import {sprintf} from "sprintf-js";
import {WebShareMain} from "./webshare-elems";
+import {loadFonts} from "./util";
+import {WebShareModel} from "./webshare-model";
+
+loadFonts();
document.addEventListener("DOMContentLoaded", () => {
let elem = document.getElementById("app");
let root = createRoot(elem);
let reactElem = React.createElement(WebShareMain, null, null);
- root.render(reactElem);
+ let isFontLoaded = document.fonts.check("12px 'JetBrains Mono'");
+ if (isFontLoaded) {
+ root.render(reactElem);
+ }
+ else {
+ document.fonts.ready.then(() => {
+ root.render(reactElem);
+ });
+ }
});
(window as any).mobx = mobx;
diff --git a/webshare/static/fonts/fontawesome-webfont-4.7.woff2 b/webshare/static/fonts/fontawesome-webfont-4.7.woff2
new file mode 100644
index 00000000..4d13fc60
Binary files /dev/null and b/webshare/static/fonts/fontawesome-webfont-4.7.woff2 differ
diff --git a/webshare/static/fonts/jetbrains-mono-v13-latin-200.woff b/webshare/static/fonts/jetbrains-mono-v13-latin-200.woff
new file mode 100644
index 00000000..2b8eeac6
Binary files /dev/null and b/webshare/static/fonts/jetbrains-mono-v13-latin-200.woff differ
diff --git a/webshare/static/fonts/jetbrains-mono-v13-latin-200.woff2 b/webshare/static/fonts/jetbrains-mono-v13-latin-200.woff2
new file mode 100644
index 00000000..d23f9b05
Binary files /dev/null and b/webshare/static/fonts/jetbrains-mono-v13-latin-200.woff2 differ
diff --git a/webshare/static/fonts/jetbrains-mono-v13-latin-700.woff b/webshare/static/fonts/jetbrains-mono-v13-latin-700.woff
new file mode 100644
index 00000000..78af4538
Binary files /dev/null and b/webshare/static/fonts/jetbrains-mono-v13-latin-700.woff differ
diff --git a/webshare/static/fonts/jetbrains-mono-v13-latin-700.woff2 b/webshare/static/fonts/jetbrains-mono-v13-latin-700.woff2
new file mode 100644
index 00000000..5737b146
Binary files /dev/null and b/webshare/static/fonts/jetbrains-mono-v13-latin-700.woff2 differ
diff --git a/webshare/static/fonts/jetbrains-mono-v13-latin-regular.woff b/webshare/static/fonts/jetbrains-mono-v13-latin-regular.woff
new file mode 100644
index 00000000..f80359f7
Binary files /dev/null and b/webshare/static/fonts/jetbrains-mono-v13-latin-regular.woff differ
diff --git a/webshare/static/fonts/jetbrains-mono-v13-latin-regular.woff2 b/webshare/static/fonts/jetbrains-mono-v13-latin-regular.woff2
new file mode 100644
index 00000000..de8b746b
Binary files /dev/null and b/webshare/static/fonts/jetbrains-mono-v13-latin-regular.woff2 differ