prettier all the files

This commit is contained in:
sawka 2023-08-21 21:37:04 -07:00
parent 8b473607c7
commit c8cae274bc
52 changed files with 8635 additions and 8090 deletions

View File

@ -1,9 +1,9 @@
var AllowedFirstParts = {
"package.json": true,
"dist": true,
"static": true,
"node_modules": true,
"bin": true,
dist: true,
static: true,
node_modules: true,
bin: true,
};
var AllowedNodeModules = {
@ -39,7 +39,6 @@ function ignoreFn(path) {
if (!AllowedNodeModules[nodeModule]) {
return true;
}
}
return false;
}
@ -75,19 +74,19 @@ module.exports = {
rebuildConfig: {},
makers: [
{
name: '@electron-forge/maker-squirrel',
name: "@electron-forge/maker-squirrel",
config: {},
},
{
name: '@electron-forge/maker-zip',
platforms: ['darwin'],
name: "@electron-forge/maker-zip",
platforms: ["darwin"],
},
{
name: '@electron-forge/maker-deb',
name: "@electron-forge/maker-deb",
config: {},
},
{
name: '@electron-forge/maker-rpm',
name: "@electron-forge/maker-rpm",
config: {},
},
],

View File

@ -1,48 +1,48 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import {sprintf} from "sprintf-js";
import {boundMethod} from "autobind-decorator";
import {v4 as uuidv4} from "uuid";
import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import { v4 as uuidv4 } from "uuid";
import dayjs from "dayjs";
import {If, For, When, Otherwise, Choose} from "tsx-control-statements/components";
import { If, For, When, Otherwise, Choose } from "tsx-control-statements/components";
import cn from "classnames";
import type {BookmarkType} from "./types";
import {GlobalModel, GlobalCommandRunner} from "./model";
import {CmdStrCode, Markdown} from "./elements";
import type { BookmarkType } from "./types";
import { GlobalModel, GlobalCommandRunner } from "./model";
import { CmdStrCode, Markdown } from "./elements";
@mobxReact.observer
class Bookmark extends React.Component<{bookmark : BookmarkType}, {}> {
class Bookmark extends React.Component<{ bookmark: BookmarkType }, {}> {
@boundMethod
handleDeleteClick() : void {
let {bookmark} = this.props;
handleDeleteClick(): void {
let { bookmark } = this.props;
let model = GlobalModel.bookmarksModel;
model.handleDeleteBookmark(bookmark.bookmarkid);
}
@boundMethod
handleEditClick() : void {
let {bookmark} = this.props;
handleEditClick(): void {
let { bookmark } = this.props;
let model = GlobalModel.bookmarksModel;
model.handleEditBookmark(bookmark.bookmarkid);
}
@boundMethod
handleEditCancel() : void {
handleEditCancel(): void {
let model = GlobalModel.bookmarksModel;
model.cancelEdit();
return;
}
@boundMethod
handleEditUpdate() : void {
handleEditUpdate(): void {
let model = GlobalModel.bookmarksModel;
model.confirmEdit();
return;
}
@boundMethod
handleDescChange(e : any) : void {
handleDescChange(e: any): void {
let model = GlobalModel.bookmarksModel;
mobx.action(() => {
model.tempDesc.set(e.target.value);
@ -50,7 +50,7 @@ class Bookmark extends React.Component<{bookmark : BookmarkType}, {}> {
}
@boundMethod
handleCmdChange(e : any) : void {
handleCmdChange(e: any): void {
let model = GlobalModel.bookmarksModel;
mobx.action(() => {
model.tempCmd.set(e.target.value);
@ -58,57 +58,76 @@ class Bookmark extends React.Component<{bookmark : BookmarkType}, {}> {
}
@boundMethod
handleClick() : void {
let {bookmark} = this.props;
handleClick(): void {
let { bookmark } = this.props;
let model = GlobalModel.bookmarksModel;
model.selectBookmark(bookmark.bookmarkid);
}
@boundMethod
handleUse() : void {
let {bookmark} = this.props;
handleUse(): void {
let { bookmark } = this.props;
let model = GlobalModel.bookmarksModel;
model.useBookmark(bookmark.bookmarkid);
}
@boundMethod
clickCopy() : void {
clickCopy(): void {
let bm = this.props.bookmark;
let model = GlobalModel.bookmarksModel;
model.handleCopyBookmark(bm.bookmarkid);
}
render() {
let bm = this.props.bookmark;
let model = GlobalModel.bookmarksModel;
let isSelected = (model.activeBookmark.get() == bm.bookmarkid);
let isSelected = model.activeBookmark.get() == bm.bookmarkid;
let markdown = bm.description ?? "";
let hasDesc = markdown != "";
let isEditing = (model.editingBookmark.get() == bm.bookmarkid);
let isCopied = mobx.computed(() => (model.copiedIndicator.get() == bm.bookmarkid)).get();
let isEditing = model.editingBookmark.get() == bm.bookmarkid;
let isCopied = mobx.computed(() => model.copiedIndicator.get() == bm.bookmarkid).get();
if (isEditing) {
return (
<div data-bookmarkid={bm.bookmarkid} className={cn("bookmark focus-parent is-editing", {"pending-delete": model.pendingDelete.get() == bm.bookmarkid})}>
<div className={cn("focus-indicator", {"active": isSelected})}/>
<div
data-bookmarkid={bm.bookmarkid}
className={cn("bookmark focus-parent is-editing", {
"pending-delete": model.pendingDelete.get() == bm.bookmarkid,
})}
>
<div className={cn("focus-indicator", { active: isSelected })} />
<div className="bookmark-edit">
<div className="field">
<label className="label">Description (markdown)</label>
<div className="control">
<textarea className="textarea mono-font" rows={6} value={model.tempDesc.get()} onChange={this.handleDescChange}/>
<textarea
className="textarea mono-font"
rows={6}
value={model.tempDesc.get()}
onChange={this.handleDescChange}
/>
</div>
</div>
<div className="field">
<label className="label">Command</label>
<div className="control">
<textarea className="textarea mono-font" rows={3} value={model.tempCmd.get()} onChange={this.handleCmdChange}/>
<textarea
className="textarea mono-font"
rows={3}
value={model.tempCmd.get()}
onChange={this.handleCmdChange}
/>
</div>
</div>
<div className="field is-grouped">
<div className="control">
<button className="button is-link" onClick={this.handleEditUpdate}>Update</button>
<button className="button is-link" onClick={this.handleEditUpdate}>
Update
</button>
</div>
<div className="control">
<button className="button" onClick={this.handleEditCancel}>Cancel</button>
<button className="button" onClick={this.handleEditCancel}>
Cancel
</button>
</div>
</div>
</div>
@ -116,18 +135,34 @@ class Bookmark extends React.Component<{bookmark : BookmarkType}, {}> {
);
}
return (
<div className={cn("bookmark focus-parent", {"pending-delete": model.pendingDelete.get() == bm.bookmarkid})} onClick={this.handleClick}>
<div className={cn("focus-indicator", {"active": isSelected})}/>
<div
className={cn("bookmark focus-parent", {
"pending-delete": model.pendingDelete.get() == bm.bookmarkid,
})}
onClick={this.handleClick}
>
<div className={cn("focus-indicator", { active: isSelected })} />
<div className="bookmark-id-div">{bm.bookmarkid.substr(0, 8)}</div>
<div className="bookmark-content">
<If condition={hasDesc}>
<Markdown text={markdown}/>
<Markdown text={markdown} />
</If>
<CmdStrCode cmdstr={bm.cmdstr} onUse={this.handleUse} onCopy={this.clickCopy} isCopied={isCopied} fontSize="large" limitHeight={false}/>
<CmdStrCode
cmdstr={bm.cmdstr}
onUse={this.handleUse}
onCopy={this.clickCopy}
isCopied={isCopied}
fontSize="large"
limitHeight={false}
/>
</div>
<div className="bookmark-controls">
<div className="bookmark-control" onClick={this.handleEditClick}><i className="fa-sharp fa-solid fa-pen"/></div>
<div className="bookmark-control" onClick={this.handleDeleteClick}><i className="fa-sharp fa-solid fa-trash"/></div>
<div className="bookmark-control" onClick={this.handleEditClick}>
<i className="fa-sharp fa-solid fa-pen" />
</div>
<div className="bookmark-control" onClick={this.handleDeleteClick}>
<i className="fa-sharp fa-solid fa-trash" />
</div>
</div>
</div>
);
@ -137,44 +172,53 @@ class Bookmark extends React.Component<{bookmark : BookmarkType}, {}> {
@mobxReact.observer
class BookmarksView extends React.Component<{}, {}> {
@boundMethod
closeView() : void {
closeView(): void {
GlobalModel.bookmarksModel.closeView();
}
render() {
let isHidden = (GlobalModel.activeMainView.get() != "bookmarks");
let isHidden = GlobalModel.activeMainView.get() != "bookmarks";
if (isHidden) {
return null;
}
let bookmarks = GlobalModel.bookmarksModel.bookmarks;
let idx : number = 0;
let bookmark : BookmarkType = null;
let idx: number = 0;
let bookmark: BookmarkType = null;
return (
<div className={cn("bookmarks-view", "alt-view", {"is-hidden": isHidden})}>
<div className="close-button" onClick={this.closeView}><i className="fa-sharp fa-solid fa-xmark"></i></div>
<div className={cn("bookmarks-view", "alt-view", { "is-hidden": isHidden })}>
<div className="close-button" onClick={this.closeView}>
<i className="fa-sharp fa-solid fa-xmark"></i>
</div>
<div className="alt-title">
<i className="fa-sharp fa-solid fa-bookmark" style={{marginRight: 10}}/>
<i className="fa-sharp fa-solid fa-bookmark" style={{ marginRight: 10 }} />
BOOKMARKS
</div>
<div className="bookmarks-list">
<For index="idx" each="bookmark" of={bookmarks}>
<Bookmark key={bookmark.bookmarkid} bookmark={bookmark}/>
<Bookmark key={bookmark.bookmarkid} bookmark={bookmark} />
</For>
<If condition={bookmarks.length == 0}>
<div className="no-content">
No Bookmarks.<br/>
Use the <i className="fa-sharp fa-solid fa-bookmark"/> icon on commands to add your first bookmark.
No Bookmarks.
<br />
Use the <i className="fa-sharp fa-solid fa-bookmark" /> icon on commands to add your first
bookmark.
</div>
</If>
</div>
<If condition={bookmarks.length > 0}>
<div className="alt-help">
<div className="help-entry">
[Enter] to Use Bookmark<br/>
[Backspace/Delete]x2 or <i className="fa-sharp fa-solid fa-trash"/> to Delete<br/>
[Arrow Up]/[Arrow Down]/[PageUp]/[PageDown] to Move in List<br/>
[e] or <i className="fa-sharp fa-solid fa-pen"/> to Edit<br/>
[c] or <i className="fa-sharp fa-regular fa-copy"/> to Copy<br/>
[Enter] to Use Bookmark
<br />
[Backspace/Delete]x2 or <i className="fa-sharp fa-solid fa-trash" /> to Delete
<br />
[Arrow Up]/[Arrow Down]/[PageUp]/[PageDown] to Move in List
<br />
[e] or <i className="fa-sharp fa-solid fa-pen" /> to Edit
<br />
[c] or <i className="fa-sharp fa-regular fa-copy" /> to Copy
<br />
</div>
</div>
</If>
@ -183,4 +227,4 @@ class BookmarksView extends React.Component<{}, {}> {
}
}
export {BookmarksView};
export { BookmarksView };

View File

@ -1,4 +1,5 @@
.cmd-input-info, .cmd-history {
.cmd-input-info,
.cmd-history {
&::-webkit-scrollbar {
background-color: #777;
width: 5px;
@ -15,7 +16,7 @@
font-weight: normal;
font-size: 12px;
color: @term-white;
.message-content {
position: absolute;
display: none;
@ -41,7 +42,7 @@
overflow: hidden;
}
}
&:hover {
.message-content {
display: flex;
@ -95,7 +96,7 @@
}
.remote-name {
.mono-font(14px)
.mono-font(14px);
}
}
@ -121,7 +122,7 @@
.cmd-input-field {
position: relative;
.cmd-input-control {
line-height: 0;
}
@ -141,7 +142,8 @@
resize: none;
overflow-wrap: anywhere;
&:active, &:focus {
&:active,
&:focus {
border-color: white !important;
}
@ -259,7 +261,7 @@
flex-shrink: 1;
overflow-y: auto;
margin-bottom: 5px;
.info-msg {
.mono-font(14px, 400);
color: @soft-blue;
@ -289,7 +291,7 @@
flex-wrap: wrap;
padding-bottom: 5px;
.mono-font(14px, 400);
.info-comp {
min-width: 200px;
color: @term-white;
@ -317,8 +319,9 @@
color: white;
font-weight: bold;
}
th, td {
th,
td {
padding: 3px 8px 3px 8px;
}
@ -341,7 +344,8 @@
color: @term-cyan;
}
.info-error, .info-msg {
.info-error,
.info-msg {
margin-top: 5px;
padding: 5px;
}
@ -354,7 +358,7 @@
white-space: pre;
width: 120px;
}
.remote-field-val {
white-space: pre;
display: flex;
@ -386,7 +390,9 @@
}
&.text-input {
input[type=text], input[type=number], input[type=password] {
input[type="text"],
input[type="number"],
input[type="password"] {
background-color: black;
color: white;
width: 200px;
@ -394,7 +400,7 @@
}
&.checkbox-input {
input[type=checkbox] {
input[type="checkbox"] {
position: relative;
top: 3px;
}

View File

@ -49,7 +49,7 @@
min-width: 100px;
overflow: auto;
border-left: 1px solid #777;
code {
flex-shrink: 0;
min-width: 100px;
@ -66,7 +66,7 @@
position: relative;
display: block;
visibility: hidden;
.inner-copy {
position: absolute;
bottom: -1px;
@ -92,7 +92,7 @@
.terminal-wrapper {
position: relative;
.term-block {
position: absolute;
top: 0;
@ -126,7 +126,7 @@
width: 5px;
height: 5px;
}
&::-webkit-scrollbar-thumb {
background: white;
}
@ -138,7 +138,7 @@
width: 5px;
height: 5px;
}
&::-webkit-scrollbar-thumb {
background: #555;
}
@ -206,7 +206,8 @@ body .xterm .xterm-viewport {
}
}
.button.is-plain, .button.is-prompt-cancel {
.button.is-plain,
.button.is-prompt-cancel {
background-color: #222;
color: @term-white;
@ -238,7 +239,7 @@ body .xterm .xterm-viewport {
.mono-font(10px);
display: flex;
flex-direction: row;
.hint-item {
padding: 0px 5px 0px 5px;
border-radius: 0 0 3px 3px;
@ -252,7 +253,7 @@ body .xterm .xterm-viewport {
.hint-item.color-green {
color: black;
background-color: @tab-green;
&:hover {
color: white;
}
@ -263,11 +264,11 @@ body .xterm .xterm-viewport {
background-color: @tab-green;
cursor: default;
}
.hint-item.color-white {
color: black;
background-color: @term-white;
&:hover {
background-color: @term-bright-white;
}
@ -282,7 +283,7 @@ body .xterm .xterm-viewport {
.hint-item.color-blue {
color: black;
background-color: @tab-blue;
&:hover {
color: white;
}
@ -426,11 +427,11 @@ body .xterm .xterm-viewport {
div:nth-child(1) {
animation-delay: -0.45s;
}
div:nth-child(2) {
animation-delay: -0.3s;
}
div:nth-child(3) {
animation-delay: -0.15s;
}
@ -457,19 +458,20 @@ body .xterm .xterm-viewport {
background-color: #171717;
outline: 2px solid #171717;
&:hover, &:focus {
&:hover,
&:focus {
color: @term-white;
background-color: #333;
outline: 2px solid #333;
}
&.connect-button {
color: @term-green;
&:hover {
color: @term-green;
}
}
&.disconnect-button {
color: @term-red;
&:hover {
@ -499,7 +501,8 @@ body .xterm .xterm-viewport {
}
&.disabled-button {
&:hover, &:focus {
&:hover,
&:focus {
outline: none;
background-color: #171717;
}
@ -521,12 +524,13 @@ body .xterm .xterm-viewport {
display: block;
background-color: #666 !important;
}
&.active, &.active.selected {
&.active,
&.active.selected {
display: block;
background-color: @tab-blue !important;
}
&.active.selected.fg-focus {
display: block;
background-color: @tab-green !important;
@ -540,7 +544,7 @@ body .xterm .xterm-viewport {
#main .term-prompt {
font-size: 14px;
i {
margin-right: 3px;
}
@ -561,7 +565,7 @@ body .xterm .xterm-viewport {
.term-prompt-remote {
color: @term-bright-green;
&.color-green {
color: @term-bright-green;
}
@ -611,18 +615,19 @@ body .xterm .xterm-viewport {
top: -3px;
color: #c4a000;
&.status-init, &.status-disconnected {
&.status-init,
&.status-disconnected {
color: #c4a000;
}
&.status-connecting {
color: #c4a000;
}
&.status-connected {
color: #4e9a06;
}
&.status-error {
color: #cc0000;
}

View File

@ -1,4 +1,4 @@
import {sprintf} from "sprintf-js";
import {Database} from "better-sqlite3";
import { sprintf } from "sprintf-js";
import { Database } from "better-sqlite3";
let DB = new Database();

View File

@ -1,31 +1,41 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import {sprintf} from "sprintf-js";
import {boundMethod} from "autobind-decorator";
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import cn from "classnames";
import {If, For, When, Otherwise, Choose} from "tsx-control-statements/components";
import type {RemoteType} from "./types";
import { If, For, When, Otherwise, Choose } from "tsx-control-statements/components";
import type { RemoteType } from "./types";
type OV<V> = mobx.IObservableValue<V>;
function renderCmdText(text : string) : any {
function renderCmdText(text: string): any {
return <span>&#x2318;{text}</span>;
}
class CmdStrCode extends React.Component<{cmdstr : string, onUse : () => void, onCopy : () => void, isCopied : boolean, fontSize : "normal" | "large", limitHeight : boolean}, {}> {
class CmdStrCode extends React.Component<
{
cmdstr: string;
onUse: () => void;
onCopy: () => void;
isCopied: boolean;
fontSize: "normal" | "large";
limitHeight: boolean;
},
{}
> {
@boundMethod
handleUse(e : any) {
handleUse(e: any) {
e.stopPropagation();
if (this.props.onUse != null) {
this.props.onUse()
this.props.onUse();
}
}
@boundMethod
handleCopy(e : any) {
handleCopy(e: any) {
e.stopPropagation();
if (this.props.onCopy != null) {
this.props.onCopy();
@ -33,21 +43,23 @@ class CmdStrCode extends React.Component<{cmdstr : string, onUse : () => void, o
}
render() {
let {isCopied, cmdstr, fontSize, limitHeight} = this.props;
let { isCopied, cmdstr, fontSize, limitHeight } = this.props;
return (
<div className={cn("cmdstr-code", {"is-large": (fontSize == "large")}, {"limit-height": limitHeight})}>
<div className={cn("cmdstr-code", { "is-large": fontSize == "large" }, { "limit-height": limitHeight })}>
<If condition={isCopied}>
<div key="copied" className="copied-indicator">
<div>copied</div>
</div>
</If>
<div key="use" className="use-button" title="Use Command" onClick={this.handleUse}><i className="fa-sharp fa-solid fa-check"/></div>
<div key="use" className="use-button" title="Use Command" onClick={this.handleUse}>
<i className="fa-sharp fa-solid fa-check" />
</div>
<div key="code" className="code-div">
<code>{cmdstr}</code>
</div>
<div key="copy" className="copy-control">
<div className="inner-copy" onClick={this.handleCopy}>
<i title="copy" className="fa-sharp fa-regular fa-copy"/>
<i title="copy" className="fa-sharp fa-regular fa-copy" />
</div>
</div>
</div>
@ -55,27 +67,27 @@ class CmdStrCode extends React.Component<{cmdstr : string, onUse : () => void, o
}
}
class Toggle extends React.Component<{checked : boolean, onChange : (value : boolean) => void}, {}> {
class Toggle extends React.Component<{ checked: boolean; onChange: (value: boolean) => void }, {}> {
@boundMethod
handleChange(e : any) : void {
let {onChange} = this.props;
handleChange(e: any): void {
let { onChange } = this.props;
if (onChange != null) {
onChange(e.target.checked);
}
}
render() {
return (
<label className="checkbox-toggle">
<input type="checkbox" checked={this.props.checked} onChange={this.handleChange}/>
<span className="slider"/>
<input type="checkbox" checked={this.props.checked} onChange={this.handleChange} />
<span className="slider" />
</label>
);
}
}
@mobxReact.observer
class RemoteStatusLight extends React.Component<{remote : RemoteType}, {}> {
class RemoteStatusLight extends React.Component<{ remote: RemoteType }, {}> {
render() {
let remote = this.props.remote;
let status = "error";
@ -84,24 +96,32 @@ class RemoteStatusLight extends React.Component<{remote : RemoteType}, {}> {
status = remote.status;
wfp = remote.waitingforpassword;
}
let icon = "fa-sharp fa-solid fa-circle"
let icon = "fa-sharp fa-solid fa-circle";
if (status == "connecting") {
icon = (wfp ? "fa-sharp fa-solid fa-key" : "fa-sharp fa-solid fa-rotate");
icon = wfp ? "fa-sharp fa-solid fa-key" : "fa-sharp fa-solid fa-rotate";
}
return (
<i className={cn("remote-status", icon, "status-" + status)}/>
);
return <i className={cn("remote-status", icon, "status-" + status)} />;
}
}
@mobxReact.observer
class InlineSettingsTextEdit extends React.Component<{text : string, value : string, onChange : (val : string) => void, maxLength : number, placeholder : string, showIcon? : boolean}, {}> {
isEditing : OV<boolean> = mobx.observable.box(false, {name: "inlineedit-isEditing"});
tempText : OV<string>;
shouldFocus : boolean = false;
inputRef : React.RefObject<any> = React.createRef();
class InlineSettingsTextEdit extends React.Component<
{
text: string;
value: string;
onChange: (val: string) => void;
maxLength: number;
placeholder: string;
showIcon?: boolean;
},
{}
> {
isEditing: OV<boolean> = mobx.observable.box(false, { name: "inlineedit-isEditing" });
tempText: OV<string>;
shouldFocus: boolean = false;
inputRef: React.RefObject<any> = React.createRef();
componentDidUpdate() : void {
componentDidUpdate(): void {
if (this.shouldFocus) {
this.shouldFocus = false;
if (this.inputRef.current != null) {
@ -111,14 +131,14 @@ class InlineSettingsTextEdit extends React.Component<{text : string, value : str
}
@boundMethod
handleChangeText(e : any) : void {
handleChangeText(e: any): void {
mobx.action(() => {
this.tempText.set(e.target.value);
})();
}
@boundMethod
confirmChange() : void {
confirmChange(): void {
mobx.action(() => {
let newText = this.tempText.get();
this.isEditing.set(false);
@ -128,7 +148,7 @@ class InlineSettingsTextEdit extends React.Component<{text : string, value : str
}
@boundMethod
cancelChange() : void {
cancelChange(): void {
mobx.action(() => {
this.isEditing.set(false);
this.tempText = null;
@ -136,7 +156,7 @@ class InlineSettingsTextEdit extends React.Component<{text : string, value : str
}
@boundMethod
handleKeyDown(e : any) : void {
handleKeyDown(e: any): void {
if (e.code == "Enter") {
e.preventDefault();
e.stopPropagation();
@ -153,36 +173,64 @@ class InlineSettingsTextEdit extends React.Component<{text : string, value : str
}
@boundMethod
clickEdit() : void {
clickEdit(): void {
mobx.action(() => {
this.isEditing.set(true);
this.shouldFocus = true;
this.tempText = mobx.observable.box(this.props.value, {name: "inlineedit-tempText"});
this.tempText = mobx.observable.box(this.props.value, { name: "inlineedit-tempText" });
})();
}
render() {
if (this.isEditing.get()) {
return (
<div className={cn("settings-input inline-edit", "edit-active")}>
<div className="field has-addons">
<div className="control">
<input ref={this.inputRef} className="input" type="text" onKeyDown={this.handleKeyDown} placeholder={this.props.placeholder} onChange={this.handleChangeText} value={this.tempText.get()} maxLength={this.props.maxLength}/>
<input
ref={this.inputRef}
className="input"
type="text"
onKeyDown={this.handleKeyDown}
placeholder={this.props.placeholder}
onChange={this.handleChangeText}
value={this.tempText.get()}
maxLength={this.props.maxLength}
/>
</div>
<div className="control">
<div onClick={this.cancelChange} title="Cancel (Esc)" className="button is-prompt-danger is-outlined is-small"><span className="icon is-small"><i className="fa-sharp fa-solid fa-xmark"/></span></div>
<div
onClick={this.cancelChange}
title="Cancel (Esc)"
className="button is-prompt-danger is-outlined is-small"
>
<span className="icon is-small">
<i className="fa-sharp fa-solid fa-xmark" />
</span>
</div>
</div>
<div className="control">
<div onClick={this.confirmChange} title="Confirm (Enter)" className="button is-prompt-green is-outlined is-small"><span className="icon is-small"><i className="fa-sharp fa-solid fa-check"/></span></div>
<div
onClick={this.confirmChange}
title="Confirm (Enter)"
className="button is-prompt-green is-outlined is-small"
>
<span className="icon is-small">
<i className="fa-sharp fa-solid fa-check" />
</span>
</div>
</div>
</div>
</div>
);
}
else {
} else {
return (
<div onClick={this.clickEdit} className={cn("settings-input inline-edit", "edit-not-active")}>
{this.props.text}<If condition={this.props.showIcon}> <i className="fa-sharp fa-solid fa-pen"/></If>
{this.props.text}
<If condition={this.props.showIcon}>
{" "}
<i className="fa-sharp fa-solid fa-pen" />
</If>
</div>
);
}
@ -190,16 +238,16 @@ class InlineSettingsTextEdit extends React.Component<{text : string, value : str
}
@mobxReact.observer
class InfoMessage extends React.Component<{width : number, children : React.ReactNode}> {
class InfoMessage extends React.Component<{ width: number; children: React.ReactNode }> {
render() {
return (
<div className="info-message">
<div className="message-icon">
<i className="fa-sharp fa-solid fa-circle-info"/>
<i className="fa-sharp fa-solid fa-circle-info" />
</div>
<div className="message-content" style={{width: this.props.width}}>
<div className="message-content" style={{ width: this.props.width }}>
<div className="info-icon">
<i className="fa-sharp fa-solid fa-circle-info"/>
<i className="fa-sharp fa-solid fa-circle-info" />
</div>
<div className="info-children">{this.props.children}</div>
</div>
@ -208,25 +256,25 @@ class InfoMessage extends React.Component<{width : number, children : React.Reac
}
}
function LinkRenderer(props : any) : any {
function LinkRenderer(props: any): any {
let newUrl = "https://extern?" + encodeURIComponent(props.href);
return <a href={newUrl} target="_blank">{props.children}</a>
}
function HeaderRenderer(props : any, hnum : number) : any {
return (
<div className={cn("title", "is-" + hnum)}>{props.children}</div>
<a href={newUrl} target="_blank">
{props.children}
</a>
);
}
function CodeRenderer(props : any) : any {
return (
<code className={cn({"inline": props.inline})}>{props.children}</code>
);
function HeaderRenderer(props: any, hnum: number): any {
return <div className={cn("title", "is-" + hnum)}>{props.children}</div>;
}
function CodeRenderer(props: any): any {
return <code className={cn({ inline: props.inline })}>{props.children}</code>;
}
@mobxReact.observer
class Markdown extends React.Component<{text : string, style? : any, extraClassName? : string}, {}> {
class Markdown extends React.Component<{ text: string; style?: any; extraClassName?: string }, {}> {
render() {
let text = this.props.text;
let markdownComponents = {
@ -241,16 +289,16 @@ class Markdown extends React.Component<{text : string, style? : any, extraClassN
};
return (
<div className={cn("markdown content", this.props.extraClassName)} style={this.props.style}>
<ReactMarkdown children={text} remarkPlugins={[remarkGfm]} components={markdownComponents}/>
<ReactMarkdown children={text} remarkPlugins={[remarkGfm]} components={markdownComponents} />
</div>
);
}
}
@mobxReact.observer
class SettingsError extends React.Component<{errorMessage : OV<string>}, {}> {
class SettingsError extends React.Component<{ errorMessage: OV<string> }, {}> {
@boundMethod
dismissError() : void {
dismissError(): void {
mobx.action(() => {
this.props.errorMessage.set(null);
})();
@ -263,11 +311,22 @@ class SettingsError extends React.Component<{errorMessage : OV<string>}, {}> {
return (
<div className="settings-field settings-error">
<div>Error: {this.props.errorMessage.get()}</div>
<div className="flex-spacer"/>
<div onClick={this.dismissError} className="error-dismiss"><i className="fa-sharp fa-solid fa-xmark"/></div>
<div className="flex-spacer" />
<div onClick={this.dismissError} className="error-dismiss">
<i className="fa-sharp fa-solid fa-xmark" />
</div>
</div>
);
}
}
export {CmdStrCode, Toggle, renderCmdText, RemoteStatusLight, InlineSettingsTextEdit, InfoMessage, Markdown, SettingsError};
export {
CmdStrCode,
Toggle,
renderCmdText,
RemoteStatusLight,
InlineSettingsTextEdit,
InfoMessage,
Markdown,
SettingsError,
};

View File

@ -3,12 +3,12 @@ import * as path from "path";
import * as fs from "fs";
import fetch from "node-fetch";
import * as child_process from "node:child_process";
import {debounce} from "throttle-debounce";
import {handleJsonFetchResponse} from "./util";
import { debounce } from "throttle-debounce";
import { handleJsonFetchResponse } from "./util";
import * as winston from "winston";
import * as util from "util";
import {sprintf} from "sprintf-js";
import {v4 as uuidv4} from "uuid";
import { sprintf } from "sprintf-js";
import { v4 as uuidv4 } from "uuid";
const PromptAppPathVarName = "PROMPT_APP_PATH";
const PromptDevVarName = "PROMPT_DEV";
@ -16,10 +16,10 @@ const AuthKeyFile = "prompt.authkey";
const DevServerEndpoint = "http://127.0.0.1:8090";
const ProdServerEndpoint = "http://127.0.0.1:1619";
let isDev = (process.env[PromptDevVarName] != null);
let isDev = process.env[PromptDevVarName] != null;
let scHome = getPromptHomeDir();
ensureDir(scHome);
let DistDir = (isDev ? "dist-dev" : "dist");
let DistDir = isDev ? "dist-dev" : "dist";
let GlobalAuthKey = "";
let instanceId = uuidv4();
let oldConsoleLog = console.log;
@ -31,18 +31,16 @@ let wasInFg = true;
let unamePlatform = process.platform;
let unameArch = process.arch;
if (unameArch == "x64") {
unameArch = "amd64"
unameArch = "amd64";
}
let logger;
let loggerConfig = {
level: "info",
format: winston.format.combine(
winston.format.timestamp({format: "YYYY-MM-DD HH:mm:ss"}),
winston.format.printf(info => `${info.timestamp} ${info.message}`),
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
winston.format.printf((info) => `${info.timestamp} ${info.message}`)
),
transports: [
new winston.transports.File({filename: path.join(scHome, "prompt-app.log"), level: "info"}),
],
transports: [new winston.transports.File({ filename: path.join(scHome, "prompt-app.log"), level: "info" })],
};
if (isDev) {
loggerConfig.transports.push(new winston.transports.Console());
@ -51,18 +49,25 @@ logger = winston.createLogger(loggerConfig);
function log(...msg) {
try {
logger.info(util.format(...msg));
}
catch (e) {
} catch (e) {
oldConsoleLog(...msg);
}
}
console.log = log;
console.log(sprintf("prompt-app starting, PROMPT_HOME=%s, apppath=%s arch=%s/%s", scHome, getAppBasePath(), unamePlatform, unameArch));
console.log(
sprintf(
"prompt-app starting, PROMPT_HOME=%s, apppath=%s arch=%s/%s",
scHome,
getAppBasePath(),
unamePlatform,
unameArch
)
);
if (isDev) {
console.log("prompt-app PROMPT_DEV set");
}
let app = electron.app;
app.setName((isDev ? "Prompt (Dev)" : "Prompt"));
app.setName(isDev ? "Prompt (Dev)" : "Prompt");
let localServerProc = null;
let localServerShouldRestart = false;
@ -78,7 +83,7 @@ function getPromptHomeDir() {
if (homeDir == null) {
homeDir = "/";
}
scHome = path.join(homeDir, (isDev ? "prompt-dev" : "prompt"));
scHome = path.join(homeDir, isDev ? "prompt-dev" : "prompt");
}
return scHome;
}
@ -116,7 +121,7 @@ function getLocalServerCwd() {
}
function ensureDir(dir) {
fs.mkdirSync(dir, {recursive: true, mode: 0o700});
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
}
function readAuthKey() {
@ -124,7 +129,7 @@ function readAuthKey() {
let authKeyFileName = path.join(homeDir, AuthKeyFile);
if (!fs.existsSync(authKeyFileName)) {
let authKeyStr = String(uuidv4());
fs.writeFileSync(authKeyFileName, authKeyStr, {mode: 0o600});
fs.writeFileSync(authKeyFileName, authKeyStr, { mode: 0o600 });
return authKeyStr;
}
let authKeyData = fs.readFileSync(authKeyFileName);
@ -141,10 +146,7 @@ let menuTemplate = [
},
{
label: "File",
submenu: [
{role: "close"},
{role: "forceReload"},
],
submenu: [{ role: "close" }, { role: "forceReload" }],
},
{
role: "editMenu",
@ -162,11 +164,11 @@ electron.Menu.setApplicationMenu(menu);
let MainWindow = null;
function getMods(input : any) {
return {meta: input.meta, shift: input.shift, ctrl: input.control, alt: input.alt};
function getMods(input: any) {
return { meta: input.meta, shift: input.shift, ctrl: input.control, alt: input.alt };
}
function shNavHandler(event : any, url : any) {
function shNavHandler(event: any, url: any) {
console.log("navigation", url);
event.preventDefault();
}
@ -181,9 +183,9 @@ function createMainWindow(clientData) {
webPreferences: {
preload: path.join(getAppBasePath(), DistDir, "preload.js"),
},
backgroundColor: '#000',
backgroundColor: "#000",
});
let indexHtml = (isDev ? "index-dev.html" : "index.html");
let indexHtml = isDev ? "index-dev.html" : "index.html";
win.loadFile(path.join(getAppBasePath(), "static", indexHtml));
win.webContents.on("before-input-event", (e, input) => {
if (win.isFocused()) {
@ -202,11 +204,10 @@ function createMainWindow(clientData) {
e.preventDefault();
if (!input.alt) {
win.webContents.send("i-cmd", mods);
}
else {
} else {
win.webContents.toggleDevTools();
}
return;
}
if (input.code == "KeyR" && input.meta) {
@ -255,18 +256,24 @@ function createMainWindow(clientData) {
return;
}
e.preventDefault();
win.webContents.send("digit-cmd", {digit: digitNum}, mods);
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);
let rel = input.code == "BracketRight" ? 1 : -1;
win.webContents.send("bracket-cmd", { relative: rel }, mods);
e.preventDefault();
return;
}
});
win.webContents.on("will-navigate", shNavHandler);
win.on("resized", debounce(400, (e) => mainResizeHandler(e, win)));
win.on("moved", debounce(400, (e) => mainResizeHandler(e, win)));
win.on(
"resized",
debounce(400, (e) => mainResizeHandler(e, win))
);
win.on(
"moved",
debounce(400, (e) => mainResizeHandler(e, win))
);
win.on("focus", () => {
wasInFg = true;
wasActive = true;
@ -278,13 +285,11 @@ function createMainWindow(clientData) {
e.preventDefault();
if (url.startsWith("https://docs.getprompt.dev/")) {
electron.shell.openExternal(url);
}
else if (url.startsWith("https://discord.gg/")) {
} else if (url.startsWith("https://discord.gg/")) {
electron.shell.openExternal(url);
}
else if (url.startsWith("https://extern/?")) {
} else if (url.startsWith("https://extern/?")) {
let qmark = url.indexOf("?");
let param = url.substr(qmark+1);
let param = url.substr(qmark + 1);
let newUrl = decodeURIComponent(param);
electron.shell.openExternal(newUrl);
}
@ -298,18 +303,20 @@ function mainResizeHandler(e, win) {
}
let bounds = win.getBounds();
console.log("resize/move", win.getBounds());
let winSize = {width: bounds.width, height: bounds.height, top: bounds.y, left: bounds.x};
let winSize = { width: bounds.width, height: bounds.height, top: bounds.y, left: bounds.x };
let url = getBaseHostPort() + "/api/set-winsize";
let fetchHeaders = getFetchHeaders();
fetch(url, {method: "post", body: JSON.stringify(winSize), headers: fetchHeaders}).then((resp) => handleJsonFetchResponse(url, resp)).catch((err) => {
console.log("error setting winsize", err)
});
fetch(url, { method: "post", body: JSON.stringify(winSize), headers: fetchHeaders })
.then((resp) => handleJsonFetchResponse(url, resp))
.catch((err) => {
console.log("error setting winsize", err);
});
}
function calcBounds(clientData) {
let primaryDisplay = electron.screen.getPrimaryDisplay();
let pdBounds = primaryDisplay.bounds;
let size = {x: 50, y: 50, width: pdBounds.width-150, height: pdBounds.height-150};
let size = { x: 50, y: 50, width: pdBounds.width - 150, height: pdBounds.height - 150 };
if (clientData != null && clientData.winsize != null && clientData.winsize.width > 0) {
let cwinSize = clientData.winsize;
if (cwinSize.width > 0) {
@ -346,8 +353,8 @@ function calcBounds(clientData) {
return size;
}
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
app.on("window-all-closed", () => {
if (process.platform !== "darwin") app.quit();
});
electron.ipcMain.on("get-id", (event) => {
@ -366,7 +373,7 @@ electron.ipcMain.on("get-authkey", (event) => {
});
electron.ipcMain.on("local-server-status", (event) => {
event.returnValue = (localServerProc != null);
event.returnValue = localServerProc != null;
return;
});
@ -375,8 +382,7 @@ electron.ipcMain.on("restart-server", (event) => {
localServerProc.kill();
localServerShouldRestart = true;
return;
}
else {
} else {
runLocalServer();
}
event.returnValue = true;
@ -391,9 +397,9 @@ electron.ipcMain.on("reload-window", (event) => {
return;
});
function getContextMenu() : any {
function getContextMenu(): any {
let menu = new electron.Menu();
let menuItem = new electron.MenuItem({label: "Testing", click: () => console.log("click testing!")});
let menuItem = new electron.MenuItem({ label: "Testing", click: () => console.log("click testing!") });
menu.append(menuItem);
return menu;
}
@ -404,32 +410,35 @@ function getFetchHeaders() {
};
}
async function getClientDataPoll(loopNum : number) {
let lastTime = (loopNum >= 6);
async function getClientDataPoll(loopNum: number) {
let lastTime = loopNum >= 6;
let cdata = await getClientData(!lastTime, loopNum);
if (lastTime || cdata != null) {
return cdata;
}
await sleep(1000);
return getClientDataPoll(loopNum+1);
return getClientDataPoll(loopNum + 1);
}
function getClientData(willRetry : boolean, retryNum : number) {
function getClientData(willRetry: boolean, retryNum: number) {
let url = getBaseHostPort() + "/api/get-client-data";
let fetchHeaders = getFetchHeaders();
return fetch(url, {headers: fetchHeaders}).then((resp) => handleJsonFetchResponse(url, resp)).then((data) => {
if (data == null) {
return fetch(url, { headers: fetchHeaders })
.then((resp) => handleJsonFetchResponse(url, resp))
.then((data) => {
if (data == null) {
return null;
}
return data.data;
})
.catch((err) => {
if (willRetry) {
console.log("error getting client-data from local-server, will retry", "(" + retryNum + ")");
return null;
}
console.log("error getting client-data from local-server, failed: ", err);
return null;
}
return data.data;
}).catch((err) => {
if (willRetry) {
console.log("error getting client-data from local-server, will retry", "(" + retryNum + ")");
return null;
}
console.log("error getting client-data from local-server, failed: ", err);
return null;
});
});
}
function sendLSSC() {
@ -479,23 +488,23 @@ function runLocalServer() {
});
proc.on("error", (e) => {
console.log("error running local-server", e);
})
proc.stdout.on("data", output => {
});
proc.stdout.on("data", (output) => {
return;
});
proc.stderr.on("data", output => {
proc.stderr.on("data", (output) => {
return;
});
return rtnPromise;
}
electron.ipcMain.on("context-screen", (event, {screenId}, {x, y}) => {
electron.ipcMain.on("context-screen", (event, { screenId }, { x, y }) => {
console.log("context-screen", screenId);
let menu = getContextMenu();
menu.popup({x, y});
menu.popup({ x, y });
});
electron.ipcMain.on("context-editmenu", (event, {x, y}, opts) => {
electron.ipcMain.on("context-editmenu", (event, { x, y }, opts) => {
if (opts == null) {
opts = {};
}
@ -503,22 +512,21 @@ electron.ipcMain.on("context-editmenu", (event, {x, y}, opts) => {
let menu = new electron.Menu();
let menuItem = null;
if (opts.showCut) {
menuItem = new electron.MenuItem({label: "Cut", role: "cut"});
menuItem = new electron.MenuItem({ label: "Cut", role: "cut" });
menu.append(menuItem);
}
menuItem = new electron.MenuItem({label: "Copy", role: "copy"});
menuItem = new electron.MenuItem({ label: "Copy", role: "copy" });
menu.append(menuItem);
menuItem = new electron.MenuItem({label: "Paste", role: "paste"});
menuItem = new electron.MenuItem({ label: "Paste", role: "paste" });
menu.append(menuItem);
menu.popup({x, y});
menu.popup({ x, y });
});
async function createMainWindowWrap() {
let clientData = null;
try {
clientData = await getClientDataPoll(1);
}
catch (e) {
} catch (e) {
console.log("error getting local-server clientdata", e.toString());
}
MainWindow = createMainWindow(clientData);
@ -532,14 +540,16 @@ async function sleep(ms) {
}
function logActiveState() {
let activeState = {fg: wasInFg, active: wasActive, open: true};
let activeState = { fg: wasInFg, active: wasActive, open: true };
let url = getBaseHostPort() + "/api/log-active-state";
let fetchHeaders = getFetchHeaders();
fetch(url, {method: "post", body: JSON.stringify(activeState), headers: fetchHeaders}).then((resp) => handleJsonFetchResponse(url, resp)).catch((err) => {
console.log("error logging active state", err)
});
fetch(url, { method: "post", body: JSON.stringify(activeState), headers: fetchHeaders })
.then((resp) => handleJsonFetchResponse(url, resp))
.catch((err) => {
console.log("error logging active state", err);
});
// for next iteration
wasInFg = (MainWindow != null && MainWindow.isFocused());
wasInFg = MainWindow != null && MainWindow.isFocused();
wasActive = false;
}
@ -561,17 +571,15 @@ function runActiveTimer() {
GlobalAuthKey = readAuthKey();
try {
await runLocalServer();
}
catch (e) {
} catch (e) {
console.log(e.toString());
}
setTimeout(runActiveTimer, 5000); // start active timer, wait 5s just to be safe
await app.whenReady();
await createMainWindowWrap();
app.on('activate', () => {
app.on("activate", () => {
if (electron.BrowserWindow.getAllWindows().length === 0) {
createMainWindowWrap().then();
}
})
});
})();

View File

@ -1,37 +1,60 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
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, TermContextUnion, RendererContainerType} from "./types";
import {PacketDataBuffer} from "./ptydata";
import {debounce, throttle} from "throttle-debounce";
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,
TermContextUnion,
RendererContainerType,
} from "./types";
import { PacketDataBuffer } from "./ptydata";
import { debounce, throttle } from "throttle-debounce";
type OV<V> = mobx.IObservableValue<V>;
type CV<V> = mobx.IComputedValue<V>;
@mobxReact.observer
class FullRenderer extends React.Component<{rendererContainer : RendererContainerType, lineId : string, plugin : RendererPluginType, onHeightChange : () => void, initParams : RendererModelInitializeParams}, {}> {
model : RendererModel;
wrapperDivRef : React.RefObject<any> = React.createRef();
rszObs : ResizeObserver;
updateHeight_debounced : (newHeight : number) => void;
class FullRenderer extends React.Component<
{
rendererContainer: RendererContainerType;
lineId: string;
plugin: RendererPluginType;
onHeightChange: () => void;
initParams: RendererModelInitializeParams;
},
{}
> {
model: RendererModel;
wrapperDivRef: React.RefObject<any> = React.createRef();
rszObs: ResizeObserver;
updateHeight_debounced: (newHeight: number) => void;
constructor(props : any) {
constructor(props: any) {
super(props);
let {rendererContainer, lineId, plugin, initParams} = this.props;
let { rendererContainer, lineId, plugin, initParams } = this.props;
this.model = plugin.modelCtor();
this.model.initialize(initParams);
rendererContainer.registerRenderer(lineId, this.model);
this.updateHeight_debounced = debounce(1000, this.updateHeight.bind(this));
}
updateHeight(newHeight : number) : void {
updateHeight(newHeight: number): void {
this.model.updateHeight(newHeight);
}
handleResize(entries : ResizeObserverEntry[]) : void {
handleResize(entries: ResizeObserverEntry[]): void {
if (this.props.onHeightChange) {
this.props.onHeightChange();
}
@ -51,13 +74,13 @@ class FullRenderer extends React.Component<{rendererContainer : RendererContaine
this.rszObs = new ResizeObserver(this.handleResize.bind(this));
this.rszObs.observe(this.wrapperDivRef.current);
}
componentDidMount() {
this.checkRszObs();
}
componentWillUnmount() {
let {rendererContainer, lineId} = this.props;
let { rendererContainer, lineId } = this.props;
rendererContainer.unloadRenderer(lineId);
if (this.rszObs != null) {
this.rszObs.disconnect();
@ -70,20 +93,17 @@ class FullRenderer extends React.Component<{rendererContainer : RendererContaine
}
render() {
let {plugin} = this.props;
let { plugin } = this.props;
let Comp = plugin.fullComponent;
if (Comp == null) {
<div ref={this.wrapperDivRef}>
(no component found in plugin)
</div>
<div ref={this.wrapperDivRef}>(no component found in plugin)</div>;
}
return (
<div ref={this.wrapperDivRef}>
<Comp model={this.model}/>
<Comp model={this.model} />
</div>
);
}
}
export {FullRenderer};
export { FullRenderer };

View File

@ -1,44 +1,42 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import {If, For, When, Otherwise, Choose} from "tsx-control-statements/components";
import {sprintf} from "sprintf-js";
import {boundMethod} from "autobind-decorator";
import { If, For, When, Otherwise, Choose } from "tsx-control-statements/components";
import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import {GlobalModel, GlobalCommandRunner, Cmd} from "./model";
import {HistoryItem, RemotePtrType, LineType, CmdDataType} from "./types";
import { GlobalModel, GlobalCommandRunner, Cmd } from "./model";
import { HistoryItem, RemotePtrType, LineType, CmdDataType } from "./types";
import dayjs from "dayjs";
import localizedFormat from 'dayjs/plugin/localizedFormat';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import {Line} from "./linecomps";
import {CmdStrCode} from "./elements";
import localizedFormat from "dayjs/plugin/localizedFormat";
import customParseFormat from "dayjs/plugin/customParseFormat";
import { Line } from "./linecomps";
import { CmdStrCode } from "./elements";
dayjs.extend(customParseFormat)
dayjs.extend(localizedFormat)
dayjs.extend(customParseFormat);
dayjs.extend(localizedFormat);
type OV<V> = mobx.IObservableValue<V>;
type OArr<V> = mobx.IObservableArray<V>;
type OMap<K,V> = mobx.ObservableMap<K,V>;
type OMap<K, V> = mobx.ObservableMap<K, V>;
type CV<V> = mobx.IComputedValue<V>;
function isBlank(s : string) {
return (s == null || s == "");
function isBlank(s: string) {
return s == null || s == "";
}
function getHistoryViewTs(nowDate : Date, ts : number) : string {
function getHistoryViewTs(nowDate: Date, ts: number): string {
let itemDate = new Date(ts);
if (nowDate.getFullYear() != itemDate.getFullYear()) {
return dayjs(itemDate).format("M/D/YY");
}
else if (nowDate.getMonth() != itemDate.getMonth() || nowDate.getDate() != itemDate.getDate()) {
} else if (nowDate.getMonth() != itemDate.getMonth() || nowDate.getDate() != itemDate.getDate()) {
return dayjs(itemDate).format("MMM D");
}
else {
} else {
return dayjs(itemDate).format("h:mm A");
}
}
function formatRemoteName(rnames : Record<string, string>, rptr : RemotePtrType) : string {
function formatRemoteName(rnames: Record<string, string>, rptr: RemotePtrType): string {
if (rptr == null || isBlank(rptr.remoteid)) {
return "";
}
@ -52,7 +50,7 @@ function formatRemoteName(rnames : Record<string, string>, rptr : RemotePtrType)
return "[" + rname + "]";
}
function formatSSName(snames : Record<string, string>, scrnames : Record<string, string>, item : HistoryItem) : string {
function formatSSName(snames: Record<string, string>, scrnames: Record<string, string>, item: HistoryItem): string {
if (isBlank(item.sessionid)) {
return "";
}
@ -65,7 +63,7 @@ function formatSSName(snames : Record<string, string>, scrnames : Record<string,
return sessionName;
}
function formatSessionName(snames : Record<string, string>, sessionId : string) : string {
function formatSessionName(snames: Record<string, string>, sessionId: string): string {
if (isBlank(sessionId)) {
return "";
}
@ -78,15 +76,15 @@ function formatSessionName(snames : Record<string, string>, sessionId : string)
@mobxReact.observer
class HistoryView extends React.Component<{}, {}> {
tableRef : React.RefObject<any> = React.createRef();
tableWidth : OV<number> = mobx.observable.box(0, {name: "tableWidth"});
tableRszObs : ResizeObserver;
sessionDropdownActive : OV<boolean> = mobx.observable.box(false, {name: "sessionDropdownActive"});
remoteDropdownActive : OV<boolean> = mobx.observable.box(false, {name: "remoteDropdownActive"});
copiedItemId : OV<string> = mobx.observable.box(null, {name: "copiedItemId"});
tableRef: React.RefObject<any> = React.createRef();
tableWidth: OV<number> = mobx.observable.box(0, { name: "tableWidth" });
tableRszObs: ResizeObserver;
sessionDropdownActive: OV<boolean> = mobx.observable.box(false, { name: "sessionDropdownActive" });
remoteDropdownActive: OV<boolean> = mobx.observable.box(false, { name: "remoteDropdownActive" });
copiedItemId: OV<string> = mobx.observable.box(null, { name: "copiedItemId" });
@boundMethod
clickCloseHandler() : void {
clickCloseHandler(): void {
GlobalModel.historyViewModel.closeView();
}
@ -101,14 +99,14 @@ class HistoryView extends React.Component<{}, {}> {
}
@boundMethod
changeSearchText(e : any) {
changeSearchText(e: any) {
mobx.action(() => {
GlobalModel.historyViewModel.searchText.set(e.target.value);
})();
}
@boundMethod
searchKeyDown(e : any) {
searchKeyDown(e: any) {
if (e.code == "Enter") {
e.preventDefault();
GlobalModel.historyViewModel.submitSearch();
@ -117,13 +115,12 @@ class HistoryView extends React.Component<{}, {}> {
}
@boundMethod
handleSelect(historyId : string) {
handleSelect(historyId: string) {
let hvm = GlobalModel.historyViewModel;
mobx.action(() => {
if (hvm.selectedItems.get(historyId)) {
hvm.selectedItems.delete(historyId);
}
else {
} else {
hvm.selectedItems.set(historyId, true);
}
})();
@ -137,9 +134,8 @@ class HistoryView extends React.Component<{}, {}> {
if (numSelected > 0) {
hvm.selectedItems.clear();
return;
}
else {
for (let i=0; i<hvm.items.length; i++) {
} else {
for (let i = 0; i < hvm.items.length; i++) {
hvm.selectedItems.set(hvm.items[i].historyid, true);
}
}
@ -152,11 +148,10 @@ class HistoryView extends React.Component<{}, {}> {
}
@boundMethod
activateItem(historyId : string) {
activateItem(historyId: string) {
if (GlobalModel.historyViewModel.activeItem.get() == historyId) {
GlobalModel.historyViewModel.setActiveItem(null);
}
else {
} else {
GlobalModel.historyViewModel.setActiveItem(historyId);
}
}
@ -192,7 +187,7 @@ class HistoryView extends React.Component<{}, {}> {
this.checkWidth();
}
searchFromTsInputValue() : string {
searchFromTsInputValue(): string {
let hvm = GlobalModel.historyViewModel;
let fromDate = hvm.searchFromDate.get();
if (fromDate == null) {
@ -202,7 +197,7 @@ class HistoryView extends React.Component<{}, {}> {
}
@boundMethod
handleFromTsChange(e : any) : void {
handleFromTsChange(e: any): void {
let hvm = GlobalModel.historyViewModel;
let newDate = e.target.value;
let today = dayjs().format("YYYY-MM-DD");
@ -215,7 +210,7 @@ class HistoryView extends React.Component<{}, {}> {
}
@boundMethod
toggleSessionDropdown() : void {
toggleSessionDropdown(): void {
mobx.action(() => {
this.sessionDropdownActive.set(!this.sessionDropdownActive.get());
if (this.sessionDropdownActive.get()) {
@ -225,7 +220,7 @@ class HistoryView extends React.Component<{}, {}> {
}
@boundMethod
clickLimitSession(sessionId : string) : void {
clickLimitSession(sessionId: string): void {
let hvm = GlobalModel.historyViewModel;
mobx.action(() => {
this.sessionDropdownActive.set(false);
@ -234,7 +229,7 @@ class HistoryView extends React.Component<{}, {}> {
}
@boundMethod
toggleRemoteDropdown() : void {
toggleRemoteDropdown(): void {
mobx.action(() => {
this.remoteDropdownActive.set(!this.remoteDropdownActive.get());
if (this.remoteDropdownActive.get()) {
@ -244,7 +239,7 @@ class HistoryView extends React.Component<{}, {}> {
}
@boundMethod
clickLimitRemote(remoteId : string) : void {
clickLimitRemote(remoteId: string): void {
let hvm = GlobalModel.historyViewModel;
mobx.action(() => {
this.remoteDropdownActive.set(false);
@ -253,7 +248,7 @@ class HistoryView extends React.Component<{}, {}> {
}
@boundMethod
toggleShowMeta() : void {
toggleShowMeta(): void {
let hvm = GlobalModel.historyViewModel;
mobx.action(() => {
hvm.setSearchShowMeta(!hvm.searchShowMeta.get());
@ -261,7 +256,7 @@ class HistoryView extends React.Component<{}, {}> {
}
@boundMethod
toggleFilterCmds() : void {
toggleFilterCmds(): void {
let hvm = GlobalModel.historyViewModel;
mobx.action(() => {
hvm.setSearchFilterCmds(!hvm.searchFilterCmds.get());
@ -269,13 +264,13 @@ class HistoryView extends React.Component<{}, {}> {
}
@boundMethod
resetAllFilters() : void {
resetAllFilters(): void {
let hvm = GlobalModel.historyViewModel;
hvm.resetAllFilters();
}
@boundMethod
handleCopy(item : HistoryItem) : void {
handleCopy(item: HistoryItem): void {
if (isBlank(item.cmdstr)) {
return;
}
@ -291,7 +286,7 @@ class HistoryView extends React.Component<{}, {}> {
}
@boundMethod
handleUse(item : HistoryItem) : void {
handleUse(item: HistoryItem): void {
if (isBlank(item.cmdstr)) {
return;
}
@ -301,15 +296,15 @@ class HistoryView extends React.Component<{}, {}> {
setTimeout(() => GlobalModel.inputModel.giveFocus(), 50);
})();
}
render() {
let isHidden = (GlobalModel.activeMainView.get() != "history");
let isHidden = GlobalModel.activeMainView.get() != "history";
if (isHidden) {
return null;
}
let hvm = GlobalModel.historyViewModel;
let idx : number = 0;
let item : HistoryItem = null;
let idx: number = 0;
let item: HistoryItem = null;
let items = hvm.items.slice();
let nowDate = new Date();
let snames = GlobalModel.getSessionNames();
@ -327,34 +322,47 @@ class HistoryView extends React.Component<{}, {}> {
}
let activeItemId = hvm.activeItem.get();
let activeItem = hvm.getHistoryItemById(activeItemId);
let activeLine : LineType = null;
let activeLine: LineType = null;
if (activeItem != null) {
activeLine = hvm.getLineById(activeItem.lineid);
}
let sessionIds = Object.keys(snames);
let sessionId : string = null;
let sessionId: string = null;
let remoteIds = Object.keys(rnames);
let remoteId : string = null;
let remoteId: string = null;
return (
<div className={cn("history-view", "alt-view", {"is-hidden": isHidden})}>
<div className={cn("history-view", "alt-view", { "is-hidden": isHidden })}>
<div className="header">
<div className="history-title">
HISTORY
</div>
<div className="history-title">HISTORY</div>
<div className="history-search">
<div className="main-search field">
<p className="control has-icons-left">
<input className="input" type="text" placeholder="Exact String Search" value={hvm.searchText.get()} onChange={this.changeSearchText} onKeyDown={this.searchKeyDown}/>
<input
className="input"
type="text"
placeholder="Exact String Search"
value={hvm.searchText.get()}
onChange={this.changeSearchText}
onKeyDown={this.searchKeyDown}
/>
<span className="icon is-small is-left">
<i className="fa-sharp fa-solid fa-search"/>
<i className="fa-sharp fa-solid fa-search" />
</span>
</p>
</div>
<div className="advanced-search">
<div className={cn("dropdown", "session-dropdown", {"is-active": this.sessionDropdownActive.get()})}>
<div
className={cn("dropdown", "session-dropdown", {
"is-active": this.sessionDropdownActive.get(),
})}
>
<div className="dropdown-trigger">
<button onClick={this.toggleSessionDropdown} className="button is-small is-dark">
<span>{hvm.searchSessionId.get() == null ? "Limit Session" : formatSessionName(snames, hvm.searchSessionId.get())}</span>
<span>
{hvm.searchSessionId.get() == null
? "Limit Session"
: formatSessionName(snames, hvm.searchSessionId.get())}
</span>
<span className="icon is-small">
<i className="fa-sharp fa-regular fa-angle-down" aria-hidden="true"></i>
</span>
@ -362,17 +370,37 @@ class HistoryView extends React.Component<{}, {}> {
</div>
<div className="dropdown-menu" role="menu">
<div className="dropdown-content has-background-black-ter">
<div onClick={() => this.clickLimitSession(null) } key="all" className="dropdown-item">(all sessions)</div>
<div
onClick={() => this.clickLimitSession(null)}
key="all"
className="dropdown-item"
>
(all sessions)
</div>
<For each="sessionId" of={sessionIds}>
<div onClick={() => this.clickLimitSession(sessionId) } key={sessionId} className="dropdown-item">#{snames[sessionId]}</div>
<div
onClick={() => this.clickLimitSession(sessionId)}
key={sessionId}
className="dropdown-item"
>
#{snames[sessionId]}
</div>
</For>
</div>
</div>
</div>
<div className={cn("dropdown", "remote-dropdown", {"is-active": this.remoteDropdownActive.get()})}>
<div
className={cn("dropdown", "remote-dropdown", {
"is-active": this.remoteDropdownActive.get(),
})}
>
<div className="dropdown-trigger">
<button onClick={this.toggleRemoteDropdown} className="button is-small is-dark">
<span>{hvm.searchRemoteId.get() == null ? "Limit Remote" : formatRemoteName(rnames, {remoteid: hvm.searchRemoteId.get()})}</span>
<span>
{hvm.searchRemoteId.get() == null
? "Limit Remote"
: formatRemoteName(rnames, { remoteid: hvm.searchRemoteId.get() })}
</span>
<span className="icon is-small">
<i className="fa-sharp fa-regular fa-angle-down" aria-hidden="true"></i>
</span>
@ -380,26 +408,64 @@ class HistoryView extends React.Component<{}, {}> {
</div>
<div className="dropdown-menu" role="menu">
<div className="dropdown-content has-background-black-ter">
<div onClick={() => this.clickLimitRemote(null) } key="all" className="dropdown-item">(all remotes)</div>
<div
onClick={() => this.clickLimitRemote(null)}
key="all"
className="dropdown-item"
>
(all remotes)
</div>
<For each="remoteId" of={remoteIds}>
<div onClick={() => this.clickLimitRemote(remoteId) } key={remoteId} className="dropdown-item">[{rnames[remoteId]}]</div>
<div
onClick={() => this.clickLimitRemote(remoteId)}
key={remoteId}
className="dropdown-item"
>
[{rnames[remoteId]}]
</div>
</For>
</div>
</div>
</div>
<div className="allow-meta search-checkbox">
<div className="checkbox-container"><input onChange={this.toggleShowMeta} type="checkbox" checked={hvm.searchShowMeta.get()}/></div>
<div onClick={this.toggleShowMeta} className="checkbox-text">Show MetaCmds</div>
</div>
<div className="fromts">
<div onClick={this.toggleShowMeta} className="fromts-text">From:&nbsp;</div>
<div>
<input type="date" onChange={this.handleFromTsChange} value={this.searchFromTsInputValue()} className="input is-small"/>
<div className="checkbox-container">
<input
onChange={this.toggleShowMeta}
type="checkbox"
checked={hvm.searchShowMeta.get()}
/>
</div>
<div onClick={this.toggleShowMeta} className="checkbox-text">
Show MetaCmds
</div>
</div>
<div className="filter-cmds search-checkbox" title="Filter common commands like 'ls' and 'cd' from the results">
<div className="checkbox-container"><input onChange={this.toggleFilterCmds} type="checkbox" checked={hvm.searchFilterCmds.get()}/></div>
<div onClick={this.toggleFilterCmds} className="checkbox-text">Filter Cmds</div>
<div className="fromts">
<div onClick={this.toggleShowMeta} className="fromts-text">
From:&nbsp;
</div>
<div>
<input
type="date"
onChange={this.handleFromTsChange}
value={this.searchFromTsInputValue()}
className="input is-small"
/>
</div>
</div>
<div
className="filter-cmds search-checkbox"
title="Filter common commands like 'ls' and 'cd' from the results"
>
<div className="checkbox-container">
<input
onChange={this.toggleFilterCmds}
type="checkbox"
checked={hvm.searchFilterCmds.get()}
/>
</div>
<div onClick={this.toggleFilterCmds} className="checkbox-text">
Filter Cmds
</div>
</div>
<div onClick={this.resetAllFilters} className="reset-button">
Reset All
@ -407,26 +473,49 @@ class HistoryView extends React.Component<{}, {}> {
</div>
</div>
<div className="close-div">
<i onClick={this.clickCloseHandler} className="fa-sharp fa-solid fa-xmark"/>
<i onClick={this.clickCloseHandler} className="fa-sharp fa-solid fa-xmark" />
</div>
</div>
<div className={cn("control-bar", "is-top", {"is-hidden": (items.length == 0)})}>
<div className={cn("control-bar", "is-top", { "is-hidden": items.length == 0 })}>
<div className="control-checkbox" onClick={this.handleControlCheckbox}>
<i className={controlCheckboxIcon} title="Toggle Selection"/>
<i className={controlCheckboxIcon} title="Toggle Selection" />
</div>
<div className={cn("control-button delete-button", {"is-disabled": (numSelected == 0)}, {"is-active": hvm.deleteActive.get()})} onClick={this.handleClickDelete}>
<i className="fa-sharp fa-solid fa-trash" title="Purge Selected Items"/> <span>Delete Items</span>
<div
className={cn(
"control-button delete-button",
{ "is-disabled": numSelected == 0 },
{ "is-active": hvm.deleteActive.get() }
)}
onClick={this.handleClickDelete}
>
<i className="fa-sharp fa-solid fa-trash" title="Purge Selected Items" />{" "}
<span>Delete Items</span>
</div>
<div className="spacer" />
<div className="showing-text">
Showing {offset + 1}-{offset + items.length}
</div>
<div
className={cn("showing-btn", { "is-disabled": offset == 0 })}
onClick={offset != 0 ? this.handlePrev : null}
>
<i className="fa-sharp fa-solid fa-chevron-left" />
</div>
<div className="btn-spacer" />
<div
className={cn("showing-btn", { "is-disabled": !hasMore })}
onClick={hasMore ? this.handleNext : null}
>
<i className="fa-sharp fa-solid fa-chevron-right" />
</div>
<div className="spacer"/>
<div className="showing-text">Showing {offset+1}-{offset+items.length}</div>
<div className={cn("showing-btn", {"is-disabled": (offset == 0)})} onClick={(offset != 0 ? this.handlePrev : null)}><i className="fa-sharp fa-solid fa-chevron-left"/></div>
<div className="btn-spacer"/>
<div className={cn("showing-btn", {"is-disabled": !hasMore})} onClick={hasMore ? this.handleNext : null}><i className="fa-sharp fa-solid fa-chevron-right"/></div>
</div>
<table className="history-table" cellSpacing="0" cellPadding="0" border={0} ref={this.tableRef}>
<tbody>
<For index="idx" each="item" of={items}>
<tr key={item.historyid} className={cn("history-item", {"is-selected": hvm.selectedItems.get(item.historyid)})}>
<tr
key={item.historyid}
className={cn("history-item", { "is-selected": hvm.selectedItems.get(item.historyid) })}
>
<td className="selectbox" onClick={() => this.handleSelect(item.historyid)}>
<If condition={hvm.selectedItems.get(item.historyid)}>
<i className="fa-sharp fa-regular fa-square-check"></i>
@ -435,38 +524,55 @@ class HistoryView extends React.Component<{}, {}> {
<i className="fa-sharp fa-regular fa-square"></i>
</If>
</td>
<td className="bookmark" style={{display: "none"}}>
<i className="fa-sharp fa-regular fa-bookmark"/>
</td>
<td className="ts">
{getHistoryViewTs(nowDate, item.ts)}
</td>
<td className="session">
{formatSSName(snames, scrnames, item)}
</td>
<td className="remote">
{formatRemoteName(rnames, item.remote)}
<td className="bookmark" style={{ display: "none" }}>
<i className="fa-sharp fa-regular fa-bookmark" />
</td>
<td className="ts">{getHistoryViewTs(nowDate, item.ts)}</td>
<td className="session">{formatSSName(snames, scrnames, item)}</td>
<td className="remote">{formatRemoteName(rnames, item.remote)}</td>
<td className="cmdstr" onClick={() => this.activateItem(item.historyid)}>
<CmdStrCode cmdstr={item.cmdstr} onUse={() => this.handleUse(item)} onCopy={() => this.handleCopy(item)} isCopied={this.copiedItemId.get() == item.historyid} fontSize="normal" limitHeight={true}/>
<CmdStrCode
cmdstr={item.cmdstr}
onUse={() => this.handleUse(item)}
onCopy={() => this.handleCopy(item)}
isCopied={this.copiedItemId.get() == item.historyid}
fontSize="normal"
limitHeight={true}
/>
</td>
</tr>
<If condition={activeItemId == item.historyid}>
<tr className="active-history-item">
<td colSpan={6}>
<LineContainer key={activeItemId} historyId={activeItemId} width={this.tableWidth.get()}/>
<LineContainer
key={activeItemId}
historyId={activeItemId}
width={this.tableWidth.get()}
/>
</td>
</tr>
</If>
</For>
</tbody>
</table>
<div className={cn("control-bar", {"is-hidden": (items.length == 0 || !hasMore)})}>
<div className="spacer"/>
<div className="showing-text">Showing {offset+1}-{offset+items.length}</div>
<div className={cn("showing-btn", {"is-disabled": (offset == 0)})} onClick={(offset != 0 ? this.handlePrev : null)}><i className="fa-sharp fa-solid fa-chevron-left"/></div>
<div className="btn-spacer"/>
<div className={cn("showing-btn", {"is-disabled": !hasMore})} onClick={hasMore ? this.handleNext : null}><i className="fa-sharp fa-solid fa-chevron-right"/></div>
<div className={cn("control-bar", { "is-hidden": items.length == 0 || !hasMore })}>
<div className="spacer" />
<div className="showing-text">
Showing {offset + 1}-{offset + items.length}
</div>
<div
className={cn("showing-btn", { "is-disabled": offset == 0 })}
onClick={offset != 0 ? this.handlePrev : null}
>
<i className="fa-sharp fa-solid fa-chevron-left" />
</div>
<div className="btn-spacer" />
<div
className={cn("showing-btn", { "is-disabled": !hasMore })}
onClick={hasMore ? this.handleNext : null}
>
<i className="fa-sharp fa-solid fa-chevron-right" />
</div>
</div>
<If condition={items.length == 0}>
<div className="no-items">
@ -475,7 +581,8 @@ class HistoryView extends React.Component<{}, {}> {
</If>
<div className="alt-help">
<div className="help-entry">
[Esc] to Close<br/>
[Esc] to Close
<br />
</div>
</div>
</div>
@ -483,14 +590,14 @@ class HistoryView extends React.Component<{}, {}> {
}
}
class LineContainer extends React.Component<{historyId : string, width : number}, {}> {
line : LineType;
cmd : Cmd;
historyItem : HistoryItem;
visible : OV<boolean> = mobx.observable.box(true);
overrideCollapsed : OV<boolean> = mobx.observable.box(false);
constructor(props : any) {
class LineContainer extends React.Component<{ historyId: string; width: number }, {}> {
line: LineType;
cmd: Cmd;
historyItem: HistoryItem;
visible: OV<boolean> = mobx.observable.box(true);
overrideCollapsed: OV<boolean> = mobx.observable.box(false);
constructor(props: any) {
super(props);
let hvm = GlobalModel.historyViewModel;
this.historyItem = hvm.getHistoryItemById(props.historyId);
@ -502,7 +609,7 @@ class LineContainer extends React.Component<{historyId : string, width : number}
}
@boundMethod
handleHeightChange(lineNum : number, newHeight : number, oldHeight : number) : void {
handleHeightChange(lineNum: number, newHeight: number, oldHeight: number): void {
return;
}
@ -515,14 +622,18 @@ class LineContainer extends React.Component<{historyId : string, width : number}
GlobalModel.historyViewModel.closeView();
GlobalCommandRunner.lineView(screen.sessionId, screen.screenId, this.line.linenum);
}
render() {
let hvm = GlobalModel.historyViewModel;
if (this.historyItem == null || this.props.width == 0) {
return null;
}
if (this.line == null) {
return <div className="line-container no-line"><div>[no line data]</div></div>;
return (
<div className="line-container no-line">
<div>[no line data]</div>
</div>
);
}
let width = this.props.width;
width = width - 50;
@ -541,17 +652,29 @@ class LineContainer extends React.Component<{historyId : string, width : number}
<div className="line-container">
<If condition={canViewInContext}>
<div className="line-context">
<div title="View in Context" className="vic-btn" onClick={this.viewInContext}><i className="fa-sharp fa-solid fa-right"/> {ssStr}</div>
<div title="View in Context" className="vic-btn" onClick={this.viewInContext}>
<i className="fa-sharp fa-solid fa-right" /> {ssStr}
</div>
</div>
</If>
<If condition={session == null}>
<div className="no-line-context"/>
<div className="no-line-context" />
</If>
<Line screen={hvm.specialLineContainer} line={this.line} width={width} staticRender={false} visible={this.visible} onHeightChange={this.handleHeightChange} overrideCollapsed={this.overrideCollapsed} topBorder={false} renderMode="normal" noSelect={true}/>
<Line
screen={hvm.specialLineContainer}
line={this.line}
width={width}
staticRender={false}
visible={this.visible}
onHeightChange={this.handleHeightChange}
overrideCollapsed={this.overrideCollapsed}
topBorder={false}
renderMode="normal"
noSelect={true}
/>
</div>
);
}
}
export {HistoryView};
export { HistoryView };

File diff suppressed because it is too large Load Diff

View File

@ -60,19 +60,20 @@
.line-minimise:hover {
color: white;
}
.line-icon + .line-icon {
margin-left: 5px;
}
.line-icon.active {
visibility: visible;
display: block;
i.fa-star, i.fa-thumb-tack {
i.fa-star,
i.fa-thumb-tack {
color: @term-bright-yellow;
}
i.fa-bookmark {
color: @term-bright-blue;
}
@ -251,7 +252,7 @@
&.rtnstate {
border: 1px solid white;
}
.status-icon {
font-size: 8px;
position: absolute;
@ -341,8 +342,10 @@
margin-top: 1px;
margin-right: 10px;
}
.ts, .termopts, .renderer {
.ts,
.termopts,
.renderer {
display: flex;
color: #aaa;
margin-top: 5px;
@ -383,7 +386,7 @@
padding-right: 3px;
}
}
.metapart-mono {
color: #ddd;
margin-left: 8px;
@ -391,7 +394,7 @@
white-space: nowrap;
.mono-font(14px);
}
.cmdtext {
color: #fff;
font-size: 16px;
@ -471,8 +474,9 @@
color: #aaa;
}
.line-sep::before, .line-sep::after {
content: '';
.line-sep::before,
.line-sep::after {
content: "";
height: 2px;
background-color: #ccc;
flex-grow: 1;

View File

@ -1,13 +1,13 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import {sprintf} from "sprintf-js";
import {boundMethod} from "autobind-decorator";
import {If, For, When, Otherwise, Choose} from "tsx-control-statements/components";
import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import { If, For, When, Otherwise, Choose } from "tsx-control-statements/components";
import cn from "classnames";
import dayjs from "dayjs";
import localizedFormat from 'dayjs/plugin/localizedFormat';
import {debounce, throttle} from "throttle-debounce";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { debounce, throttle } from "throttle-debounce";
import * as T from "./types";
import * as util from "./util";
import * as lineutil from "./lineutil";
@ -15,45 +15,57 @@ import * as lineutil from "./lineutil";
dayjs.extend(localizedFormat);
type OV<V> = mobx.IObservableValue<V>;
type OArr<V> = mobx.IObservableArray<V>;
type OMap<K,V> = mobx.ObservableMap<K,V>;
type OMap<K, V> = mobx.ObservableMap<K, V>;
const LinesVisiblePadding = 500;
type ScreenInterface = {
setAnchorFields(anchorLine : number, anchorOffset : number, reason : string) : void,
getSelectedLine() : number,
getAnchor() : {anchorLine : number, anchorOffset : number},
}
setAnchorFields(anchorLine: number, anchorOffset: number, reason: string): void;
getSelectedLine(): number;
getAnchor(): { anchorLine: number; anchorOffset: number };
};
// <Line key={line.lineid} line={line} screen={screen} width={width} visible={this.visibleMap.get(lineNumStr)} staticRender={this.staticRender.get()} onHeightChange={this.onHeightChange} overrideCollapsed={this.collapsedMap.get(lineNumStr)} topBorder={topBorder} renderMode={renderMode}/>;
type LineCompFactory = (props : T.LineFactoryProps) => JSX.Element;
type LineCompFactory = (props: T.LineFactoryProps) => JSX.Element;
@mobxReact.observer
class LinesView extends React.Component<{screen : ScreenInterface, width : number, lines : T.LineInterface[], renderMode : T.RenderModeType, lineFactory : LineCompFactory}, {}> {
rszObs : ResizeObserver;
linesRef : React.RefObject<any>;
staticRender : OV<boolean> = mobx.observable.box(true, {name: "static-render"});
lastOffsetHeight : number = 0;
lastOffsetWidth : number = 0;
ignoreNextScroll : boolean = false;
visibleMap : Map<string, OV<boolean>>; // linenum => OV<vis>
collapsedMap : Map<string, OV<boolean>>; // linenum => OV<collapsed>
lastLinesLength : number = 0;
lastSelectedLine : number = 0;
class LinesView extends React.Component<
{
screen: ScreenInterface;
width: number;
lines: T.LineInterface[];
renderMode: T.RenderModeType;
lineFactory: LineCompFactory;
},
{}
> {
rszObs: ResizeObserver;
linesRef: React.RefObject<any>;
staticRender: OV<boolean> = mobx.observable.box(true, { name: "static-render" });
lastOffsetHeight: number = 0;
lastOffsetWidth: number = 0;
ignoreNextScroll: boolean = false;
visibleMap: Map<string, OV<boolean>>; // linenum => OV<vis>
collapsedMap: Map<string, OV<boolean>>; // linenum => OV<collapsed>
lastLinesLength: number = 0;
lastSelectedLine: number = 0;
computeAnchorLine_throttled : () => void;
computeVisibleMap_debounced : () => void;
computeAnchorLine_throttled: () => void;
computeVisibleMap_debounced: () => void;
constructor(props) {
super(props);
this.linesRef = React.createRef();
this.computeAnchorLine_throttled = throttle(100, this.computeAnchorLine.bind(this), {noLeading: true, noTrailing: false});
this.computeAnchorLine_throttled = throttle(100, this.computeAnchorLine.bind(this), {
noLeading: true,
noTrailing: false,
});
this.visibleMap = new Map();
this.collapsedMap = new Map();
this.computeVisibleMap_debounced = debounce(1000, this.computeVisibleMap.bind(this));
}
@boundMethod
scrollHandler() {
let linesElem = this.linesRef.current;
@ -74,8 +86,8 @@ class LinesView extends React.Component<{screen : ScreenInterface, width : numbe
this.computeAnchorLine_throttled(); // only do this when we're not ignoring the scroll
}
computeAnchorLine() : void {
let {screen} = this.props;
computeAnchorLine(): void {
let { screen } = this.props;
let linesElem = this.linesRef.current;
if (linesElem == null) {
screen.setAnchorFields(null, 0, "no-lines");
@ -89,8 +101,8 @@ class LinesView extends React.Component<{screen : ScreenInterface, width : numbe
let scrollTop = linesElem.scrollTop;
let height = linesElem.clientHeight;
let containerBottom = scrollTop + height;
let anchorElem : HTMLElement = null;
for (let i=lineElemArr.length-1; i >= 0; i--) {
let anchorElem: HTMLElement = null;
for (let i = lineElemArr.length - 1; i >= 0; i--) {
let lineElem = lineElemArr[i];
let bottomPos = lineElem.offsetTop + lineElem.offsetHeight;
if (anchorElem == null && (bottomPos <= containerBottom || lineElem.offsetTop <= scrollTop)) {
@ -106,26 +118,26 @@ class LinesView extends React.Component<{screen : ScreenInterface, width : numbe
screen.setAnchorFields(anchorLineNum, anchorOffset, "computeAnchorLine");
}
computeVisibleMap() : void {
computeVisibleMap(): void {
let linesElem = this.linesRef.current;
if (linesElem == null) {
return;
}
if (linesElem.offsetParent == null) {
return; // handles when parent is set to display:none (is-hidden)
return; // handles when parent is set to display:none (is-hidden)
}
let lineElemArr = linesElem.querySelectorAll(".line");
if (lineElemArr == null) {
return;
}
if (linesElem.clientHeight == 0) {
return; // when linesElem is collapsed (or display:none)
return; // when linesElem is collapsed (or display:none)
}
let containerTop = linesElem.scrollTop - LinesVisiblePadding;
let containerBot = linesElem.scrollTop + linesElem.clientHeight + LinesVisiblePadding;
let newMap = new Map<string, boolean>();
// console.log("computevismap", linesElem.scrollTop, linesElem.clientHeight, containerTop + "-" + containerBot);
for (let i=0; i<lineElemArr.length; i++) {
for (let i = 0; i < lineElemArr.length; i++) {
let lineElem = lineElemArr[i];
let lineTop = lineElem.offsetTop;
let lineBot = lineElem.offsetTop + lineElem.offsetHeight;
@ -134,7 +146,7 @@ class LinesView extends React.Component<{screen : ScreenInterface, width : numbe
isVis = true;
}
if (lineBot >= containerTop && lineBot <= containerBot) {
isVis = true
isVis = true;
}
// console.log("line", lineElem.dataset.linenum, "top=" + lineTop, "bot=" + lineTop, isVis);
let lineNumInt = parseInt(lineElem.dataset.linenum);
@ -146,7 +158,7 @@ class LinesView extends React.Component<{screen : ScreenInterface, width : numbe
for (let [k, v] of newMap) {
let oldVal = this.visibleMap.get(k);
if (oldVal == null) {
oldVal = mobx.observable.box(v, {name: "lines-vis-map"});
oldVal = mobx.observable.box(v, { name: "lines-vis-map" });
this.visibleMap.set(k, oldVal);
}
if (oldVal.get() != v) {
@ -161,11 +173,11 @@ class LinesView extends React.Component<{screen : ScreenInterface, width : numbe
})();
}
printVisMap() : void {
printVisMap(): void {
let visMap = this.visibleMap;
let lines = this.props.lines;
let visLines : string[] = [];
for (let i=0; i<lines.length; i++) {
let visLines: string[] = [];
for (let i = 0; i < lines.length; i++) {
let linenum = String(lines[i].linenum);
if (visMap.get(linenum).get()) {
visLines.push(linenum);
@ -174,18 +186,18 @@ class LinesView extends React.Component<{screen : ScreenInterface, width : numbe
console.log("vislines", visLines);
}
restoreAnchorOffset(reason : string) : void {
let {lines} = this.props;
restoreAnchorOffset(reason: string): void {
let { lines } = this.props;
let linesElem = this.linesRef.current;
if (linesElem == null) {
return;
}
let anchor = this.getAnchor();
let anchorElem = linesElem.querySelector(sprintf(".line[data-linenum=\"%d\"]", anchor.anchorLine));
let anchorElem = linesElem.querySelector(sprintf('.line[data-linenum="%d"]', anchor.anchorLine));
if (anchorElem == null) {
return;
}
let isLastLine = (anchor.anchorIndex == lines.length-1);
let isLastLine = anchor.anchorIndex == lines.length - 1;
let scrollTop = linesElem.scrollTop;
let height = linesElem.clientHeight;
let containerBottom = scrollTop + height;
@ -203,17 +215,16 @@ class LinesView extends React.Component<{screen : ScreenInterface, width : numbe
}
}
componentDidMount() : void {
let {screen, lines} = this.props;
componentDidMount(): void {
let { screen, lines } = this.props;
let linesElem = this.linesRef.current;
let anchor = this.getAnchor();
if (anchor.anchorIndex == lines.length-1) {
if (anchor.anchorIndex == lines.length - 1) {
if (linesElem != null) {
linesElem.scrollTop = linesElem.scrollHeight;
}
this.computeAnchorLine();
}
else {
} else {
this.restoreAnchorOffset("re-mount");
}
this.lastSelectedLine = screen.getSelectedLine();
@ -225,21 +236,21 @@ class LinesView extends React.Component<{screen : ScreenInterface, width : numbe
this.rszObs.observe(linesElem);
}
mobx.action(() => {
this.staticRender.set(false)
this.staticRender.set(false);
this.computeVisibleMap();
})();
}
getLineElem(lineNum : number) : HTMLElement {
getLineElem(lineNum: number): HTMLElement {
let linesElem = this.linesRef.current;
if (linesElem == null) {
return null;
}
let elem = linesElem.querySelector(sprintf(".line[data-linenum=\"%d\"]", lineNum));
let elem = linesElem.querySelector(sprintf('.line[data-linenum="%d"]', lineNum));
return elem;
}
getLineViewInfo(lineNum : number) : {height: number, topOffset: number, botOffset: number, anchorOffset: number} {
getLineViewInfo(lineNum: number): { height: number; topOffset: number; botOffset: number; anchorOffset: number } {
let linesElem = this.linesRef.current;
if (linesElem == null) {
return null;
@ -260,22 +271,20 @@ class LinesView extends React.Component<{screen : ScreenInterface, width : numbe
let lineBot = lineElem.offsetTop + lineElem.offsetHeight;
if (lineTop < containerTop) {
rtn.topOffset = lineTop - containerTop;
}
else if (lineTop > containerBot) {
} else if (lineTop > containerBot) {
rtn.topOffset = lineTop - containerBot;
}
if (lineBot < containerTop) {
rtn.botOffset = lineBot - containerTop;
}
else if (lineBot > containerBot) {
} else if (lineBot > containerBot) {
rtn.botOffset = lineBot - containerBot;
}
rtn.anchorOffset = containerBot - lineBot;
return rtn;
}
updateSelectedLine() : void {
let {screen, lines} = this.props;
updateSelectedLine(): void {
let { screen, lines } = this.props;
let linesElem = this.linesRef.current;
if (linesElem == null) {
return null;
@ -288,45 +297,41 @@ class LinesView extends React.Component<{screen : ScreenInterface, width : numbe
this.setLineVisible(newLine, true);
// console.log("update selected line", this.lastSelectedLine, "=>", newLine, sprintf("anchor=%d:%d", screen.anchorLine, screen.anchorOffset));
let viewInfo = this.getLineViewInfo(newLine);
let isFirst = (lidx.index == 0);
let isLast = (lidx.index == lines.length-1);
let offsetDelta = (isLast ? 10 : (isFirst ? -28 : 0));
let isFirst = lidx.index == 0;
let isLast = lidx.index == lines.length - 1;
let offsetDelta = isLast ? 10 : isFirst ? -28 : 0;
if (viewInfo == null) {
screen.setAnchorFields(newLine, 0+offsetDelta, "updateSelectedLine");
}
else if (viewInfo.botOffset > 0) {
screen.setAnchorFields(newLine, 0 + offsetDelta, "updateSelectedLine");
} else if (viewInfo.botOffset > 0) {
linesElem.scrollTop = linesElem.scrollTop + viewInfo.botOffset + offsetDelta;
this.ignoreNextScroll = true;
screen.setAnchorFields(newLine, offsetDelta, "updateSelectedLine");
}
else if (viewInfo.topOffset < 0) {
} else if (viewInfo.topOffset < 0) {
linesElem.scrollTop = linesElem.scrollTop + viewInfo.topOffset + offsetDelta;
this.ignoreNextScroll = true;
let newOffset = linesElem.clientHeight - viewInfo.height;
screen.setAnchorFields(newLine, newOffset, "updateSelectedLine");
}
else {
} else {
screen.setAnchorFields(newLine, viewInfo.anchorOffset, "updateSelectedLine");
}
// console.log("new anchor", screen.getAnchorStr());
}
setLineVisible(lineNum : number, vis : boolean) : void {
setLineVisible(lineNum: number, vis: boolean): void {
mobx.action(() => {
let key = String(lineNum);
let visObj = this.visibleMap.get(key);
if (visObj == null) {
visObj = mobx.observable.box(true, {name: "lines-vis-map"});
visObj = mobx.observable.box(true, { name: "lines-vis-map" });
this.visibleMap.set(key, visObj);
}
else {
} else {
visObj.set(true);
}
})();
}
componentDidUpdate(prevProps, prevState, snapshot) : void {
let {screen, lines} = this.props;
componentDidUpdate(prevProps, prevState, snapshot): void {
let { screen, lines } = this.props;
if (screen.getSelectedLine() != this.lastSelectedLine) {
this.updateSelectedLine();
this.lastSelectedLine = screen.getSelectedLine();
@ -335,13 +340,13 @@ class LinesView extends React.Component<{screen : ScreenInterface, width : numbe
}
}
componentWillUnmount() : void {
componentWillUnmount(): void {
if (this.rszObs != null) {
this.rszObs.disconnect();
}
}
handleResize(entries : any) {
handleResize(entries: any) {
let linesElem = this.linesRef.current;
if (linesElem == null) {
return;
@ -359,7 +364,7 @@ class LinesView extends React.Component<{screen : ScreenInterface, width : numbe
}
@boundMethod
onHeightChange(lineNum : number, newHeight : number, oldHeight : number) : void {
onHeightChange(lineNum: number, newHeight: number, oldHeight: number): void {
if (oldHeight == null) {
return;
}
@ -368,81 +373,87 @@ class LinesView extends React.Component<{screen : ScreenInterface, width : numbe
this.computeVisibleMap_debounced();
}
hasTopBorder(lines : T.LineInterface[], idx : number) : boolean {
hasTopBorder(lines: T.LineInterface[], idx: number): boolean {
if (idx == 0) {
return false;
}
let curLineNumStr = String(lines[idx].linenum);
let prevLineNumStr = String(lines[idx-1].linenum);
let prevLineNumStr = String(lines[idx - 1].linenum);
return !this.collapsedMap.get(curLineNumStr).get() || !this.collapsedMap.get(prevLineNumStr).get();
}
getDateSepStr(lines : T.LineInterface[], idx : number, prevStr : string, todayStr : string, yesterdayStr : string) : string {
getDateSepStr(
lines: T.LineInterface[],
idx: number,
prevStr: string,
todayStr: string,
yesterdayStr: string
): string {
let curLineDate = new Date(lines[idx].ts);
let curLineFormat = dayjs(curLineDate).format("ddd YYYY-MM-DD");
if (idx == 0) {
return ;
return;
}
let prevLineDate = new Date(lines[idx].ts);
let prevLineFormat = dayjs(prevLineDate).format("YYYY-MM-DD");
return null;
}
findClosestLineIndex(lineNum : number) : {line : T.LineInterface, index : number} {
let {lines} = this.props;
findClosestLineIndex(lineNum: number): { line: T.LineInterface; index: number } {
let { lines } = this.props;
if (lines.length == 0) {
throw new Error("invalid lines, cannot have 0 length in LinesView");
}
if (lineNum == null || lineNum == 0) {
return {line: lines[lines.length-1], index: lines.length-1};
return { line: lines[lines.length - 1], index: lines.length - 1 };
}
// todo: bsearch
// lines is sorted by linenum
for (let idx=0; idx<lines.length; idx++) {
for (let idx = 0; idx < lines.length; idx++) {
let line = lines[idx];
if (line.linenum >= lineNum) {
return {line: line, index: idx};
return { line: line, index: idx };
}
}
return {line: lines[lines.length-1], index: lines.length-1};
return { line: lines[lines.length - 1], index: lines.length - 1 };
}
getAnchor() : {anchorLine : number, anchorOffset : number, anchorIndex : number} {
let {screen, lines} = this.props;
getAnchor(): { anchorLine: number; anchorOffset: number; anchorIndex: number } {
let { screen, lines } = this.props;
let anchor = screen.getAnchor();
if (anchor.anchorLine == null || anchor.anchorLine == 0) {
return {anchorLine: lines[lines.length-1].linenum, anchorOffset: 0, anchorIndex: lines.length-1};
return { anchorLine: lines[lines.length - 1].linenum, anchorOffset: 0, anchorIndex: lines.length - 1 };
}
let lidx = this.findClosestLineIndex(anchor.anchorLine);
if (lidx.line.linenum == anchor.anchorLine) {
return {anchorLine: anchor.anchorLine, anchorOffset: anchor.anchorOffset, anchorIndex: lidx.index};
return { anchorLine: anchor.anchorLine, anchorOffset: anchor.anchorOffset, anchorIndex: lidx.index };
}
return {anchorLine: lidx.line.linenum, anchorOffset: 0, anchorIndex: lidx.index};
return { anchorLine: lidx.line.linenum, anchorOffset: 0, anchorIndex: lidx.index };
}
render() {
let {screen, width, lines, renderMode} = this.props;
let selectedLine = screen.getSelectedLine(); // for re-rendering
let line : T.LineInterface = null;
for (let i=0; i<lines.length; i++) {
let { screen, width, lines, renderMode } = this.props;
let selectedLine = screen.getSelectedLine(); // for re-rendering
let line: T.LineInterface = null;
for (let i = 0; i < lines.length; i++) {
let key = String(lines[i].linenum);
let visObs = this.visibleMap.get(key);
if (visObs == null) {
this.visibleMap.set(key, mobx.observable.box(false, {name: "lines-vis-map"}));
this.visibleMap.set(key, mobx.observable.box(false, { name: "lines-vis-map" }));
}
let collObs = this.collapsedMap.get(key);
if (collObs == null) {
this.collapsedMap.set(key, mobx.observable.box(false, {name: "lines-collapsed-map"}));
this.collapsedMap.set(key, mobx.observable.box(false, { name: "lines-collapsed-map" }));
}
}
let lineElements : any = [];
let lineElements: any = [];
let todayStr = util.getTodayStr();
let yesterdayStr = util.getYesterdayStr();
let prevDateStr : string = null;
let prevDateStr: string = null;
let anchor = this.getAnchor();
let startIdx = util.boundInt(anchor.anchorIndex-50, 0, lines.length-1);
let endIdx = util.boundInt(anchor.anchorIndex+50, 0, lines.length-1);
let startIdx = util.boundInt(anchor.anchorIndex - 50, 0, lines.length - 1);
let endIdx = util.boundInt(anchor.anchorIndex + 50, 0, lines.length - 1);
// console.log("render", anchor, "[" + startIdx + "," + endIdx + "]");
for (let idx=startIdx; idx <= endIdx; idx++) {
for (let idx = startIdx; idx <= endIdx; idx++) {
let line = lines[idx];
let lineNumStr = String(line.linenum);
let dateSepStr = null;
@ -452,10 +463,14 @@ class LinesView extends React.Component<{screen : ScreenInterface, width : numbe
}
prevDateStr = curDateStr;
if (dateSepStr != null) {
let sepElem = (<div key={"sep-" + line.lineid} className="line-sep">{dateSepStr}</div>);
let sepElem = (
<div key={"sep-" + line.lineid} className="line-sep">
{dateSepStr}
</div>
);
lineElements.push(sepElem);
}
let topBorder = (dateSepStr == null) && this.hasTopBorder(lines, idx);
let topBorder = dateSepStr == null && this.hasTopBorder(lines, idx);
let lineProps = {
key: line.lineid,
line: line,
@ -471,10 +486,7 @@ class LinesView extends React.Component<{screen : ScreenInterface, width : numbe
// let lineElem = <Line key={line.lineid} line={line} screen={screen} width={width} visible={this.visibleMap.get(lineNumStr)} staticRender={this.staticRender.get()} onHeightChange={this.onHeightChange} overrideCollapsed={this.collapsedMap.get(lineNumStr)} topBorder={topBorder} renderMode={renderMode}/>;
lineElements.push(lineElem);
}
let linesClass = cn(
"lines",
(renderMode == "normal" ? "lines-expanded" : "lines-collapsed"),
);
let linesClass = cn("lines", renderMode == "normal" ? "lines-expanded" : "lines-collapsed");
return (
<div key="lines" className={linesClass} onScroll={this.scrollHandler} ref={this.linesRef}>
<div className="lines-spacer"></div>
@ -484,4 +496,4 @@ class LinesView extends React.Component<{screen : ScreenInterface, width : numbe
}
}
export {LinesView};
export { LinesView };

View File

@ -1,18 +1,18 @@
import dayjs from "dayjs";
import localizedFormat from 'dayjs/plugin/localizedFormat';
import {isBlank, getDateStr} from "./util";
import {LineType, WebLine, RendererContext} from "./types";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { isBlank, getDateStr } from "./util";
import { LineType, WebLine, RendererContext } from "./types";
dayjs.extend(localizedFormat)
dayjs.extend(localizedFormat);
function getRendererType(line : LineType|WebLine) : "terminal" | "plugin" {
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 {
function getLineDateStr(todayDate: string, yesterdayDate: string, ts: number): string {
let lineDate = new Date(ts);
let dateStr = getDateStr(lineDate);
if (dateStr == todayDate) {
@ -24,36 +24,34 @@ function getLineDateStr(todayDate : string, yesterdayDate : string, ts : number)
return dateStr;
}
function getLineDateTimeStr(ts : number) : string {
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);
} 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 "Yesterday " + dayjs(lineDate).format("LTS");
}
return dayjs(lineDate).format("ddd L LTS");
}
else {
} else {
return dayjs(lineDate).format("LTS");
}
}
function isMultiLineCmdText(cmdText : string) : boolean {
function isMultiLineCmdText(cmdText: string): boolean {
if (cmdText == null) {
return false;
}
cmdText = cmdText.trim();
let nlIdx = cmdText.indexOf("\n");
return (nlIdx != -1);
return nlIdx != -1;
}
function getFullCmdText(cmdText : string) {
function getFullCmdText(cmdText: string) {
if (cmdText == null) {
return "(none)";
}
@ -61,7 +59,7 @@ function getFullCmdText(cmdText : string) {
return cmdText;
}
function getSingleLineCmdText(cmdText : string) {
function getSingleLineCmdText(cmdText: string) {
if (cmdText == null) {
return "(none)";
}
@ -73,7 +71,7 @@ function getSingleLineCmdText(cmdText : string) {
return cmdText;
}
function getRendererContext(line : LineType) : RendererContext {
function getRendererContext(line: LineType): RendererContext {
return {
screenId: line.screenid,
lineId: line.lineid,
@ -81,7 +79,7 @@ function getRendererContext(line : LineType) : RendererContext {
};
}
function getWebRendererContext(line : WebLine) : RendererContext {
function getWebRendererContext(line: WebLine): RendererContext {
return {
screenId: line.screenid,
lineId: line.lineid,
@ -89,8 +87,18 @@ function getWebRendererContext(line : WebLine) : RendererContext {
};
}
function cmdStatusIsRunning(status : string) : boolean {
function cmdStatusIsRunning(status: string): boolean {
return status == "running" || status == "detached";
}
export {getRendererType, getLineDateStr, getLineDateTimeStr, isMultiLineCmdText, getFullCmdText, getSingleLineCmdText, getRendererContext, getWebRendererContext, cmdStatusIsRunning};
export {
getRendererType,
getLineDateStr,
getLineDateTimeStr,
isMultiLineCmdText,
getFullCmdText,
getSingleLineCmdText,
getRendererContext,
getWebRendererContext,
cmdStatusIsRunning,
};

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@
z-index: 105;
.modal-background {
background-color: rgba(10,10,10,.6);
background-color: rgba(10, 10, 10, 0.6);
}
}
@ -35,7 +35,7 @@
background-color: black;
height: 250px;
overflow: auto;
.ws-logline {
.mono-font(12px);
color: @term-white;
@ -66,7 +66,7 @@
.modal.alert-modal {
z-index: 205;
footer {
justify-content: center;
@ -171,7 +171,7 @@
.modal-content {
min-width: 850px;
}
.inner-content {
display: flex;
flex-direction: row;
@ -209,7 +209,7 @@
color: white;
}
}
&:first-child {
border-top: 0;
}
@ -291,9 +291,8 @@
i.fa-check {
color: @term-green;
}
&.is-ok {
}
.message-row {
@ -316,7 +315,7 @@
.update-auth-button {
visibility: hidden;
}
&:hover {
.update-auth-button {
visibility: visible;
@ -328,7 +327,8 @@
}
}
&.auth-editing, &.create-remote {
&.auth-editing,
&.create-remote {
.settings-field.align-top {
align-items: flex-start;
@ -366,11 +366,11 @@
.settings-input input {
width: 250px;
}
.dropdown .dropdown-trigger button {
font-size: 12px;
}
.dropdown .dropdown-item {
font-size: 12px;
padding: 5px 5px 5px 12px;
@ -525,7 +525,7 @@
height: 20px;
}
}
input {
padding: 4px;
border-radius: 3px;
@ -534,7 +534,7 @@
.control {
font-size: 12px;
}
.tab-color-icon.color-green {
color: desaturate(@tab-green, 30%);
}

View File

@ -1,25 +1,25 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import {sprintf} from "sprintf-js";
import {boundMethod} from "autobind-decorator";
import {If, For, When, Otherwise, Choose} from "tsx-control-statements/components";
import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import { If, For, When, Otherwise, Choose } from "tsx-control-statements/components";
import cn from "classnames";
import {GlobalModel, GlobalCommandRunner} from "./model";
import { GlobalModel, GlobalCommandRunner } from "./model";
import * as util from "./util";
type OV<V> = mobx.IObservableValue<V>;
class TosModal extends React.Component<{}, {}> {
@boundMethod
acceptTos() : void {
acceptTos(): void {
GlobalCommandRunner.clientAcceptTos();
}
render() {
return (
<div className={cn("modal tos-modal prompt-modal is-active")}>
<div className="modal-background"/>
<div className="modal-background" />
<div className="modal-content">
<header>
<div className="modal-title">Welcome to [prompt]</div>
@ -29,20 +29,29 @@ class TosModal extends React.Component<{}, {}> {
<p>Thank you for downloading Prompt!</p>
<p>
Prompt is a new terminal designed to help you save time and organize your command life.
Prompt is currently in beta. If you'd like to give feedback, run into problems, have questions,
or need help, please join the Prompt <a target="_blank" href={util.makeExternLink("https://discord.gg/XfvZ334gwU")}>discord&nbsp;server</a>.
Prompt is currently in beta. If you'd like to give feedback, run into problems, have
questions, or need help, please join the Prompt{" "}
<a target="_blank" href={util.makeExternLink("https://discord.gg/XfvZ334gwU")}>
discord&nbsp;server
</a>
.
</p>
<p>
Prompt is free to use, no email or registration required (unless you're using the cloud features).
Prompt is free to use, no email or registration required (unless you're using the cloud
features).
</p>
<p>
<a target="_blank" href={util.makeExternLink("https://www.getprompt.dev/tos.html")}>Full Terms of Service</a>
<a target="_blank" href={util.makeExternLink("https://www.getprompt.dev/tos.html")}>
Full Terms of Service
</a>
</p>
</div>
</div>
<footer>
<div className="flex-spacer"/>
<div onClick={this.acceptTos} className="button is-prompt-green is-outlined is-small">Accept Terms of Service</div>
<div className="flex-spacer" />
<div onClick={this.acceptTos} className="button is-prompt-green is-outlined is-small">
Accept Terms of Service
</div>
</footer>
</div>
</div>
@ -50,4 +59,4 @@ class TosModal extends React.Component<{}, {}> {
}
}
export {TosModal};
export { TosModal };

File diff suppressed because it is too large Load Diff

View File

@ -8,103 +8,99 @@ 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/*"],
simpleComponent: SimpleImageRenderer,
name: "image",
rendererType: "simple",
heightType: "pixels",
dataType: "blob",
collapseType: "hide",
globalCss: null,
mimeTypes: ["image/*"],
simpleComponent: SimpleImageRenderer,
};
const MarkdownPlugin: RendererPluginType = {
name: "markdown",
rendererType: "simple",
heightType: "pixels",
dataType: "blob",
collapseType: "hide",
globalCss: null,
mimeTypes: ["text/markdown"],
simpleComponent: SimpleMarkdownRenderer,
name: "markdown",
rendererType: "simple",
heightType: "pixels",
dataType: "blob",
collapseType: "hide",
globalCss: null,
mimeTypes: ["text/markdown"],
simpleComponent: SimpleMarkdownRenderer,
};
const JsonPlugin: RendererPluginType = {
name: "json",
rendererType: "simple",
heightType: "pixels",
dataType: "blob",
collapseType: "hide",
globalCss: null,
mimeTypes: ["application/json"],
simpleComponent: SimpleJsonRenderer,
name: "json",
rendererType: "simple",
heightType: "pixels",
dataType: "blob",
collapseType: "hide",
globalCss: null,
mimeTypes: ["application/json"],
simpleComponent: SimpleJsonRenderer,
};
const CodePlugin: RendererPluginType = {
name: "code",
rendererType: "simple",
heightType: "pixels",
dataType: "blob",
collapseType: "hide",
globalCss: null,
mimeTypes: ["text/plain"],
simpleComponent: SourceCodeRenderer,
name: "code",
rendererType: "simple",
heightType: "pixels",
dataType: "blob",
collapseType: "hide",
globalCss: null,
mimeTypes: ["text/plain"],
simpleComponent: SourceCodeRenderer,
};
const OpenAIPlugin: RendererPluginType = {
name: "openai",
rendererType: "full",
heightType: "pixels",
dataType: "model",
collapseType: "remove",
hidePrompt: true,
globalCss: null,
mimeTypes: ["application/json"],
fullComponent: OpenAIRenderer,
modelCtor: () => new OpenAIRendererModel(),
name: "openai",
rendererType: "full",
heightType: "pixels",
dataType: "model",
collapseType: "remove",
hidePrompt: true,
globalCss: null,
mimeTypes: ["application/json"],
fullComponent: OpenAIRenderer,
modelCtor: () => new OpenAIRendererModel(),
};
class PluginModelClass {
rendererPlugins: RendererPluginType[] = [];
rendererPlugins: RendererPluginType[] = [];
registerRendererPlugin(plugin: RendererPluginType) {
if (isBlank(plugin.name)) {
throw new Error("invalid plugin, no name");
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);
}
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 < this.rendererPlugins.length; i++) {
let plugin = this.rendererPlugins[i];
if (plugin.name == name) {
return plugin;
}
getRendererPluginByName(name: string): RendererPluginType {
for (let i = 0; i < this.rendererPlugins.length; i++) {
let plugin = this.rendererPlugins[i];
if (plugin.name == name) {
return plugin;
}
}
return null;
}
return null;
}
}
let PluginModel: PluginModelClass = null;
if ((window as any).PluginModel == null) {
PluginModel = new PluginModelClass();
PluginModel.registerRendererPlugin(ImagePlugin);
PluginModel.registerRendererPlugin(MarkdownPlugin);
PluginModel.registerRendererPlugin(JsonPlugin);
PluginModel.registerRendererPlugin(CodePlugin);
PluginModel.registerRendererPlugin(OpenAIPlugin);
(window as any).PluginModel = PluginModel;
PluginModel = new PluginModelClass();
PluginModel.registerRendererPlugin(ImagePlugin);
PluginModel.registerRendererPlugin(MarkdownPlugin);
PluginModel.registerRendererPlugin(JsonPlugin);
PluginModel.registerRendererPlugin(CodePlugin);
PluginModel.registerRendererPlugin(OpenAIPlugin);
(window as any).PluginModel = PluginModel;
}
export { PluginModel };

View File

@ -1,4 +1,4 @@
let {contextBridge, ipcRenderer} = require("electron");
let { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("api", {
getId: () => ipcRenderer.sendSync("get-id"),

View File

@ -47,259 +47,259 @@
// global settings / overrides
:root {
--fa-style-family: "Font Awesome 6 Sharp";
--fa-style-family: "Font Awesome 6 Sharp";
}
html,
body,
#app,
#main {
background-color: #000;
height: 100vh;
background-color: #000;
height: 100vh;
}
.content {
a:hover {
color: #485fc7;
}
a:hover {
color: #485fc7;
}
}
body {
overflow: hidden;
overflow: hidden;
}
body::-webkit-scrollbar {
display: none;
display: none;
}
*::-webkit-scrollbar {
background-color: #777;
width: 5px;
height: 5px;
background-color: #777;
width: 5px;
height: 5px;
}
*::-webkit-scrollbar-thumb {
background: white;
background: white;
}
input[type="checkbox"] {
cursor: pointer;
cursor: pointer;
}
// main layout
#main {
height: 100vh;
display: flex;
flex-direction: column;
.main-content {
height: 100vh;
display: flex;
flex-direction: row;
background-color: black;
height: 100%;
flex-direction: column;
.session-view,
.history-view,
.bookmarks-view {
flex-grow: 1;
display: flex;
flex-direction: column;
min-width: 300px;
position: relative;
&.is-hidden {
display: none;
}
}
.screen-view {
flex-grow: 1;
border-right: 1px solid #ccc;
position: relative;
}
.window-view {
display: flex;
flex-direction: column;
position: relative;
.rendermode-tag {
position: absolute;
top: 0;
right: 0;
background-color: rgba(78, 154, 6, 0.65);
color: black;
padding: 2px 8px 2px 4px;
border-bottom-left-radius: 5px;
z-index: 10;
font-size: 12px;
&.is-active {
color: #ccc;
}
.render-mode {
padding-top: 2px;
font-size: 16px;
position: relative;
cursor: pointer;
color: #ccc;
&:hover {
color: white;
}
}
}
.share-tag {
color: #ccc;
position: absolute;
top: 0;
left: 40%;
background-color: darken(rgb(0, 177, 10), 20%);
padding: 2px 8px 2px 4px;
z-index: 11;
font-size: 12px;
/* border-radius: 0 0 5px 5px; */
opacity: 0.8;
.main-content {
display: flex;
flex-direction: column;
.share-tag-link {
margin-top: 10px;
display: none;
}
&:hover {
.share-tag-title {
font-size: 14px;
font-weight: bold;
}
opacity: 1;
padding: 20px;
width: 250px;
border: 1px solid #ccc;
border-top: 0;
.share-tag-link {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
}
}
}
.window-empty {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 10px;
flex-direction: row;
background-color: black;
height: 100%;
color: #ccc;
.mono-font();
code {
background-color: black;
color: #4e9a06;
.session-view,
.history-view,
.bookmarks-view {
flex-grow: 1;
display: flex;
flex-direction: column;
min-width: 300px;
position: relative;
&.is-hidden {
display: none;
}
}
&.should-fade {
opacity: 1;
animation: fade-in 2.5s;
.screen-view {
flex-grow: 1;
border-right: 1px solid #ccc;
position: relative;
}
.window-view {
display: flex;
flex-direction: column;
position: relative;
.rendermode-tag {
position: absolute;
top: 0;
right: 0;
background-color: rgba(78, 154, 6, 0.65);
color: black;
padding: 2px 8px 2px 4px;
border-bottom-left-radius: 5px;
z-index: 10;
font-size: 12px;
&.is-active {
color: #ccc;
}
.render-mode {
padding-top: 2px;
font-size: 16px;
position: relative;
cursor: pointer;
color: #ccc;
&:hover {
color: white;
}
}
}
.share-tag {
color: #ccc;
position: absolute;
top: 0;
left: 40%;
background-color: darken(rgb(0, 177, 10), 20%);
padding: 2px 8px 2px 4px;
z-index: 11;
font-size: 12px;
/* border-radius: 0 0 5px 5px; */
opacity: 0.8;
display: flex;
flex-direction: column;
.share-tag-link {
margin-top: 10px;
display: none;
}
&:hover {
.share-tag-title {
font-size: 14px;
font-weight: bold;
}
opacity: 1;
padding: 20px;
width: 250px;
border: 1px solid #ccc;
border-top: 0;
.share-tag-link {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
}
}
}
.window-empty {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 10px;
height: 100%;
color: #ccc;
.mono-font();
code {
background-color: black;
color: #4e9a06;
}
&.should-fade {
opacity: 1;
animation: fade-in 2.5s;
}
}
}
}
}
}
}
.remote-field .remote-status {
top: 4px;
top: 4px;
}
.image-renderer {
padding: 10px;
img {
display: block;
}
padding: 10px;
img {
display: block;
}
}
.renderer-container {
.error-container {
color: @term-red;
font-size: 14px;
padding: 5px;
}
.error-container {
color: @term-red;
font-size: 14px;
padding: 5px;
}
.scroller {
overflow: auto;
display: flex;
flex-direction: row;
}
.scroller {
overflow: auto;
display: flex;
flex-direction: row;
}
}
.renderer-container.code-renderer {
margin-top: 10px;
margin-top: 10px;
}
.renderer-container.json-renderer {
padding: 10px;
font-size: 12px;
padding: 10px;
font-size: 12px;
}
.markdown-renderer .markdown {
padding: 5px;
line-height: 1.5;
width: fit-content;
padding: 5px;
line-height: 1.5;
width: fit-content;
blockquote {
background-color: #222;
}
blockquote {
background-color: #222;
}
code {
background-color: #222;
font-size: 14px;
}
code {
background-color: #222;
font-size: 14px;
}
pre {
background-color: #222;
margin: 2px 10px 6px 10px;
padding: 4px 4px 4px 6px;
}
pre {
background-color: #222;
margin: 2px 10px 6px 10px;
padding: 4px 4px 4px 6px;
}
}
.openai-renderer {
.openai-message {
display: flex;
flex-direction: row;
justify-content: flex-start;
.openai-message {
display: flex;
flex-direction: row;
justify-content: flex-start;
.openai-role {
color: @term-bright-green;
font-weight: bold;
width: 100px;
}
.openai-role {
color: @term-bright-green;
font-weight: bold;
width: 100px;
}
.openai-role.openai-role-assistant {
color: @term-bright-white;
}
.openai-role.openai-role-assistant {
color: @term-bright-white;
}
.openai-content-user {
white-space: pre;
color: white;
}
.openai-content-user {
white-space: pre;
color: white;
}
.openai-content-assistant {
color: white;
}
.openai-content-assistant {
color: white;
}
.openai-role-error {
color: @term-bright-red;
}
.openai-role-error {
color: @term-bright-red;
}
.openai-content-error {
color: @term-bright-red;
.openai-content-error {
color: @term-bright-red;
}
}
}
}

View File

@ -1,12 +1,12 @@
import * as mobx from "mobx";
import * as React from "react";
import {createRoot} from 'react-dom/client';
import {sprintf} from "sprintf-js";
import {Terminal} from 'xterm';
import {Main} from "./main";
import {GlobalModel} from "./model";
import {v4 as uuidv4} from "uuid";
import {loadFonts} from "./util";
import { createRoot } from "react-dom/client";
import { sprintf } from "sprintf-js";
import { Terminal } from "xterm";
import { Main } from "./main";
import { GlobalModel } from "./model";
import { v4 as uuidv4 } from "uuid";
import { loadFonts } from "./util";
// @ts-ignore
let VERSION = __PROMPT_VERSION__;
@ -22,8 +22,7 @@ document.addEventListener("DOMContentLoaded", () => {
let isFontLoaded = document.fonts.check("12px 'JetBrains Mono'");
if (isFontLoaded) {
root.render(reactElem);
}
else {
} else {
document.fonts.ready.then(() => {
root.render(reactElem);
});
@ -33,4 +32,4 @@ document.addEventListener("DOMContentLoaded", () => {
(window as any).mobx = mobx;
(window as any).sprintf = sprintf;
console.log("PROMPT", VERSION, BUILD)
console.log("PROMPT", VERSION, BUILD);

View File

@ -1,23 +1,23 @@
import * as mobx from "mobx";
import {incObs} from "./util";
import { incObs } from "./util";
type OV<V> = mobx.IObservableValue<V>;
type OArr<V> = mobx.IObservableArray<V>;
type OMap<K,V> = mobx.ObservableMap<K,V>;
type OMap<K, V> = mobx.ObservableMap<K, V>;
const InitialSize = 10*1024;
const InitialSize = 10 * 1024;
const IncreaseFactor = 1.5;
class PtyDataBuffer {
ptyPos : number;
dataVersion : mobx.IObservableValue<number>;
brokenData : boolean;
rawData : Uint8Array;
dataSize : number;
ptyPos: number;
dataVersion: mobx.IObservableValue<number>;
brokenData: boolean;
rawData: Uint8Array;
dataSize: number;
constructor() {
this.ptyPos = 0;
this.dataVersion = mobx.observable.box(0, {name: "dataVersion"});
this.dataVersion = mobx.observable.box(0, { name: "dataVersion" });
this._resetData();
}
@ -27,15 +27,15 @@ class PtyDataBuffer {
this.brokenData = false;
}
reset() : void {
reset(): void {
this._resetData();
}
getData() : Uint8Array {
getData(): Uint8Array {
return this.rawData.slice(0, this.dataSize);
}
_growArray(minSize : number) : void {
_growArray(minSize: number): void {
let newSize = Math.round(this.rawData.length * IncreaseFactor);
if (newSize < minSize) {
newSize = minSize;
@ -45,7 +45,7 @@ class PtyDataBuffer {
this.rawData = newData;
}
receiveData(pos : number, data : Uint8Array, reason? : string) : void {
receiveData(pos: number, data: Uint8Array, reason?: string): void {
if (pos != this.dataSize) {
this.brokenData = true;
return;
@ -62,21 +62,21 @@ class PtyDataBuffer {
const NewLineCharCode = "\n".charCodeAt(0);
class PacketDataBuffer extends PtyDataBuffer {
parsePos : number;
callback : (any) => void;
parsePos: number;
callback: (any) => void;
constructor(callback : (any) => void) {
constructor(callback: (any) => void) {
super();
this.parsePos = 0;
this.callback = callback;
}
reset() : void {
reset(): void {
super.reset();
this.parsePos = 0;
}
processLine(line : string) {
processLine(line: string) {
if (line.length == 0) {
return;
}
@ -97,11 +97,10 @@ class PacketDataBuffer extends PtyDataBuffer {
console.log("invalid line packet", line);
}
}
let packet : any = null;
let packet: any = null;
try {
packet = JSON.parse(packetStr);
}
catch (e) {
} catch (e) {
console.log("invalid line packet (bad json)", line, e);
return;
}
@ -111,22 +110,24 @@ class PacketDataBuffer extends PtyDataBuffer {
}
parseData() {
for (let i=this.parsePos; i<this.dataSize; i++) {
for (let i = this.parsePos; i < this.dataSize; i++) {
let ch = this.rawData[i];
if (ch == NewLineCharCode) {
// line does *not* include the newline
let line = (new TextDecoder()).decode(new Uint8Array(this.rawData.buffer, this.parsePos, i-this.parsePos));
this.parsePos = i+1;
let line = new TextDecoder().decode(
new Uint8Array(this.rawData.buffer, this.parsePos, i - this.parsePos)
);
this.parsePos = i + 1;
this.processLine(line);
}
}
return;
}
receiveData(pos : number, data : Uint8Array, reason? : string) : void {
receiveData(pos: number, data: Uint8Array, reason?: string): void {
super.receiveData(pos, data, reason);
this.parseData();
}
}
export {PtyDataBuffer, PacketDataBuffer};
export { PtyDataBuffer, PacketDataBuffer };

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +1,19 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import {sprintf} from "sprintf-js";
import {boundMethod} from "autobind-decorator";
import {If, For, When, Otherwise, Choose} from "tsx-control-statements/components";
import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import { If, For, When, Otherwise, Choose } from "tsx-control-statements/components";
import cn from "classnames";
import {GlobalModel, GlobalCommandRunner, TabColors} from "./model";
import {Toggle, RemoteStatusLight, InlineSettingsTextEdit, SettingsError, InfoMessage} from "./elements";
import {LineType, RendererPluginType, ClientDataType, CommandRtnType} from "./types";
import {PluginModel} from "./plugins";
import { GlobalModel, GlobalCommandRunner, TabColors } from "./model";
import { Toggle, RemoteStatusLight, InlineSettingsTextEdit, SettingsError, InfoMessage } from "./elements";
import { LineType, RendererPluginType, ClientDataType, CommandRtnType } from "./types";
import { PluginModel } from "./plugins";
import * as util from "./util";
type OV<V> = mobx.IObservableValue<V>;
type OArr<V> = mobx.IObservableArray<V>;
type OMap<K,V> = mobx.ObservableMap<K,V>;
type OMap<K, V> = mobx.ObservableMap<K, V>;
type CV<V> = mobx.IComputedValue<V>;
const RemotePtyRows = 8;
@ -25,7 +25,6 @@ const VERSION = __PROMPT_VERSION__;
// @ts-ignore
const BUILD = __PROMPT_BUILD__;
const ScreenDeleteMessage = `
Are you sure you want to delete this screen/tab?
@ -48,7 +47,7 @@ const WebStopShareConfirmMarkdown = `
Are you sure you want to stop web-sharing this screen?
`.trim();
function commandRtnHandler(prtn : Promise<CommandRtnType>, errorMessage : OV<string>) {
function commandRtnHandler(prtn: Promise<CommandRtnType>, errorMessage: OV<string>) {
prtn.then((crtn) => {
if (crtn.success) {
return;
@ -60,29 +59,29 @@ function commandRtnHandler(prtn : Promise<CommandRtnType>, errorMessage : OV<str
}
@mobxReact.observer
class ScreenSettingsModal extends React.Component<{sessionId : string, screenId : string}, {}> {
shareCopied : OV<boolean> = mobx.observable.box(false, {name: "ScreenSettings-shareCopied"});
errorMessage : OV<string> = mobx.observable.box(null, {name: "ScreenSettings-errorMessage"});
class ScreenSettingsModal extends React.Component<{ sessionId: string; screenId: string }, {}> {
shareCopied: OV<boolean> = mobx.observable.box(false, { name: "ScreenSettings-shareCopied" });
errorMessage: OV<string> = mobx.observable.box(null, { name: "ScreenSettings-errorMessage" });
constructor(props : any) {
constructor(props: any) {
super(props);
let {sessionId, screenId} = props;
let { sessionId, screenId } = props;
let screen = GlobalModel.getScreenById(sessionId, screenId);
if (screen == null) {
return;
}
}
@boundMethod
closeModal() : void {
closeModal(): void {
mobx.action(() => {
GlobalModel.screenSettingsModal.set(null);
})();
}
@boundMethod
selectTabColor(color : string) : void {
let {sessionId, screenId} = this.props;
selectTabColor(color: string): void {
let { sessionId, screenId } = this.props;
let screen = GlobalModel.getScreenById(sessionId, screenId);
if (screen == null) {
return;
@ -90,13 +89,13 @@ class ScreenSettingsModal extends React.Component<{sessionId : string, screenId
if (screen.getTabColor() == color) {
return;
}
let prtn = GlobalCommandRunner.screenSetSettings(this.props.screenId, {tabcolor: color}, false);
let prtn = GlobalCommandRunner.screenSetSettings(this.props.screenId, { tabcolor: color }, false);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
handleChangeArchived(val : boolean) : void {
let {sessionId, screenId} = this.props;
handleChangeArchived(val: boolean): void {
let { sessionId, screenId } = this.props;
let screen = GlobalModel.getScreenById(sessionId, screenId);
if (screen == null) {
return;
@ -109,8 +108,8 @@ class ScreenSettingsModal extends React.Component<{sessionId : string, screenId
}
@boundMethod
handleChangeWebShare(val : boolean) : void {
let {sessionId, screenId} = this.props;
handleChangeWebShare(val: boolean): void {
let { sessionId, screenId } = this.props;
let screen = GlobalModel.getScreenById(sessionId, screenId);
if (screen == null) {
return;
@ -118,8 +117,8 @@ class ScreenSettingsModal extends React.Component<{sessionId : string, screenId
if (screen.isWebShared() == val) {
return;
}
let message = (val ? WebShareConfirmMarkdown : WebStopShareConfirmMarkdown);
let alertRtn = GlobalModel.showAlert({message: message, confirm: true, markdown: true});
let message = val ? WebShareConfirmMarkdown : WebStopShareConfirmMarkdown;
let alertRtn = GlobalModel.showAlert({ message: message, confirm: true, markdown: true });
alertRtn.then((result) => {
if (!result) {
return;
@ -130,8 +129,8 @@ class ScreenSettingsModal extends React.Component<{sessionId : string, screenId
}
@boundMethod
copyShareLink() : void {
let {sessionId, screenId} = this.props;
copyShareLink(): void {
let { sessionId, screenId } = this.props;
let screen = GlobalModel.getScreenById(sessionId, screenId);
if (screen == null) {
return null;
@ -148,12 +147,12 @@ class ScreenSettingsModal extends React.Component<{sessionId : string, screenId
mobx.action(() => {
this.shareCopied.set(false);
})();
}, 600)
}, 600);
}
@boundMethod
inlineUpdateName(val : string) : void {
let {sessionId, screenId} = this.props;
inlineUpdateName(val: string): void {
let { sessionId, screenId } = this.props;
let screen = GlobalModel.getScreenById(sessionId, screenId);
if (screen == null) {
return;
@ -161,13 +160,13 @@ class ScreenSettingsModal extends React.Component<{sessionId : string, screenId
if (util.isStrEq(val, screen.name.get())) {
return;
}
let prtn = GlobalCommandRunner.screenSetSettings(this.props.screenId, {name: val}, false);
let prtn = GlobalCommandRunner.screenSetSettings(this.props.screenId, { name: val }, false);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
inlineUpdateShareName(val : string) : void {
let {sessionId, screenId} = this.props;
inlineUpdateShareName(val: string): void {
let { sessionId, screenId } = this.props;
let screen = GlobalModel.getScreenById(sessionId, screenId);
if (screen == null) {
return;
@ -175,26 +174,26 @@ class ScreenSettingsModal extends React.Component<{sessionId : string, screenId
if (util.isStrEq(val, screen.getShareName())) {
return;
}
let prtn = GlobalCommandRunner.screenSetSettings(this.props.screenId, {sharename: val}, false);
let prtn = GlobalCommandRunner.screenSetSettings(this.props.screenId, { sharename: val }, false);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
dismissError() : void {
dismissError(): void {
mobx.action(() => {
this.errorMessage.set(null);
})();
}
@boundMethod
handleDeleteScreen() : void {
let {sessionId, screenId} = this.props;
handleDeleteScreen(): void {
let { sessionId, screenId } = this.props;
let screen = GlobalModel.getScreenById(sessionId, screenId);
if (screen == null) {
return;
}
let message = ScreenDeleteMessage;
let alertRtn = GlobalModel.showAlert({message: message, confirm: true, markdown: true});
let alertRtn = GlobalModel.showAlert({ message: message, confirm: true, markdown: true });
alertRtn.then((result) => {
if (!result) {
return;
@ -205,59 +204,62 @@ class ScreenSettingsModal extends React.Component<{sessionId : string, screenId
}
render() {
let {sessionId, screenId} = this.props;
let { sessionId, screenId } = this.props;
let screen = GlobalModel.getScreenById(sessionId, screenId);
if (screen == null) {
return null;
}
let color : string = null;
let color: string = null;
return (
<div className={cn("modal screen-settings-modal settings-modal prompt-modal is-active")}>
<div className="modal-background"/>
<div className="modal-background" />
<div className="modal-content">
<If condition={this.shareCopied.get()}>
<div className="copied-indicator"/>
<div className="copied-indicator" />
</If>
<header>
<div className="modal-title">screen settings ({screen.name.get()})</div>
<div className="close-icon">
<i onClick={this.closeModal} className="fa-sharp fa-solid fa-times"/>
<i onClick={this.closeModal} className="fa-sharp fa-solid fa-times" />
</div>
</header>
<div className="inner-content">
<div className="settings-field">
<div className="settings-label">
Screen Id
</div>
<div className="settings-label">Screen Id</div>
<div className="settings-input">{screen.screenId}</div>
</div>
<div className="settings-field">
<div className="settings-label">Name</div>
<div className="settings-input">
{screen.screenId}
<InlineSettingsTextEdit
placeholder="name"
text={screen.name.get() ?? "(none)"}
value={screen.name.get() ?? ""}
onChange={this.inlineUpdateName}
maxLength={50}
showIcon={true}
/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">
Name
</div>
<div className="settings-input">
<InlineSettingsTextEdit placeholder="name" text={screen.name.get() ?? "(none)"} value={screen.name.get() ?? ""} onChange={this.inlineUpdateName} maxLength={50} showIcon={true}/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">
Tab Color
</div>
<div className="settings-label">Tab Color</div>
<div className="settings-input">
<div className="tab-colors">
<div className="tab-color-cur">
<span className={cn("icon tab-color-icon", "color-" + screen.getTabColor())}>
<i className="fa-sharp fa-solid fa-square"/>
<i className="fa-sharp fa-solid fa-square" />
</span>
<span>{screen.getTabColor()}</span>
</div>
<div className="tab-color-sep">|</div>
<For each="color" of={TabColors}>
<div key={color} className="tab-color-select" onClick={() => this.selectTabColor(color)}>
<div
key={color}
className="tab-color-select"
onClick={() => this.selectTabColor(color)}
>
<span className={cn("tab-color-icon", "color-" + color)}>
<i className="fa-sharp fa-solid fa-square"/>
<i className="fa-sharp fa-solid fa-square" />
</span>
</div>
</For>
@ -268,51 +270,58 @@ class ScreenSettingsModal extends React.Component<{sessionId : string, screenId
<div className="settings-label">
<div>Archived</div>
<InfoMessage width={400}>
Archive will hide the screen tab. Commands and output will be retained in history.
Archive will hide the screen tab. Commands and output will be retained in history.
</InfoMessage>
</div>
<div className="settings-input">
<Toggle checked={screen.archived.get()} onChange={this.handleChangeArchived}/>
<Toggle checked={screen.archived.get()} onChange={this.handleChangeArchived} />
</div>
</div>
<div className="settings-field">
<div className="settings-label">
Web Sharing
</div>
<div className="settings-label">Web Sharing</div>
<div className="settings-input">
<Toggle checked={screen.isWebShared()} onChange={this.handleChangeWebShare}/>
<Toggle checked={screen.isWebShared()} onChange={this.handleChangeWebShare} />
</div>
</div>
<If condition={screen.isWebShared()}>
<div className="settings-field sub-field">
<div className="settings-label">
Share Link
</div>
<div className="settings-label">Share Link</div>
<div className="settings-input">
<a href={util.makeExternLink(screen.getWebShareUrl())} target="_blank" className="button is-prompt-green is-outlined is-small a-block">
<a
href={util.makeExternLink(screen.getWebShareUrl())}
target="_blank"
className="button is-prompt-green is-outlined is-small a-block"
>
<span>open in browser</span>
<span className="icon">
<i className="fa-sharp fa-solid fa-up-right-from-square"/>
<i className="fa-sharp fa-solid fa-up-right-from-square" />
</span>
</a>
<div className="button is-prompt-green is-outlined is-small ml-4" onClick={this.copyShareLink}>
<div
className="button is-prompt-green is-outlined is-small ml-4"
onClick={this.copyShareLink}
>
<span>copy link</span>
<span className="icon">
<i className="fa-sharp fa-solid fa-copy"/>
<i className="fa-sharp fa-solid fa-copy" />
</span>
</div>
</div>
</div>
<div className="settings-field sub-field">
<div className="settings-label">
Share Name
</div>
<div className="settings-label">Share Name</div>
<div className="settings-input">
<div className="settings-input">
<InlineSettingsTextEdit placeholder="name"
text={util.isBlank(screen.getShareName()) ? "(none)" : screen.getShareName()}
value={screen.getShareName() ?? ""}
onChange={this.inlineUpdateShareName} maxLength={150} showIcon={true}/>
<InlineSettingsTextEdit
placeholder="name"
text={
util.isBlank(screen.getShareName()) ? "(none)" : screen.getShareName()
}
value={screen.getShareName() ?? ""}
onChange={this.inlineUpdateShareName}
maxLength={150}
showIcon={true}
/>
</div>
</div>
</div>
@ -325,13 +334,20 @@ class ScreenSettingsModal extends React.Component<{sessionId : string, screenId
</InfoMessage>
</div>
<div className="settings-input">
<div onClick={this.handleDeleteScreen} className="button is-prompt-danger is-outlined is-small">Delete Screen</div>
<div
onClick={this.handleDeleteScreen}
className="button is-prompt-danger is-outlined is-small"
>
Delete Screen
</div>
</div>
</div>
<SettingsError errorMessage={this.errorMessage}/>
<SettingsError errorMessage={this.errorMessage} />
</div>
<footer>
<div onClick={this.closeModal} className="button is-prompt-green is-outlined is-small">Close</div>
<div onClick={this.closeModal} className="button is-prompt-green is-outlined is-small">
Close
</div>
</footer>
</div>
</div>
@ -340,28 +356,28 @@ class ScreenSettingsModal extends React.Component<{sessionId : string, screenId
}
@mobxReact.observer
class SessionSettingsModal extends React.Component<{sessionId : string}, {}> {
errorMessage : OV<string> = mobx.observable.box(null, {name: "ScreenSettings-errorMessage"});
constructor(props : any) {
class SessionSettingsModal extends React.Component<{ sessionId: string }, {}> {
errorMessage: OV<string> = mobx.observable.box(null, { name: "ScreenSettings-errorMessage" });
constructor(props: any) {
super(props);
let {sessionId} = props;
let { sessionId } = props;
let session = GlobalModel.getSessionById(sessionId);
if (session == null) {
return;
}
}
@boundMethod
closeModal() : void {
closeModal(): void {
mobx.action(() => {
GlobalModel.sessionSettingsModal.set(null);
})();
}
@boundMethod
handleInlineChangeName(newVal : string) : void {
let {sessionId} = this.props;
handleInlineChangeName(newVal: string): void {
let { sessionId } = this.props;
let session = GlobalModel.getSessionById(sessionId);
if (session == null) {
return;
@ -369,13 +385,13 @@ class SessionSettingsModal extends React.Component<{sessionId : string}, {}> {
if (util.isStrEq(newVal, session.name.get())) {
return;
}
let prtn = GlobalCommandRunner.sessionSetSettings(this.props.sessionId, {"name": newVal}, false);
let prtn = GlobalCommandRunner.sessionSetSettings(this.props.sessionId, { name: newVal }, false);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
handleChangeArchived(val : boolean) : void {
let {sessionId} = this.props;
handleChangeArchived(val: boolean): void {
let { sessionId } = this.props;
let session = GlobalModel.getSessionById(sessionId);
if (session == null) {
return;
@ -388,10 +404,10 @@ class SessionSettingsModal extends React.Component<{sessionId : string}, {}> {
}
@boundMethod
handleDeleteSession() : void {
let {sessionId} = this.props;
handleDeleteSession(): void {
let { sessionId } = this.props;
let message = SessionDeleteMessage;
let alertRtn = GlobalModel.showAlert({message: message, confirm: true, markdown: true});
let alertRtn = GlobalModel.showAlert({ message: message, confirm: true, markdown: true });
alertRtn.then((result) => {
if (!result) {
return;
@ -402,46 +418,52 @@ class SessionSettingsModal extends React.Component<{sessionId : string}, {}> {
}
@boundMethod
dismissError() : void {
dismissError(): void {
mobx.action(() => {
this.errorMessage.set(null);
})();
}
render() {
let {sessionId} = this.props;
let { sessionId } = this.props;
let session = GlobalModel.getSessionById(sessionId);
if (session == null) {
return null;
}
return (
<div className={cn("modal session-settings-modal settings-modal prompt-modal is-active")}>
<div className="modal-background"/>
<div className="modal-background" />
<div className="modal-content">
<header>
<div className="modal-title">session settings ({session.name.get()})</div>
<div className="close-icon">
<i onClick={this.closeModal} className="fa-sharp fa-solid fa-times"/>
<i onClick={this.closeModal} className="fa-sharp fa-solid fa-times" />
</div>
</header>
<div className="inner-content">
<div className="settings-field">
<div className="settings-label">
Name
</div>
<div className="settings-label">Name</div>
<div className="settings-input">
<InlineSettingsTextEdit placeholder="name" text={session.name.get() ?? "(none)"} value={session.name.get() ?? ""} onChange={this.handleInlineChangeName} maxLength={50} showIcon={true}/>
<InlineSettingsTextEdit
placeholder="name"
text={session.name.get() ?? "(none)"}
value={session.name.get() ?? ""}
onChange={this.handleInlineChangeName}
maxLength={50}
showIcon={true}
/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">
<div>Archived</div>
<InfoMessage width={400}>
Archive will hide the session from the active menu. Commands and output will be retained in history.
Archive will hide the session from the active menu. Commands and output will be
retained in history.
</InfoMessage>
</div>
<div className="settings-input">
<Toggle checked={session.archived.get()} onChange={this.handleChangeArchived}/>
<Toggle checked={session.archived.get()} onChange={this.handleChangeArchived} />
</div>
</div>
<div className="settings-field">
@ -452,13 +474,20 @@ class SessionSettingsModal extends React.Component<{sessionId : string}, {}> {
</InfoMessage>
</div>
<div className="settings-input">
<div onClick={this.handleDeleteSession} className="button is-prompt-danger is-outlined is-small">Delete Session</div>
<div
onClick={this.handleDeleteSession}
className="button is-prompt-danger is-outlined is-small"
>
Delete Session
</div>
</div>
</div>
<SettingsError errorMessage={this.errorMessage}/>
<SettingsError errorMessage={this.errorMessage} />
</div>
<footer>
<div onClick={this.closeModal} className="button is-prompt-green is-outlined is-small">Close</div>
<div onClick={this.closeModal} className="button is-prompt-green is-outlined is-small">
Close
</div>
</footer>
</div>
</div>
@ -467,19 +496,19 @@ class SessionSettingsModal extends React.Component<{sessionId : string}, {}> {
}
@mobxReact.observer
class LineSettingsModal extends React.Component<{linenum : number}, {}> {
rendererDropdownActive : OV<boolean> = mobx.observable.box(false, {name: "lineSettings-rendererDropdownActive"});
errorMessage : OV<string> = mobx.observable.box(null, {name: "ScreenSettings-errorMessage"});
class LineSettingsModal extends React.Component<{ linenum: number }, {}> {
rendererDropdownActive: OV<boolean> = mobx.observable.box(false, { name: "lineSettings-rendererDropdownActive" });
errorMessage: OV<string> = mobx.observable.box(null, { name: "ScreenSettings-errorMessage" });
@boundMethod
closeModal() : void {
closeModal(): void {
mobx.action(() => {
GlobalModel.lineSettingsModal.set(null);
})();
}
@boundMethod
handleChangeArchived(val : boolean) : void {
handleChangeArchived(val: boolean): void {
let line = this.getLine();
if (line == null) {
return;
@ -489,13 +518,13 @@ class LineSettingsModal extends React.Component<{linenum : number}, {}> {
}
@boundMethod
toggleRendererDropdown() : void {
toggleRendererDropdown(): void {
mobx.action(() => {
this.rendererDropdownActive.set(!this.rendererDropdownActive.get());
})();
}
getLine() : LineType {
getLine(): LineType {
let screen = GlobalModel.getActiveScreen();
if (screen == null) {
return;
@ -504,31 +533,33 @@ class LineSettingsModal extends React.Component<{linenum : number}, {}> {
}
@boundMethod
clickSetRenderer(renderer : string) : void {
clickSetRenderer(renderer: string): void {
let line = this.getLine();
if (line == null) {
return;
}
let prtn = GlobalCommandRunner.lineSet(line.lineid, {renderer: renderer});
let prtn = GlobalCommandRunner.lineSet(line.lineid, { renderer: renderer });
commandRtnHandler(prtn, this.errorMessage);
mobx.action(() => {
this.rendererDropdownActive.set(false);
})();
}
renderRendererDropdown() : any {
renderRendererDropdown(): any {
let line = this.getLine();
if (line == null) {
return null;
}
let plugins = PluginModel.rendererPlugins;
let plugin : RendererPluginType = null;
let plugin: RendererPluginType = null;
let renderer = line.renderer ?? "terminal";
return (
<div className={cn("dropdown", "renderer-dropdown", {"is-active": this.rendererDropdownActive.get()})}>
<div className={cn("dropdown", "renderer-dropdown", { "is-active": this.rendererDropdownActive.get() })}>
<div className="dropdown-trigger">
<button onClick={this.toggleRendererDropdown} className="button is-small is-dark">
<span><i className="fa-sharp fa-solid fa-fill"/> {renderer}</span>
<span>
<i className="fa-sharp fa-solid fa-fill" /> {renderer}
</span>
<span className="icon is-small">
<i className="fa-sharp fa-regular fa-angle-down" aria-hidden="true"></i>
</span>
@ -536,11 +567,21 @@ class LineSettingsModal extends React.Component<{linenum : number}, {}> {
</div>
<div className="dropdown-menu" role="menu">
<div className="dropdown-content has-background-black">
<div onClick={() => this.clickSetRenderer(null) } key="terminal" className="dropdown-item">terminal</div>
<div onClick={() => this.clickSetRenderer(null)} key="terminal" className="dropdown-item">
terminal
</div>
<For each="plugin" of={plugins}>
<div onClick={() => this.clickSetRenderer(plugin.name) } key={plugin.name} className="dropdown-item">{plugin.name}</div>
<div
onClick={() => this.clickSetRenderer(plugin.name)}
key={plugin.name}
className="dropdown-item"
>
{plugin.name}
</div>
</For>
<div onClick={() => this.clickSetRenderer("none") } key="none" className="dropdown-item">none</div>
<div onClick={() => this.clickSetRenderer("none")} key="none" className="dropdown-item">
none
</div>
</div>
</div>
</div>
@ -557,36 +598,32 @@ class LineSettingsModal extends React.Component<{linenum : number}, {}> {
}
return (
<div className={cn("modal line-settings-modal settings-modal prompt-modal is-active")}>
<div className="modal-background"/>
<div className="modal-background" />
<div className="modal-content">
<header>
<div className="modal-title">line settings ({line.linenum})</div>
<div className="close-icon">
<i onClick={this.closeModal} className="fa-sharp fa-solid fa-times"/>
<i onClick={this.closeModal} className="fa-sharp fa-solid fa-times" />
</div>
</header>
<div className="inner-content">
<div className="settings-field">
<div className="settings-label">
Renderer
</div>
<div className="settings-input">
{this.renderRendererDropdown()}
</div>
<div className="settings-label">Renderer</div>
<div className="settings-input">{this.renderRendererDropdown()}</div>
</div>
<div className="settings-field">
<div className="settings-label">
Archived
</div>
<div className="settings-label">Archived</div>
<div className="settings-input">
<Toggle checked={!!line.archived} onChange={this.handleChangeArchived}/>
<Toggle checked={!!line.archived} onChange={this.handleChangeArchived} />
</div>
</div>
<SettingsError errorMessage={this.errorMessage}/>
<div style={{height: 50}}/>
<SettingsError errorMessage={this.errorMessage} />
<div style={{ height: 50 }} />
</div>
<footer>
<div onClick={this.closeModal} className="button is-prompt-green is-outlined is-small">Close</div>
<div onClick={this.closeModal} className="button is-prompt-green is-outlined is-small">
Close
</div>
</footer>
</div>
</div>
@ -596,32 +633,34 @@ class LineSettingsModal extends React.Component<{linenum : number}, {}> {
@mobxReact.observer
class ClientSettingsModal extends React.Component<{}, {}> {
tempFontSize : OV<number>;
fontSizeDropdownActive : OV<boolean> = mobx.observable.box(false, {name: "clientSettings-fontSizeDropdownActive"});
errorMessage : OV<string> = mobx.observable.box(null, {name: "ClientSettings-errorMessage"});
tempFontSize: OV<number>;
fontSizeDropdownActive: OV<boolean> = mobx.observable.box(false, { name: "clientSettings-fontSizeDropdownActive" });
errorMessage: OV<string> = mobx.observable.box(null, { name: "ClientSettings-errorMessage" });
constructor(props : any) {
constructor(props: any) {
super(props);
let cdata = GlobalModel.clientData.get();
this.tempFontSize = mobx.observable.box(GlobalModel.termFontSize.get(), {name: "clientSettings-tempFontSize"});
this.tempFontSize = mobx.observable.box(GlobalModel.termFontSize.get(), {
name: "clientSettings-tempFontSize",
});
}
@boundMethod
closeModal() : void {
closeModal(): void {
mobx.action(() => {
GlobalModel.clientSettingsModal.set(false);
})();
}
@boundMethod
dismissError() : void {
dismissError(): void {
mobx.action(() => {
this.errorMessage.set(null);
})();
}
@boundMethod
handleChangeFontSize(newFontSize : number) : void {
handleChangeFontSize(newFontSize: number): void {
if (GlobalModel.termFontSize.get() == newFontSize) {
return;
}
@ -630,29 +669,28 @@ class ClientSettingsModal extends React.Component<{}, {}> {
}
@boundMethod
togglefontSizeDropdown() : void {
togglefontSizeDropdown(): void {
mobx.action(() => {
this.fontSizeDropdownActive.set(!this.fontSizeDropdownActive.get());
})();
}
@boundMethod
handleChangeTelemetry(val : boolean) : void {
let prtn : Promise<CommandRtnType> = null;
handleChangeTelemetry(val: boolean): void {
let prtn: Promise<CommandRtnType> = null;
if (val) {
prtn = GlobalCommandRunner.telemetryOn(false);
}
else {
} else {
prtn = GlobalCommandRunner.telemetryOff(false);
}
commandRtnHandler(prtn, this.errorMessage);
}
renderFontSizeDropdown() : any {
renderFontSizeDropdown(): any {
let availableFontSizes = [8, 9, 10, 11, 12, 13, 14, 15];
let fsize : number = 0;
let fsize: number = 0;
return (
<div className={cn("dropdown", "font-size-dropdown", {"is-active": this.fontSizeDropdownActive.get()})}>
<div className={cn("dropdown", "font-size-dropdown", { "is-active": this.fontSizeDropdownActive.get() })}>
<div className="dropdown-trigger">
<button onClick={this.togglefontSizeDropdown} className="button is-small is-dark">
<span>{this.tempFontSize.get()}px</span>
@ -664,7 +702,13 @@ class ClientSettingsModal extends React.Component<{}, {}> {
<div className="dropdown-menu" role="menu">
<div className="dropdown-content has-background-black">
<For each="fsize" of={availableFontSizes}>
<div onClick={() => this.handleChangeFontSize(fsize) } key={fsize + "px"} className="dropdown-item">{fsize}px</div>
<div
onClick={() => this.handleChangeFontSize(fsize)}
key={fsize + "px"}
className="dropdown-item"
>
{fsize}px
</div>
</For>
</div>
</div>
@ -673,114 +717,117 @@ class ClientSettingsModal extends React.Component<{}, {}> {
}
@boundMethod
inlineUpdateOpenAIModel(newModel : string) : void {
let prtn = GlobalCommandRunner.setClientOpenAISettings({model: newModel});
inlineUpdateOpenAIModel(newModel: string): void {
let prtn = GlobalCommandRunner.setClientOpenAISettings({ model: newModel });
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
inlineUpdateOpenAIToken(newToken : string) : void {
let prtn = GlobalCommandRunner.setClientOpenAISettings({apitoken: newToken});
inlineUpdateOpenAIToken(newToken: string): void {
let prtn = GlobalCommandRunner.setClientOpenAISettings({ apitoken: newToken });
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
inlineUpdateOpenAIMaxTokens(newMaxTokensStr : string) : void {
let prtn = GlobalCommandRunner.setClientOpenAISettings({maxtokens: newMaxTokensStr});
inlineUpdateOpenAIMaxTokens(newMaxTokensStr: string): void {
let prtn = GlobalCommandRunner.setClientOpenAISettings({ maxtokens: newMaxTokensStr });
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
setErrorMessage(msg : string) : void {
setErrorMessage(msg: string): void {
mobx.action(() => {
this.errorMessage.set(msg);
})();
}
render() {
let cdata : ClientDataType = GlobalModel.clientData.get();
let cdata: ClientDataType = GlobalModel.clientData.get();
let openAIOpts = cdata.openaiopts ?? {};
let apiTokenStr = (util.isBlank(openAIOpts.apitoken) ? "(not set)" : "********");
let maxTokensStr = String(openAIOpts.maxtokens == null || openAIOpts.maxtokens == 0 ? 1000 : openAIOpts.maxtokens);
let apiTokenStr = util.isBlank(openAIOpts.apitoken) ? "(not set)" : "********";
let maxTokensStr = String(
openAIOpts.maxtokens == null || openAIOpts.maxtokens == 0 ? 1000 : openAIOpts.maxtokens
);
return (
<div className={cn("modal client-settings-modal settings-modal prompt-modal is-active")}>
<div className="modal-background"/>
<div className="modal-background" />
<div className="modal-content">
<header>
<div className="modal-title">client settings</div>
<div className="close-icon">
<i onClick={this.closeModal} className="fa-sharp fa-solid fa-times"/>
<i onClick={this.closeModal} className="fa-sharp fa-solid fa-times" />
</div>
</header>
<div className="inner-content">
<div className="settings-field">
<div className="settings-label">
Term Font Size
</div>
<div className="settings-input">
{this.renderFontSizeDropdown()}
</div>
<div className="settings-label">Term Font Size</div>
<div className="settings-input">{this.renderFontSizeDropdown()}</div>
</div>
<div className="settings-field">
<div className="settings-label">
Client ID
</div>
<div className="settings-input">
{cdata.clientid}
</div>
<div className="settings-label">Client ID</div>
<div className="settings-input">{cdata.clientid}</div>
</div>
<div className="settings-field">
<div className="settings-label">
Client Version
</div>
<div className="settings-label">Client Version</div>
<div className="settings-input">
{VERSION} {BUILD}
</div>
</div>
<div className="settings-field">
<div className="settings-label">
DB Version
</div>
<div className="settings-label">DB Version</div>
<div className="settings-input">{cdata.dbversion}</div>
</div>
<div className="settings-field">
<div className="settings-label">Basic Telemetry</div>
<div className="settings-input">
{cdata.dbversion}
<Toggle checked={!cdata.clientopts.notelemetry} onChange={this.handleChangeTelemetry} />
</div>
</div>
<div className="settings-field">
<div className="settings-label">
Basic Telemetry
</div>
<div className="settings-label">OpenAI Token</div>
<div className="settings-input">
<Toggle checked={!cdata.clientopts.notelemetry} onChange={this.handleChangeTelemetry}/>
<InlineSettingsTextEdit
placeholder=""
text={apiTokenStr}
value={""}
onChange={this.inlineUpdateOpenAIToken}
maxLength={100}
showIcon={true}
/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">
OpenAI Token
</div>
<div className="settings-label">OpenAI Model</div>
<div className="settings-input">
<InlineSettingsTextEdit placeholder="" text={apiTokenStr} value={""} onChange={this.inlineUpdateOpenAIToken} maxLength={100} showIcon={true}/>
<InlineSettingsTextEdit
placeholder="gpt-3.5-turbo"
text={util.isBlank(openAIOpts.model) ? "gpt-3.5-turbo" : openAIOpts.model}
value={openAIOpts.model ?? ""}
onChange={this.inlineUpdateOpenAIModel}
maxLength={100}
showIcon={true}
/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">
OpenAI Model
</div>
<div className="settings-label">OpenAI MaxTokens</div>
<div className="settings-input">
<InlineSettingsTextEdit placeholder="gpt-3.5-turbo" text={util.isBlank(openAIOpts.model) ? "gpt-3.5-turbo" : openAIOpts.model} value={openAIOpts.model ?? ""} onChange={this.inlineUpdateOpenAIModel} maxLength={100} showIcon={true}/>
<InlineSettingsTextEdit
placeholder=""
text={maxTokensStr}
value={maxTokensStr}
onChange={this.inlineUpdateOpenAIMaxTokens}
maxLength={10}
showIcon={true}
/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">
OpenAI MaxTokens
</div>
<div className="settings-input">
<InlineSettingsTextEdit placeholder="" text={maxTokensStr} value={maxTokensStr} onChange={this.inlineUpdateOpenAIMaxTokens} maxLength={10} showIcon={true}/>
</div>
</div>
<SettingsError errorMessage={this.errorMessage}/>
<SettingsError errorMessage={this.errorMessage} />
</div>
<footer>
<div onClick={this.closeModal} className="button is-prompt-green is-outlined is-small">Close</div>
<div onClick={this.closeModal} className="button is-prompt-green is-outlined is-small">
Close
</div>
</footer>
</div>
</div>
@ -788,4 +835,10 @@ class ClientSettingsModal extends React.Component<{}, {}> {
}
}
export {ScreenSettingsModal, SessionSettingsModal, LineSettingsModal, ClientSettingsModal, WebStopShareConfirmMarkdown};
export {
ScreenSettingsModal,
SessionSettingsModal,
LineSettingsModal,
ClientSettingsModal,
WebStopShareConfirmMarkdown,
};

View File

@ -5,7 +5,7 @@
border-bottom: 2px solid #ddd;
margin-left: -4px;
margin-right: -5px;
.title.prompt-logo-small {
padding-left: 5px;
padding-top: 8px;
@ -18,15 +18,15 @@
white-space: nowrap;
overflow: hidden;
flex-shrink: 0;
&.is-dev {
background-color: darken(rgb(177, 0, 10), 30%);
}
&.collapsed {
padding-left: 1px;
}
.title-cursor {
position: relative;
bottom: 3px;
@ -84,12 +84,13 @@
}
.menu-list {
li.new-session, li.add-remote {
li.new-session,
li.add-remote {
a {
color: #666;
font-size: 13px;
}
.fa {
font-size: 10px;
}
@ -103,7 +104,10 @@
.mono-font();
}
li.menu-history, li.menu-bookmarks, li.menu-settings, li.menu-websharing {
li.menu-history,
li.menu-bookmarks,
li.menu-settings,
li.menu-websharing {
margin-left: -10px;
}

View File

@ -3,27 +3,21 @@ import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import {
If,
For,
When,
Otherwise,
Choose,
} from "tsx-control-statements/components";
import { If, For, When, Otherwise, Choose } from "tsx-control-statements/components";
import type {
RendererModelInitializeParams,
TermOptsType,
RendererContext,
RendererOpts,
SimpleBlobRendererComponent,
RendererModelContainerApi,
RendererPluginType,
PtyDataType,
RendererModel,
RendererOptsUpdate,
LineType,
TermContextUnion,
RendererContainerType,
RendererModelInitializeParams,
TermOptsType,
RendererContext,
RendererOpts,
SimpleBlobRendererComponent,
RendererModelContainerApi,
RendererPluginType,
PtyDataType,
RendererModel,
RendererOptsUpdate,
LineType,
TermContextUnion,
RendererContainerType,
} from "./types";
import { PacketDataBuffer } from "./ptydata";
import { debounce, throttle } from "throttle-debounce";
@ -32,198 +26,193 @@ type OV<V> = mobx.IObservableValue<V>;
type CV<V> = mobx.IComputedValue<V>;
class SimpleBlobRendererModel {
context: RendererContext;
opts: RendererOpts;
isDone: OV<boolean>;
api: RendererModelContainerApi;
savedHeight: number;
loading: OV<boolean>;
loadError: OV<string> = mobx.observable.box(null, {
name: "renderer-loadError",
});
ptyData: PtyDataType;
ptyDataSource: (termContext: TermContextUnion) => Promise<PtyDataType>;
initialize(params: RendererModelInitializeParams): void {
this.loading = mobx.observable.box(true, { name: "renderer-loading" });
this.isDone = mobx.observable.box(params.isDone, {
name: "renderer-isDone",
context: RendererContext;
opts: RendererOpts;
isDone: OV<boolean>;
api: RendererModelContainerApi;
savedHeight: number;
loading: OV<boolean>;
loadError: OV<string> = mobx.observable.box(null, {
name: "renderer-loadError",
});
this.context = params.context;
this.opts = params.opts;
this.api = params.api;
this.savedHeight = params.savedHeight;
this.ptyDataSource = params.ptyDataSource;
if (this.isDone.get()) {
setTimeout(() => this.reload(0), 10);
ptyData: PtyDataType;
ptyDataSource: (termContext: TermContextUnion) => Promise<PtyDataType>;
initialize(params: RendererModelInitializeParams): void {
this.loading = mobx.observable.box(true, { name: "renderer-loading" });
this.isDone = mobx.observable.box(params.isDone, {
name: "renderer-isDone",
});
this.context = params.context;
this.opts = params.opts;
this.api = params.api;
this.savedHeight = params.savedHeight;
this.ptyDataSource = params.ptyDataSource;
if (this.isDone.get()) {
setTimeout(() => this.reload(0), 10);
}
}
}
dispose(): void {
return;
}
giveFocus(): void {
return;
}
updateOpts(update: RendererOptsUpdate): void {
Object.assign(this.opts, update);
}
updateHeight(newHeight: number): void {
if (this.savedHeight != newHeight) {
this.savedHeight = newHeight;
this.api.saveHeight(newHeight);
dispose(): void {
return;
}
}
setIsDone(): void {
if (this.isDone.get()) {
return;
giveFocus(): void {
return;
}
mobx.action(() => {
this.isDone.set(true);
})();
this.reload(0);
}
reload(delayMs: number): void {
mobx.action(() => {
this.loading.set(true);
})();
let rtnp = this.ptyDataSource(this.context);
if (rtnp == null) {
console.log(
"no promise returned from ptyDataSource (simplerenderer)",
this.context
);
return;
updateOpts(update: RendererOptsUpdate): void {
Object.assign(this.opts, update);
}
rtnp
.then((ptydata) => {
setTimeout(() => {
this.ptyData = ptydata;
mobx.action(() => {
this.loading.set(false);
this.loadError.set(null);
})();
}, delayMs);
})
.catch((e) => {
console.log("error loading data", e);
updateHeight(newHeight: number): void {
if (this.savedHeight != newHeight) {
this.savedHeight = newHeight;
this.api.saveHeight(newHeight);
}
}
setIsDone(): void {
if (this.isDone.get()) {
return;
}
mobx.action(() => {
this.loadError.set("error loading data: " + e);
this.isDone.set(true);
})();
});
}
this.reload(0);
}
receiveData(pos: number, data: Uint8Array, reason?: string): void {
// this.dataBuf.receiveData(pos, data, reason);
}
reload(delayMs: number): void {
mobx.action(() => {
this.loading.set(true);
})();
let rtnp = this.ptyDataSource(this.context);
if (rtnp == null) {
console.log("no promise returned from ptyDataSource (simplerenderer)", this.context);
return;
}
rtnp.then((ptydata) => {
setTimeout(() => {
this.ptyData = ptydata;
mobx.action(() => {
this.loading.set(false);
this.loadError.set(null);
})();
}, delayMs);
}).catch((e) => {
console.log("error loading data", e);
mobx.action(() => {
this.loadError.set("error loading data: " + e);
})();
});
}
receiveData(pos: number, data: Uint8Array, reason?: string): void {
// this.dataBuf.receiveData(pos, data, reason);
}
}
@mobxReact.observer
class SimpleBlobRenderer extends React.Component<
{
rendererContainer: RendererContainerType;
lineId: string;
plugin: RendererPluginType;
onHeightChange: () => void;
initParams: RendererModelInitializeParams;
},
{}
{
rendererContainer: RendererContainerType;
lineId: string;
plugin: RendererPluginType;
onHeightChange: () => void;
initParams: RendererModelInitializeParams;
},
{}
> {
model: SimpleBlobRendererModel;
wrapperDivRef: React.RefObject<any> = React.createRef();
rszObs: ResizeObserver;
updateHeight_debounced: (newHeight: number) => void;
model: SimpleBlobRendererModel;
wrapperDivRef: React.RefObject<any> = React.createRef();
rszObs: ResizeObserver;
updateHeight_debounced: (newHeight: number) => void;
constructor(props: any) {
super(props);
let { rendererContainer, lineId, plugin, initParams } = this.props;
this.model = new SimpleBlobRendererModel();
this.model.initialize(initParams);
rendererContainer.registerRenderer(lineId, this.model);
this.updateHeight_debounced = debounce(1000, this.updateHeight.bind(this));
}
constructor(props: any) {
super(props);
let { rendererContainer, lineId, plugin, initParams } = this.props;
this.model = new SimpleBlobRendererModel();
this.model.initialize(initParams);
rendererContainer.registerRenderer(lineId, this.model);
this.updateHeight_debounced = debounce(1000, this.updateHeight.bind(this));
}
updateHeight(newHeight: number): void {
this.model.updateHeight(newHeight);
}
updateHeight(newHeight: number): void {
this.model.updateHeight(newHeight);
}
handleResize(entries: ResizeObserverEntry[]): void {
if (this.model.loading.get()) {
return;
handleResize(entries: ResizeObserverEntry[]): void {
if (this.model.loading.get()) {
return;
}
if (this.props.onHeightChange) {
this.props.onHeightChange();
}
if (!this.model.loading.get() && this.wrapperDivRef.current != null) {
let height = this.wrapperDivRef.current.offsetHeight;
this.updateHeight_debounced(height);
}
}
if (this.props.onHeightChange) {
this.props.onHeightChange();
}
if (!this.model.loading.get() && this.wrapperDivRef.current != null) {
let height = this.wrapperDivRef.current.offsetHeight;
this.updateHeight_debounced(height);
}
}
checkRszObs() {
if (this.rszObs != null) {
return;
checkRszObs() {
if (this.rszObs != null) {
return;
}
if (this.wrapperDivRef.current == null) {
return;
}
this.rszObs = new ResizeObserver(this.handleResize.bind(this));
this.rszObs.observe(this.wrapperDivRef.current);
}
if (this.wrapperDivRef.current == null) {
return;
}
this.rszObs = new ResizeObserver(this.handleResize.bind(this));
this.rszObs.observe(this.wrapperDivRef.current);
}
componentDidMount() {
this.checkRszObs();
}
componentWillUnmount() {
let { rendererContainer, lineId } = this.props;
rendererContainer.unloadRenderer(lineId);
if (this.rszObs != null) {
this.rszObs.disconnect();
this.rszObs = null;
componentDidMount() {
this.checkRszObs();
}
}
componentDidUpdate() {
this.checkRszObs();
}
componentWillUnmount() {
let { rendererContainer, lineId } = this.props;
rendererContainer.unloadRenderer(lineId);
if (this.rszObs != null) {
this.rszObs.disconnect();
this.rszObs = null;
}
}
render() {
let { plugin } = this.props;
let model = this.model;
if (model.loading.get()) {
let height = this.model.savedHeight;
return (
<div ref={this.wrapperDivRef} style={{ minHeight: height }}>
...
</div>
);
componentDidUpdate() {
this.checkRszObs();
}
let Comp = plugin.simpleComponent;
if (Comp == null) {
<div ref={this.wrapperDivRef}>(no component found in plugin)</div>;
render() {
let { plugin } = this.props;
let model = this.model;
if (model.loading.get()) {
let height = this.model.savedHeight;
return (
<div ref={this.wrapperDivRef} style={{ minHeight: height }}>
...
</div>
);
}
let Comp = plugin.simpleComponent;
if (Comp == null) {
<div ref={this.wrapperDivRef}>(no component found in plugin)</div>;
}
let dataBlob = new Blob([model.ptyData.data]);
let simpleModel = model as SimpleBlobRendererModel;
let { festate, cmdstr } = this.props.initParams.rawCmd;
return (
<div ref={this.wrapperDivRef}>
<Comp
cwd={festate.cwd}
cmdstr={cmdstr}
data={dataBlob}
context={simpleModel.context}
opts={simpleModel.opts}
savedHeight={simpleModel.savedHeight}
/>
</div>
);
}
let dataBlob = new Blob([model.ptyData.data]);
let simpleModel = model as SimpleBlobRendererModel;
let { festate, cmdstr } = this.props.initParams.rawCmd;
return (
<div ref={this.wrapperDivRef}>
<Comp
cwd={festate.cwd}
cmdstr={cmdstr}
data={dataBlob}
context={simpleModel.context}
opts={simpleModel.opts}
savedHeight={simpleModel.savedHeight}
/>
</div>
);
}
}
export { SimpleBlobRendererModel, SimpleBlobRenderer };

View File

@ -44,9 +44,7 @@
&.is-active {
background-color: @tab-yellow;
color: black;
box-shadow:
0 3px 0 #fff inset,
0 4px 0 #000 inset;
box-shadow: 0 3px 0 #fff inset, 0 4px 0 #000 inset;
}
}
@ -95,9 +93,7 @@
&.is-active {
color: black;
background-color: @tab-white;
box-shadow:
0 3px 0 #fff inset,
0 4px 0 #000 inset;
box-shadow: 0 3px 0 #fff inset, 0 4px 0 #000 inset;
}
}
@ -122,7 +118,7 @@
.screen-tabs-container {
position: relative;
&:hover {
z-index: 200;
}
@ -169,7 +165,7 @@
height: 30px;
min-width: 80px;
width: 150px;
line-height: 1.0;
line-height: 1;
flex-shrink: 1;
display: flex;
justify-content: center;
@ -193,7 +189,7 @@
&.is-active {
box-shadow: 0 3px 0 #fff inset;
border-top: none;
opacity: 1.0;
opacity: 1;
}
&.is-archived {

View File

@ -1,58 +1,58 @@
import * as mobx from "mobx";
import {Terminal} from 'xterm';
import {sprintf} from "sprintf-js";
import {boundMethod} from "autobind-decorator";
import {v4 as uuidv4} from "uuid";
import {termHeightFromRows, windowWidthToCols, windowHeightToRows} from "./textmeasure";
import {boundInt} from "./util";
import type {TermContextUnion, TermOptsType, TermWinSize, RendererContext, WindowSize, PtyDataType} from "./types";
import { Terminal } from "xterm";
import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import { v4 as uuidv4 } from "uuid";
import { termHeightFromRows, windowWidthToCols, windowHeightToRows } from "./textmeasure";
import { boundInt } from "./util";
import type { TermContextUnion, TermOptsType, TermWinSize, RendererContext, WindowSize, PtyDataType } from "./types";
type DataUpdate = {
data : Uint8Array,
pos : number,
}
data: Uint8Array;
pos: number;
};
const MinTermCols = 10;
const MaxTermCols = 1024;
type TermWrapOpts = {
termContext : TermContextUnion,
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,
fontSize : number,
ptyDataSource : (termContext : TermContextUnion) => Promise<PtyDataType>,
onUpdateContentHeight : (termContext : RendererContext, height : number) => void,
termContext: TermContextUnion;
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;
fontSize: number;
ptyDataSource: (termContext: TermContextUnion) => Promise<PtyDataType>;
onUpdateContentHeight: (termContext: RendererContext, height: number) => void;
};
// cmd-instance
class TermWrap {
terminal : any;
termContext : TermContextUnion;
atRowMax : boolean;
usedRows : mobx.IObservableValue<number>;
flexRows : boolean;
connectedElem : Element;
ptyPos : number = 0;
reloading : boolean = false;
dataUpdates : DataUpdate[] = [];
loadError : mobx.IObservableValue<boolean> = mobx.observable.box(false, {name: "term-loaderror"});
winSize : WindowSize;
numParseErrors : number = 0;
termSize : TermWinSize;
focusHandler : (focus : boolean) => void;
isRunning : boolean;
fontSize : number;
onUpdateContentHeight : (termContext : RendererContext, height : number) => void;
ptyDataSource : (termContext : TermContextUnion) => Promise<PtyDataType>;
initializing : boolean;
terminal: any;
termContext: TermContextUnion;
atRowMax: boolean;
usedRows: mobx.IObservableValue<number>;
flexRows: boolean;
connectedElem: Element;
ptyPos: number = 0;
reloading: boolean = false;
dataUpdates: DataUpdate[] = [];
loadError: mobx.IObservableValue<boolean> = mobx.observable.box(false, { name: "term-loaderror" });
winSize: WindowSize;
numParseErrors: number = 0;
termSize: TermWinSize;
focusHandler: (focus: boolean) => void;
isRunning: boolean;
fontSize: number;
onUpdateContentHeight: (termContext: RendererContext, height: number) => void;
ptyDataSource: (termContext: TermContextUnion) => Promise<PtyDataType>;
initializing: boolean;
constructor(elem : Element, opts : TermWrapOpts) {
constructor(elem: Element, opts: TermWrapOpts) {
opts = opts ?? ({} as any);
this.termContext = opts.termContext;
this.connectedElem = elem;
@ -66,23 +66,27 @@ class TermWrap {
this.initializing = true;
if (this.flexRows) {
this.atRowMax = false;
this.usedRows = mobx.observable.box(opts.usedRows ?? (opts.isRunning ? 1 : 0), {name: "term-usedrows"});
}
else {
this.usedRows = mobx.observable.box(opts.usedRows ?? (opts.isRunning ? 1 : 0), { name: "term-usedrows" });
} else {
this.atRowMax = true;
this.usedRows = mobx.observable.box(opts.termOpts.rows, {name: "term-usedrows"});
this.usedRows = mobx.observable.box(opts.termOpts.rows, { name: "term-usedrows" });
}
if (opts.winSize == null) {
this.termSize = {rows: opts.termOpts.rows, cols: opts.termOpts.cols};
}
else {
this.termSize = { rows: opts.termOpts.rows, cols: opts.termOpts.cols };
} else {
let cols = windowWidthToCols(opts.winSize.width, opts.fontSize);
this.termSize = {rows: opts.termOpts.rows, cols: cols};
this.termSize = { rows: opts.termOpts.rows, cols: cols };
}
let theme = {
foreground: "#d3d7cf",
};
this.terminal = new Terminal({rows: this.termSize.rows, cols: this.termSize.cols, fontSize: opts.fontSize, fontFamily: "JetBrains Mono", theme: theme});
this.terminal = new Terminal({
rows: this.termSize.rows,
cols: this.termSize.cols,
fontSize: opts.fontSize,
fontFamily: "JetBrains Mono",
theme: theme,
});
this.terminal._core._inputHandler._parser.setErrorHandler((state) => {
this.numParseErrors++;
return state;
@ -99,7 +103,7 @@ class TermWrap {
this.focusHandler(true);
}
});
this.terminal.textarea.addEventListener("blur", (e : any) => {
this.terminal.textarea.addEventListener("blur", (e: any) => {
if (document.activeElement == this.terminal.textarea) {
return;
}
@ -114,12 +118,12 @@ class TermWrap {
setTimeout(() => this.reload(0), 10);
}
getUsedRows() : number {
getUsedRows(): number {
return this.usedRows.get();
}
@boundMethod
elemScrollHandler(e : any) {
elemScrollHandler(e: any) {
// this stops a weird behavior in the terminal
// 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)
@ -130,21 +134,21 @@ class TermWrap {
e.target.scrollTop = 0;
}
getContextRemoteId() : string {
getContextRemoteId(): string {
if ("remoteId" in this.termContext) {
return this.termContext.remoteId;
}
return null;
}
getRendererContext() : RendererContext {
getRendererContext(): RendererContext {
if ("remoteId" in this.termContext) {
return null;
}
return this.termContext;
}
getFontHeight() : number {
getFontHeight(): number {
return this.terminal._core.viewport._currentRowHeight;
}
@ -160,14 +164,14 @@ class TermWrap {
return;
}
this.terminal.focus();
setTimeout(() => this.terminal._core.viewport.syncScrollArea(true), 0)
setTimeout(() => this.terminal._core.viewport.syncScrollArea(true), 0);
}
disconnectElem() {
this.connectedElem = null;
}
getTermUsedRows() : number {
getTermUsedRows(): number {
let term = this.terminal;
if (term == null) {
return 0;
@ -178,21 +182,21 @@ class TermWrap {
if (termNumLines > term.rows) {
return term.rows;
}
let usedRows = (this.isRunning ? 1 : 0);
let usedRows = this.isRunning ? 1 : 0;
if (this.isRunning && termYPos >= usedRows) {
usedRows = termYPos + 1;
}
for (let i=term.rows-1; i>=usedRows; i--) {
for (let i = term.rows - 1; i >= usedRows; i--) {
let line = termBuf.translateBufferLineToString(i, true);
if (line != null && line.trim() != "") {
usedRows = i+1;
usedRows = i + 1;
break;
}
}
return usedRows;
}
updateUsedRows(forceFull : boolean, reason : string) {
updateUsedRows(forceFull: boolean, reason: string) {
if (this.terminal == null) {
return;
}
@ -228,15 +232,15 @@ class TermWrap {
})();
}
resizeCols(cols : number) : void {
this.resize({rows: this.termSize.rows, cols: cols});
resizeCols(cols: number): void {
this.resize({ rows: this.termSize.rows, cols: cols });
}
resize(size : TermWinSize) : void {
resize(size: TermWinSize): void {
if (this.terminal == null) {
return;
}
let newSize = {rows: size.rows, cols: size.cols};
let newSize = { rows: size.rows, cols: size.cols };
newSize.cols = boundInt(newSize.cols, MinTermCols, MaxTermCols);
if (newSize.rows == this.termSize.rows && newSize.cols == this.termSize.cols) {
return;
@ -246,17 +250,17 @@ class TermWrap {
this.updateUsedRows(true, "resize");
}
resizeWindow(size : WindowSize) : void {
resizeWindow(size: WindowSize): void {
let cols = windowWidthToCols(size.width, this.fontSize);
let rows = windowHeightToRows(size.height, this.fontSize);
this.resize({rows, cols});
this.resize({ rows, cols });
}
_reloadThenHandler(ptydata : PtyDataType) {
_reloadThenHandler(ptydata: PtyDataType) {
this.reloading = false;
this.ptyPos = ptydata.pos;
this.receiveData(ptydata.pos, ptydata.data, "reload-main");
for (let i=0; i<this.dataUpdates.length; i++) {
for (let i = 0; i < this.dataUpdates.length; i++) {
this.receiveData(this.dataUpdates[i].pos, this.dataUpdates[i].data, "reload-update-" + i);
}
this.dataUpdates = [];
@ -267,7 +271,7 @@ class TermWrap {
}
}
getLineNum() : number {
getLineNum(): number {
let context = this.getRendererContext();
if (context == null) {
return 0;
@ -275,7 +279,7 @@ class TermWrap {
return context.lineNum;
}
hardResetTerminal() : void {
hardResetTerminal(): void {
if (this.terminal == null) {
return;
}
@ -286,7 +290,7 @@ class TermWrap {
this.numParseErrors = 0;
}
reload(delayMs : number) {
reload(delayMs: number) {
if (this.terminal == null) {
return;
}
@ -306,14 +310,16 @@ class TermWrap {
this._reloadThenHandler(ptydata);
}, delayMs);
}).catch((e) => {
mobx.action(() => { this.loadError.set(true); })();
mobx.action(() => {
this.loadError.set(true);
})();
this.dataUpdates = [];
this.reloading = false;
console.log("error reloading terminal", this.termContext, e);
});
}
receiveData(pos : number, data : Uint8Array, reason? : string) {
receiveData(pos: number, data: Uint8Array, reason?: string) {
// console.log("update-pty-data", reason, "line:" + this.getLineNum(), pos, data.length, "=>", pos + data.length);
if (this.initializing) {
return;
@ -325,7 +331,7 @@ class TermWrap {
return;
}
if (this.reloading) {
this.dataUpdates.push({data: data, pos: pos});
this.dataUpdates.push({ data: data, pos: pos });
return;
}
if (pos > this.ptyPos) {
@ -347,10 +353,10 @@ class TermWrap {
});
}
cmdDone() : void {
cmdDone(): void {
this.isRunning = false;
this.updateUsedRows(true, "cmd-done");
}
}
export {TermWrap};
export { TermWrap };

View File

@ -1,9 +1,9 @@
import {boundInt} from "./util";
import { boundInt } from "./util";
const MinTermCols = 10;
const MaxTermCols = 1024;
let MonoFontSizes : {height : number, width : number}[] = [];
let MonoFontSizes: { height: number; width: number }[] = [];
// MonoFontSizes[8] = {height: 11, width: 4.797};
// MonoFontSizes[9] = {height: 12, width: 5.398};
@ -15,18 +15,21 @@ let MonoFontSizes : {height : number, width : number}[] = [];
// MonoFontSizes[15] = {height: 20, width: 9};
// MonoFontSizes[16] = {height: 22, width: 9.594};
function getMonoFontSize(fontSize : number) : {height : number, width : number} {
function getMonoFontSize(fontSize: number): { height: number; width: number } {
if (MonoFontSizes[fontSize] != null) {
return MonoFontSizes[fontSize];
}
let size = measureText("W", {pre: true, mono: true, fontSize: fontSize});
let size = measureText("W", { pre: true, mono: true, fontSize: fontSize });
if (size.height != 0 && size.width != 0) {
MonoFontSizes[fontSize] = size;
}
return size;
}
function measureText(text : string, textOpts? : {pre? : boolean, mono? : boolean, fontSize? : number|string}) : {height : number, width : number} {
function measureText(
text: string,
textOpts?: { pre?: boolean; mono?: boolean; fontSize?: number | string }
): { height: number; width: number } {
if (textOpts == null) {
textOpts = {};
}
@ -38,10 +41,9 @@ function measureText(text : string, textOpts? : {pre? : boolean, mono? : boolean
textElem.classList.add("mono");
}
if (textOpts.fontSize != null) {
if (typeof(textOpts.fontSize) == "number") {
if (typeof textOpts.fontSize == "number") {
textElem.style.fontSize = textOpts.fontSize + "px";
}
else {
} else {
textElem.style.fontSize = textOpts.fontSize;
}
}
@ -51,18 +53,18 @@ function measureText(text : string, textOpts? : {pre? : boolean, mono? : boolean
throw new Error("cannot measure text, no #measure div");
}
measureDiv.replaceChildren(textElem);
let rect = textElem.getBoundingClientRect()
return {width: rect.width, height: Math.ceil(rect.height)};
let rect = textElem.getBoundingClientRect();
return { width: rect.width, height: Math.ceil(rect.height) };
}
function windowWidthToCols(width : number, fontSize : number) : number {
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 {
function windowHeightToRows(height: number, fontSize: number): number {
let dr = getMonoFontSize(fontSize);
let rows = Math.floor((height - 80) / dr.height) - 1;
if (rows <= 0) {
@ -71,14 +73,14 @@ function windowHeightToRows(height : number, fontSize : number) : number {
return rows;
}
function termWidthFromCols(cols : number, fontSize : number) : number {
function termWidthFromCols(cols: number, fontSize: number): number {
let dr = getMonoFontSize(fontSize);
return Math.ceil(dr.width*cols) + 15;
return Math.ceil(dr.width * cols) + 15;
}
function termHeightFromRows(rows : number, fontSize : number) : number {
function termHeightFromRows(rows: number, fontSize: number): number {
let dr = getMonoFontSize(fontSize);
return Math.ceil(dr.height*rows);
return Math.ceil(dr.height * rows);
}
export {measureText, getMonoFontSize, windowWidthToCols, windowHeightToRows, termWidthFromCols, termHeightFromRows};
export { measureText, getMonoFontSize, windowWidthToCols, windowHeightToRows, termWidthFromCols, termHeightFromRows };

View File

@ -1,44 +1,47 @@
import * as mobx from "mobx";
import {sprintf} from "sprintf-js";
import { sprintf } from "sprintf-js";
import dayjs from "dayjs";
import localizedFormat from 'dayjs/plugin/localizedFormat';
import type {RemoteType} from "./types";
import localizedFormat from "dayjs/plugin/localizedFormat";
import type { RemoteType } from "./types";
dayjs.extend(localizedFormat)
dayjs.extend(localizedFormat);
function isBlank(s : string) : boolean {
return (s == null || s == "");
function isBlank(s: string): boolean {
return s == null || s == "";
}
function handleNotOkResp(resp : any, url : URL) : Promise<any> {
let errMsg = sprintf("Bad status code response from fetch '%s': code=%d %s", url.toString(), resp.status, resp.statusText);
function handleNotOkResp(resp: any, url: URL): Promise<any> {
let errMsg = sprintf(
"Bad status code response from fetch '%s': code=%d %s",
url.toString(),
resp.status,
resp.statusText
);
return resp.text().then((textData) => {
if (textData == null || textData == "") {
throw new Error(errMsg);
}
let rtnData : any = null;
let rtnData: any = null;
try {
rtnData = JSON.parse(textData);
}
catch (err) {
} catch (err) {
// nothing (rtnData will be null)
}
if (rtnData != null && typeof(rtnData) == "object" && rtnData["error"] != null) {
if (rtnData != null && typeof rtnData == "object" && rtnData["error"] != null) {
throw new Error(rtnData["error"]);
}
throw new Error(errMsg + "\n" + textData);
});
}
function fetchJsonData(resp : any, ctErr : boolean) : Promise<any> {
function fetchJsonData(resp: any, ctErr: boolean): Promise<any> {
let contentType = resp.headers.get("Content-Type");
if (contentType != null && contentType.startsWith("application/json")) {
return resp.text().then((textData) => {
let rtnData : any = null;
let rtnData: any = null;
try {
rtnData = JSON.parse(textData);
}
catch (err) {
} catch (err) {
let errMsg = sprintf("Unparseable JSON: " + err.message);
let rtnErr = new Error(errMsg);
throw rtnErr;
@ -54,7 +57,7 @@ function fetchJsonData(resp : any, ctErr : boolean) : Promise<any> {
}
}
function handleJsonFetchResponse(url : URL, resp : any) : Promise<any> {
function handleJsonFetchResponse(url: URL, resp: any): Promise<any> {
if (!resp.ok) {
return handleNotOkResp(resp, url);
}
@ -62,40 +65,45 @@ function handleJsonFetchResponse(url : URL, resp : any) : Promise<any> {
return rtnData;
}
function base64ToArray(b64 : string) : Uint8Array {
function base64ToArray(b64: string): Uint8Array {
let rawStr = atob(b64);
let rtnArr = new Uint8Array(new ArrayBuffer(rawStr.length));
for (let i=0; i<rawStr.length; i++) {
for (let i = 0; i < rawStr.length; i++) {
rtnArr[i] = rawStr.charCodeAt(i);
}
return rtnArr;
}
interface IDataType {
remove? : boolean;
full? : boolean;
remove?: boolean;
full?: boolean;
}
interface IObjType<DataType> {
dispose : () => void;
mergeData : (data : DataType) => void,
dispose: () => void;
mergeData: (data: DataType) => void;
}
interface ISimpleDataType {
remove? : boolean;
remove?: boolean;
}
function genMergeSimpleData<T extends ISimpleDataType>(objs : mobx.IObservableArray<T>, dataArr : T[], idFn : (obj : T) => string, sortIdxFn : (obj : T) => string) {
function genMergeSimpleData<T extends ISimpleDataType>(
objs: mobx.IObservableArray<T>,
dataArr: T[],
idFn: (obj: T) => string,
sortIdxFn: (obj: T) => string
) {
if (dataArr == null || dataArr.length == 0) {
return;
}
let objMap : Record<string, T> = {};
for (let i=0; i<objs.length; i++) {
let objMap: Record<string, T> = {};
for (let i = 0; i < objs.length; i++) {
let obj = objs[i];
let id = idFn(obj);
objMap[id] = obj;
}
for (let i=0; i<dataArr.length; i++) {
for (let i = 0; i < dataArr.length; i++) {
let dataItem = dataArr[i];
if (dataItem == null) {
console.log("genMergeSimpleData, null item");
@ -105,8 +113,7 @@ function genMergeSimpleData<T extends ISimpleDataType>(objs : mobx.IObservableAr
if (dataItem.remove) {
delete objMap[id];
continue;
}
else {
} else {
objMap[id] = dataItem;
}
}
@ -122,23 +129,23 @@ function genMergeSimpleData<T extends ISimpleDataType>(objs : mobx.IObservableAr
}
function genMergeData<ObjType extends IObjType<DataType>, DataType extends IDataType>(
objs : mobx.IObservableArray<ObjType>,
dataArr : DataType[],
objIdFn : (obj : ObjType) => string,
dataIdFn : (data : DataType) => string,
ctorFn : (data : DataType) => ObjType,
sortIdxFn : (obj : ObjType) => number,
objs: mobx.IObservableArray<ObjType>,
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<string, ObjType> = {};
for (let i=0; i<objs.length; i++) {
let objMap: Record<string, ObjType> = {};
for (let i = 0; i < objs.length; i++) {
let obj = objs[i];
let id = objIdFn(obj);
objMap[id] = obj;
}
for (let i=0; i<dataArr.length; i++) {
for (let i = 0; i < dataArr.length; i++) {
let dataItem = dataArr[i];
if (dataItem == null) {
console.log("genMergeData, null item");
@ -157,7 +164,7 @@ function genMergeData<ObjType extends IObjType<DataType>, DataType extends IData
if (obj == null) {
if (!dataItem.full) {
console.log("cannot create object, dataitem is not full", objs, dataItem);
continue
continue;
}
obj = ctorFn(dataItem);
objMap[id] = obj;
@ -175,18 +182,17 @@ function genMergeData<ObjType extends IObjType<DataType>, DataType extends IData
}
function genMergeDataMap<ObjType extends IObjType<DataType>, DataType extends IDataType>(
objMap : mobx.ObservableMap<string, ObjType>,
dataArr : DataType[],
objIdFn : (obj : ObjType) => string,
dataIdFn : (data : DataType) => string,
ctorFn : (data : DataType) => ObjType,
) : {added : string[], removed : string[]}
{
let rtn : {added : string[], removed : string[]} = {added: [], removed: []};
objMap: mobx.ObservableMap<string, ObjType>,
dataArr: DataType[],
objIdFn: (obj: ObjType) => string,
dataIdFn: (data: DataType) => string,
ctorFn: (data: DataType) => ObjType
): { added: string[]; removed: string[] } {
let rtn: { added: string[]; removed: string[] } = { added: [], removed: [] };
if (dataArr == null || dataArr.length == 0) {
return rtn;
}
for (let i=0; i<dataArr.length; i++) {
for (let i = 0; i < dataArr.length; i++) {
let dataItem = dataArr[i];
if (dataItem == null) {
console.log("genMergeDataMap, null item");
@ -206,7 +212,7 @@ function genMergeDataMap<ObjType extends IObjType<DataType>, DataType extends ID
if (obj == null) {
if (!dataItem.full) {
console.log("cannot create object, dataitem is not full", dataItem);
continue
continue;
}
obj = ctorFn(dataItem);
objMap.set(id, obj);
@ -218,11 +224,11 @@ function genMergeDataMap<ObjType extends IObjType<DataType>, DataType extends ID
return rtn;
}
function parseEnv0(envStr64 : string) : Map<string, string> {
function parseEnv0(envStr64: string): Map<string, string> {
let envStr = atob(envStr64);
let parts = envStr.split("\x00");
let rtn : Map<string, string> = new Map();
for (let i=0; i<parts.length; i++) {
let rtn: Map<string, string> = new Map();
for (let i = 0; i < parts.length; i++) {
let part = parts[i];
if (part == "") {
continue;
@ -232,13 +238,13 @@ function parseEnv0(envStr64 : string) : Map<string, string> {
continue;
}
let varName = part.substr(0, eqIdx);
let varVal = part.substr(eqIdx+1)
rtn.set(varName, varVal)
let varVal = part.substr(eqIdx + 1);
rtn.set(varName, varVal);
}
return rtn;
}
function boundInt(ival : number, minVal : number, maxVal : number) : number {
function boundInt(ival: number, minVal: number, maxVal: number): number {
if (ival < minVal) {
return minVal;
}
@ -248,22 +254,34 @@ function boundInt(ival : number, minVal : number, maxVal : number) : number {
return ival;
}
function isModKeyPress(e : any) {
function isModKeyPress(e: any) {
return e.code.match(/^(Control|Meta|Alt|Shift)(Left|Right)$/);
}
function incObs(inum : mobx.IObservableValue<number>) {
function incObs(inum: mobx.IObservableValue<number>) {
mobx.action(() => {
inum.set(inum.get() + 1);
})();
}
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
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);
@ -276,19 +294,19 @@ function loadFonts() {
const DOW_STRS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
function getTodayStr() : string {
function getTodayStr(): string {
return getDateStr(new Date());
}
function getYesterdayStr() : string {
function getYesterdayStr(): string {
let d = new Date();
d.setDate(d.getDate()-1);
d.setDate(d.getDate() - 1);
return getDateStr(d);
}
function getDateStr(d : Date) : string {
function getDateStr(d: Date): string {
let yearStr = String(d.getFullYear());
let monthStr = String(d.getMonth()+1);
let monthStr = String(d.getMonth() + 1);
if (monthStr.length == 1) {
monthStr = "0" + monthStr;
}
@ -300,7 +318,7 @@ function getDateStr(d : Date) : string {
return dowStr + " " + yearStr + "-" + monthStr + "-" + dayStr;
}
function getRemoteConnVal(r : RemoteType) : number {
function getRemoteConnVal(r: RemoteType): number {
if (r.status == "connected") {
return 1;
}
@ -316,7 +334,7 @@ function getRemoteConnVal(r : RemoteType) : number {
return 5;
}
function sortAndFilterRemotes(origRemotes : RemoteType[]) : RemoteType[] {
function sortAndFilterRemotes(origRemotes: RemoteType[]): RemoteType[] {
let remotes = origRemotes.filter((r) => !r.archived);
remotes.sort((a, b) => {
let connValA = getRemoteConnVal(a);
@ -329,23 +347,48 @@ function sortAndFilterRemotes(origRemotes : RemoteType[]) : RemoteType[] {
return remotes;
}
function makeExternLink(url : string) : string {
function makeExternLink(url: string): string {
return "https://extern?" + encodeURIComponent(url);
}
function isStrEq(s1 : string, s2 : string) {
function isStrEq(s1: string, s2: string) {
if (isBlank(s1) && isBlank(s2)) {
return true;
}
return s1 == s2;
}
function isBoolEq(b1 : boolean, b2 : boolean) {
return (!!b1) == (!!b2);
function isBoolEq(b1: boolean, b2: boolean) {
return !!b1 == !!b2;
}
function hasNoModifiers(e : any) : boolean {
return !e.getModifierState("Shift") && !e.getModifierState("Control") && !e.getModifierState("Meta") && !e.getModifierState("Alt");
function hasNoModifiers(e: any): boolean {
return (
!e.getModifierState("Shift") &&
!e.getModifierState("Control") &&
!e.getModifierState("Meta") &&
!e.getModifierState("Alt")
);
}
export {handleJsonFetchResponse, base64ToArray, genMergeData, genMergeDataMap, genMergeSimpleData, parseEnv0, boundInt, isModKeyPress, incObs, isBlank, loadFonts, getTodayStr, getYesterdayStr, getDateStr, sortAndFilterRemotes, makeExternLink, isStrEq, isBoolEq, hasNoModifiers};
export {
handleJsonFetchResponse,
base64ToArray,
genMergeData,
genMergeDataMap,
genMergeSimpleData,
parseEnv0,
boundInt,
isModKeyPress,
incObs,
isBlank,
loadFonts,
getTodayStr,
getYesterdayStr,
getDateStr,
sortAndFilterRemotes,
makeExternLink,
isStrEq,
isBoolEq,
hasNoModifiers,
};

View File

@ -1,5 +1,5 @@
.mono-font(@size: inherit, @weight: inherit) {
font-family: 'JetBrains Mono', monospace;
font-family: "JetBrains Mono", monospace;
font-size: @size;
font-weight: @weight;
}

View File

@ -16,80 +16,78 @@ const MaxJsonSize = 50000;
@mobxReact.observer
class SourceCodeRenderer extends React.Component<
{
data: Blob;
path: String;
context: RendererContext;
opts: RendererOpts;
savedHeight: number;
},
{}
{
data: Blob;
path: String;
context: RendererContext;
opts: RendererOpts;
savedHeight: number;
},
{}
> {
code: OV<any> = mobx.observable.box(null, {
name: "code",
deep: false,
});
language: OV<any> = mobx.observable.box(null, {
name: "language",
deep: false,
});
code: OV<any> = mobx.observable.box(null, {
name: "code",
deep: false,
});
language: OV<any> = mobx.observable.box(null, {
name: "language",
deep: false,
});
editorRef;
constructor(props) {
super(props);
this.editorRef = React.createRef();
}
componentDidMount() {
let prtn = this.props.data.text();
prtn.then((text) => this.code.set(text));
}
handleEditorDidMount = (editor, monaco) => {
const extension = this.props.cmdstr.split(".").pop();
const detectedLanguage = monaco.languages
.getLanguages()
.find(
(lang) => lang.extensions && lang.extensions.includes("." + extension)
);
if (detectedLanguage) {
this.editorRef.current = editor;
const model = editor.getModel();
if (model) {
monaco.editor.setModelLanguage(model, detectedLanguage.id);
this.language.set(detectedLanguage.id);
}
editorRef;
constructor(props) {
super(props);
this.editorRef = React.createRef();
}
};
render() {
let opts = this.props.opts;
let maxWidth = opts.maxSize.width;
let minWidth = opts.maxSize.width;
if (minWidth > 1000) {
minWidth = 1000;
componentDidMount() {
let prtn = this.props.data.text();
prtn.then((text) => this.code.set(text));
}
handleEditorDidMount = (editor, monaco) => {
const extension = this.props.cmdstr.split(".").pop();
const detectedLanguage = monaco.languages
.getLanguages()
.find((lang) => lang.extensions && lang.extensions.includes("." + extension));
if (detectedLanguage) {
this.editorRef.current = editor;
const model = editor.getModel();
if (model) {
monaco.editor.setModelLanguage(model, detectedLanguage.id);
this.language.set(detectedLanguage.id);
}
}
};
render() {
let opts = this.props.opts;
let maxWidth = opts.maxSize.width;
let minWidth = opts.maxSize.width;
if (minWidth > 1000) {
minWidth = 1000;
}
let lang = this.language.get();
let code = this.code.get();
if (!code) return <></>;
return (
<div className="renderer-container code-renderer">
<div className="scroller" style={{ maxHeight: opts.maxSize.height }}>
<Editor
height="30vh"
theme="hc-black"
defaultLanguage={lang}
defaultValue={code}
onMount={this.handleEditorDidMount}
options={{
scrollBeyondLastLine: false,
fontSize: "14px",
}}
/>
</div>
</div>
);
}
let lang = this.language.get();
let code = this.code.get();
if (!code) return <></>;
return (
<div className="renderer-container code-renderer">
<div className="scroller" style={{ maxHeight: opts.maxSize.height }}>
<Editor
height="30vh"
theme="hc-black"
defaultLanguage={lang}
defaultValue={code}
onMount={this.handleEditorDidMount}
options={{
scrollBeyondLastLine: false,
fontSize: "14px",
}}
/>
</div>
</div>
);
}
}
export { SourceCodeRenderer };

View File

@ -2,8 +2,8 @@ import * as React from "react";
import * as mobx from "mobx";
import * as mobxReact from "mobx-react";
import cn from "classnames";
import {If, For, When, Otherwise, Choose} from "tsx-control-statements/components";
import {WindowSize, RendererContext, TermOptsType, LineType, RendererOpts} from "../types";
import { If, For, When, Otherwise, Choose } from "tsx-control-statements/components";
import { WindowSize, RendererContext, TermOptsType, LineType, RendererOpts } from "../types";
type OV<V> = mobx.IObservableValue<V>;
type CV<V> = mobx.IComputedValue<V>;
@ -26,10 +26,13 @@ type CV<V> = mobx.IComputedValue<V>;
//
@mobxReact.observer
class SimpleImageRenderer extends React.Component<{data : Blob, context : RendererContext, opts : RendererOpts, savedHeight : number}, {}> {
objUrl : string = null;
imageRef : React.RefObject<any> = React.createRef();
imageLoaded : OV<boolean> = mobx.observable.box(false, {name: "imageLoaded"});
class SimpleImageRenderer extends React.Component<
{ data: Blob; context: RendererContext; opts: RendererOpts; savedHeight: number },
{}
> {
objUrl: string = null;
imageRef: React.RefObject<any> = React.createRef();
imageLoaded: OV<boolean> = mobx.observable.box(false, { name: "imageLoaded" });
componentDidMount() {
let img = this.imageRef.current;
@ -56,24 +59,27 @@ class SimpleImageRenderer extends React.Component<{data : Blob, context : Render
URL.revokeObjectURL(this.objUrl);
}
}
render() {
if (this.objUrl == null) {
let dataBlob = this.props.data;
this.objUrl = URL.createObjectURL(dataBlob);
}
let opts = this.props.opts;
let forceHeight : number = null;
let forceHeight: number = null;
if (!this.imageLoaded.get() && this.props.savedHeight >= 0) {
forceHeight = this.props.savedHeight;
}
return (
<div className="renderer-container image-renderer" style={{height: forceHeight}}>
<img ref={this.imageRef} style={{maxHeight: opts.idealSize.height, maxWidth: opts.idealSize.width}} src={this.objUrl}/>
<div className="renderer-container image-renderer" style={{ height: forceHeight }}>
<img
ref={this.imageRef}
style={{ maxHeight: opts.idealSize.height, maxWidth: opts.idealSize.width }}
src={this.objUrl}
/>
</div>
);
}
}
export {SimpleImageRenderer};
export { SimpleImageRenderer };

View File

@ -2,10 +2,10 @@ import * as React from "react";
import * as mobx from "mobx";
import * as mobxReact from "mobx-react";
import cn from "classnames";
import {If, For, When, Otherwise, Choose} from "tsx-control-statements/components";
import {WindowSize, RendererContext, TermOptsType, LineType, RendererOpts} from "../types";
import {sprintf} from "sprintf-js";
import {Markdown} from "../elements";
import { If, For, When, Otherwise, Choose } from "tsx-control-statements/components";
import { WindowSize, RendererContext, TermOptsType, LineType, RendererOpts } from "../types";
import { sprintf } from "sprintf-js";
import { Markdown } from "../elements";
import ReactJson from "react-json-view";
type OV<V> = mobx.IObservableValue<V>;
@ -13,11 +13,14 @@ type OV<V> = mobx.IObservableValue<V>;
const MaxJsonSize = 50000;
@mobxReact.observer
class SimpleJsonRenderer extends React.Component<{data : Blob, context : RendererContext, opts : RendererOpts, savedHeight : number}, {}> {
jsonObj : OV<any> = mobx.observable.box(null, {name: "jsonObj", deep: false});
jsonError : OV<string> = mobx.observable.box(null, {name: "jsonError"});
class SimpleJsonRenderer extends React.Component<
{ data: Blob; context: RendererContext; opts: RendererOpts; savedHeight: number },
{}
> {
jsonObj: OV<any> = mobx.observable.box(null, { name: "jsonObj", deep: false });
jsonError: OV<string> = mobx.observable.box(null, { name: "jsonError" });
setJsonError(err : string) {
setJsonError(err: string) {
mobx.action(() => {
this.jsonError.set(err);
})();
@ -29,7 +32,7 @@ class SimpleJsonRenderer extends React.Component<{data : Blob, context : Rendere
this.setJsonError(sprintf("error: json too large to render size=%d", dataBlob.size));
return;
}
let prtn = dataBlob.text()
let prtn = dataBlob.text();
prtn.then((text) => {
if (/[\x00-\x08]/.test(text)) {
this.setJsonError(sprintf("error: not rendering json, binary characters detected"));
@ -40,19 +43,22 @@ class SimpleJsonRenderer extends React.Component<{data : Blob, context : Rendere
mobx.action(() => {
this.jsonObj.set(obj);
})();
}
catch (e) {
} catch (e) {
this.setJsonError(sprintf("error: JSON parse error: %s", e.message));
}
});
}
render() {
if (this.jsonError.get() != null) {
return <div className="renderer-container json-renderer"><div className="error-container">{this.jsonError.get()}</div></div>;
return (
<div className="renderer-container json-renderer">
<div className="error-container">{this.jsonError.get()}</div>
</div>
);
}
if (this.jsonObj.get() == null) {
return <div className="renderer-container json-renderer" style={{height: this.props.savedHeight}}/>
return <div className="renderer-container json-renderer" style={{ height: this.props.savedHeight }} />;
}
let opts = this.props.opts;
let maxWidth = opts.maxSize.width;
@ -62,12 +68,19 @@ class SimpleJsonRenderer extends React.Component<{data : Blob, context : Rendere
}
return (
<div className="renderer-container json-renderer">
<div className="scroller" style={{maxHeight: opts.maxSize.height}}>
<ReactJson src={this.jsonObj.get()} theme="monokai" style={{backgroundColor: "black"}} displayDataTypes={false} quotesOnKeys={false} sortKeys={true}/>
<div className="scroller" style={{ maxHeight: opts.maxSize.height }}>
<ReactJson
src={this.jsonObj.get()}
theme="monokai"
style={{ backgroundColor: "black" }}
displayDataTypes={false}
quotesOnKeys={false}
sortKeys={true}
/>
</div>
</div>
);
}
}
export {SimpleJsonRenderer};
export { SimpleJsonRenderer };

View File

@ -2,19 +2,22 @@ import * as React from "react";
import * as mobx from "mobx";
import * as mobxReact from "mobx-react";
import cn from "classnames";
import {If, For, When, Otherwise, Choose} from "tsx-control-statements/components";
import {WindowSize, RendererContext, TermOptsType, LineType, RendererOpts} from "../types";
import {sprintf} from "sprintf-js";
import {Markdown} from "../elements";
import { If, For, When, Otherwise, Choose } from "tsx-control-statements/components";
import { WindowSize, RendererContext, TermOptsType, LineType, RendererOpts } from "../types";
import { sprintf } from "sprintf-js";
import { Markdown } from "../elements";
type OV<V> = mobx.IObservableValue<V>;
const MaxMarkdownSize = 50000;
@mobxReact.observer
class SimpleMarkdownRenderer extends React.Component<{data : Blob, context : RendererContext, opts : RendererOpts, savedHeight : number}, {}> {
markdownText : OV<string> = mobx.observable.box(null, {name: "markdownText"});
markdownError : OV<string> = mobx.observable.box(null, {name: "markdownError"});
class SimpleMarkdownRenderer extends React.Component<
{ data: Blob; context: RendererContext; opts: RendererOpts; savedHeight: number },
{}
> {
markdownText: OV<string> = mobx.observable.box(null, { name: "markdownText" });
markdownError: OV<string> = mobx.observable.box(null, { name: "markdownError" });
componentDidMount() {
let dataBlob = this.props.data;
@ -22,7 +25,7 @@ class SimpleMarkdownRenderer extends React.Component<{data : Blob, context : Ren
this.markdownError.set(sprintf("error: markdown too large to render size=%d", dataBlob.size));
return;
}
let prtn = dataBlob.text()
let prtn = dataBlob.text();
prtn.then((text) => {
if (/[\x00-\x08]/.test(text)) {
this.markdownError.set(sprintf("error: not rendering markdown, binary characters detected"));
@ -33,13 +36,17 @@ class SimpleMarkdownRenderer extends React.Component<{data : Blob, context : Ren
})();
});
}
render() {
if (this.markdownError.get() != null) {
return <div className="renderer-container markdown-renderer"><div className="error-container">{this.markdownError.get()}</div></div>;
return (
<div className="renderer-container markdown-renderer">
<div className="error-container">{this.markdownError.get()}</div>
</div>
);
}
if (this.markdownText.get() == null) {
return <div className="renderer-container markdown-renderer" style={{height: this.props.savedHeight}}/>
return <div className="renderer-container markdown-renderer" style={{ height: this.props.savedHeight }} />;
}
let opts = this.props.opts;
let markdownText = this.markdownText.get();
@ -50,12 +57,20 @@ class SimpleMarkdownRenderer extends React.Component<{data : Blob, context : Ren
}
return (
<div className="renderer-container markdown-renderer">
<div className="scroller" style={{maxHeight: opts.maxSize.height, minWidth: minWidth, width: "min-content", maxWidth: maxWidth}}>
<Markdown text={this.markdownText.get()} style={{maxHeight: opts.maxSize.height}}/>
<div
className="scroller"
style={{
maxHeight: opts.maxSize.height,
minWidth: minWidth,
width: "min-content",
maxWidth: maxWidth,
}}
>
<Markdown text={this.markdownText.get()} style={{ maxHeight: opts.maxSize.height }} />
</div>
</div>
);
}
}
export {SimpleMarkdownRenderer};
export { SimpleMarkdownRenderer };

View File

@ -2,50 +2,50 @@ import * as React from "react";
import * as mobx from "mobx";
import * as mobxReact from "mobx-react";
import cn from "classnames";
import {If, For, When, Otherwise, Choose} from "tsx-control-statements/components";
import { If, For, When, Otherwise, Choose } from "tsx-control-statements/components";
import * as T from "../types";
import {debounce, throttle} from "throttle-debounce";
import {boundMethod} from "autobind-decorator";
import {sprintf} from "sprintf-js";
import {PacketDataBuffer} from "../ptydata";
import {Markdown} from "../elements";
import { debounce, throttle } from "throttle-debounce";
import { boundMethod } from "autobind-decorator";
import { sprintf } from "sprintf-js";
import { PacketDataBuffer } from "../ptydata";
import { Markdown } from "../elements";
type OV<V> = mobx.IObservableValue<V>;
type OArr<V> = mobx.IObservableArray<V>;
type OMap<K,V> = mobx.ObservableMap<K,V>;
type OMap<K, V> = mobx.ObservableMap<K, V>;
type OpenAIOutputType = {
model : string,
created : number,
finish_reason : string,
message : string,
model: string;
created: number;
finish_reason: string;
message: string;
};
class OpenAIRendererModel {
context : T.RendererContext;
opts : T.RendererOpts;
isDone : OV<boolean>;
api : T.RendererModelContainerApi;
savedHeight : number;
loading : OV<boolean>;
loadError : OV<string> = mobx.observable.box(null, {name: "renderer-loadError"});
updateHeight_debounced : (newHeight : number) => void;
ptyDataSource : (termContext : T.TermContextUnion) => Promise<T.PtyDataType>;
packetData : PacketDataBuffer;
rawCmd : T.WebCmd;
output : OV<OpenAIOutputType>;
version : OV<number>;
context: T.RendererContext;
opts: T.RendererOpts;
isDone: OV<boolean>;
api: T.RendererModelContainerApi;
savedHeight: number;
loading: OV<boolean>;
loadError: OV<string> = mobx.observable.box(null, { name: "renderer-loadError" });
updateHeight_debounced: (newHeight: number) => void;
ptyDataSource: (termContext: T.TermContextUnion) => Promise<T.PtyDataType>;
packetData: PacketDataBuffer;
rawCmd: T.WebCmd;
output: OV<OpenAIOutputType>;
version: OV<number>;
constructor() {
this.updateHeight_debounced = debounce(1000, this.updateHeight.bind(this));
this.packetData = new PacketDataBuffer(this.packetCallback);
this.output = mobx.observable.box(null, {name: "openai-output"});
this.output = mobx.observable.box(null, { name: "openai-output" });
this.version = mobx.observable.box(0);
}
initialize(params : T.RendererModelInitializeParams) : void {
this.loading = mobx.observable.box(true, {name: "renderer-loading"});
this.isDone = mobx.observable.box(params.isDone, {name: "renderer-isDone"});
initialize(params: T.RendererModelInitializeParams): void {
this.loading = mobx.observable.box(true, { name: "renderer-loading" });
this.isDone = mobx.observable.box(params.isDone, { name: "renderer-isDone" });
this.context = params.context;
this.opts = params.opts;
this.api = params.api;
@ -56,8 +56,8 @@ class OpenAIRendererModel {
}
@boundMethod
packetCallback(packetAny : any) {
let packet : T.OpenAIPacketType = packetAny
packetCallback(packetAny: any) {
let packet: T.OpenAIPacketType = packetAny;
if (packet == null) {
return;
}
@ -65,7 +65,7 @@ class OpenAIRendererModel {
if (packet.error != null) {
mobx.action(() => {
this.loadError.set(packet.error);
this.version.set(this.version.get()+1);
this.version.set(this.version.get() + 1);
})();
return;
}
@ -74,7 +74,7 @@ class OpenAIRendererModel {
model: packet.model,
created: packet.created,
finish_reason: packet.finish_reason,
message: (packet.text ?? ""),
message: packet.text ?? "",
};
mobx.action(() => {
this.output.set(output);
@ -93,31 +93,31 @@ class OpenAIRendererModel {
if (packet.text != null) {
this.output.get().message += packet.text;
}
this.version.set(this.version.get()+1);
this.version.set(this.version.get() + 1);
})();
}
}
dispose() : void {
return;
}
giveFocus() : void {
dispose(): void {
return;
}
updateOpts(update : T.RendererOptsUpdate) : void {
giveFocus(): void {
return;
}
updateOpts(update: T.RendererOptsUpdate): void {
Object.assign(this.opts, update);
}
updateHeight(newHeight : number) : void {
updateHeight(newHeight: number): void {
if (this.savedHeight != newHeight) {
this.savedHeight = newHeight;
this.api.saveHeight(newHeight);
}
}
setIsDone() : void {
setIsDone(): void {
if (this.isDone.get()) {
return;
}
@ -127,7 +127,7 @@ class OpenAIRendererModel {
// this.reload(0);
}
reload(delayMs : number) : void {
reload(delayMs: number): void {
mobx.action(() => {
this.loading.set(true);
this.loadError.set(null);
@ -152,45 +152,41 @@ class OpenAIRendererModel {
})();
});
}
receiveData(pos : number, data : Uint8Array, reason? : string) : void {
receiveData(pos: number, data: Uint8Array, reason?: string): void {
this.packetData.receiveData(pos, data, reason);
}
}
@mobxReact.observer
class OpenAIRenderer extends React.Component<{model : OpenAIRendererModel}> {
renderPrompt(cmd : T.WebCmd) {
class OpenAIRenderer extends React.Component<{ model: OpenAIRendererModel }> {
renderPrompt(cmd: T.WebCmd) {
let cmdStr = cmd.cmdstr.trim();
if (cmdStr.startsWith("/openai")) {
let spaceIdx = cmdStr.indexOf(" ");
if (spaceIdx > 0) {
cmdStr = cmdStr.substr(spaceIdx+1).trim();
cmdStr = cmdStr.substr(spaceIdx + 1).trim();
}
}
return (
<div className="openai-message">
<span className="openai-role openai-role-user">[user]</span>
<div className="openai-content-user">
{cmdStr}
</div>
<div className="openai-content-user">{cmdStr}</div>
</div>
);
}
renderError() {
let model : OpenAIRendererModel = this.props.model;
let model: OpenAIRendererModel = this.props.model;
return (
<div className="openai-message">
<span className="openai-role openai-role-error">[error]</span>
<div className="openai-content-error">
{model.loadError.get()}
</div>
<div className="openai-content-error">{model.loadError.get()}</div>
</div>
);
}
renderOutput(cmd : T.WebCmd) {
renderOutput(cmd: T.WebCmd) {
let output = this.props.model.output.get();
let message = "";
if (output != null) {
@ -207,20 +203,28 @@ class OpenAIRenderer extends React.Component<{model : OpenAIRendererModel}> {
<div className="openai-message">
<div className="openai-role openai-role-assistant">[assistant]</div>
<div className="openai-content-assistant">
<div className="scroller" style={{maxHeight: opts.maxSize.height, minWidth: minWidth, width: "min-content", maxWidth: maxWidth}}>
<Markdown text={message} style={{maxHeight: opts.maxSize.height}}/>
<div
className="scroller"
style={{
maxHeight: opts.maxSize.height,
minWidth: minWidth,
width: "min-content",
maxWidth: maxWidth,
}}
>
<Markdown text={message} style={{ maxHeight: opts.maxSize.height }} />
</div>
</div>
</div>
);
}
render() {
let model : OpenAIRendererModel = this.props.model;
let model: OpenAIRendererModel = this.props.model;
let cmd = model.rawCmd;
let styleVal : Record<string, any> = null;
let styleVal: Record<string, any> = null;
if (model.loading.get() && model.savedHeight >= 0 && model.isDone) {
styleVal = {height: model.savedHeight};
styleVal = { height: model.savedHeight };
}
let version = model.version.get();
let loadError = model.loadError.get();
@ -241,4 +245,4 @@ class OpenAIRenderer extends React.Component<{model : OpenAIRendererModel}> {
}
}
export {OpenAIRenderer, OpenAIRendererModel};
export { OpenAIRenderer, OpenAIRendererModel };

View File

@ -1,9 +1,8 @@
.alt-view {
background-color: #111;
overflow-y: auto;
flex-grow: 1;
.alt-title {
margin: 20px 10px 0px 5px;
padding-left: 10px;
@ -113,8 +112,9 @@
display: flex;
flex-direction: row;
align-items: center;
.session-dropdown, .remote-dropdown {
.session-dropdown,
.remote-dropdown {
.dropdown-item {
color: white;
cursor: pointer;
@ -163,7 +163,7 @@
flex-direction: row;
align-items: center;
margin-left: 15px;
.fromts-text {
}
}
@ -174,7 +174,7 @@
font-size: 12px;
border: 1px solid #777;
padding: 5px 10px 5px 10px;
&:hover {
background-color: #666;
}
@ -220,7 +220,8 @@
&.is-disabled {
cursor: default;
i, span {
i,
span {
display: none;
}
}
@ -271,7 +272,7 @@
border-radius: 3px;
margin: 20px 50px 20px 20px;
}
.history-table {
margin: 0px 10px 10px 10px;
table-layout: fixed;
@ -280,7 +281,7 @@
tr.active-history-item {
td {
padding-right: 10px;
.line-container {
padding: 0px 10px 10px 10px;
overflow-x: auto;
@ -418,7 +419,6 @@
}
}
.webshare-view {
.webshare-item {
padding: 4px 5px 8px 15px;
@ -439,7 +439,7 @@
.webshare-vic-link {
cursor: pointer;
&:hover {
text-decoration: underline;
}
@ -492,8 +492,9 @@
&.pending-delete {
background-color: #522;
}
.bookmark-content, .bookmark-edit {
.bookmark-content,
.bookmark-edit {
flex-grow: 1;
max-width: calc(100% - 50px);
}

View File

@ -1,43 +1,42 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import {sprintf} from "sprintf-js";
import {boundMethod} from "autobind-decorator";
import {v4 as uuidv4} from "uuid";
import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import { v4 as uuidv4 } from "uuid";
import dayjs from "dayjs";
import {If, For, When, Otherwise, Choose} from "tsx-control-statements/components";
import { If, For, When, Otherwise, Choose } from "tsx-control-statements/components";
import cn from "classnames";
import {GlobalModel, GlobalCommandRunner, Screen} from "./model";
import {WebStopShareConfirmMarkdown} from "./settings";
import { GlobalModel, GlobalCommandRunner, Screen } from "./model";
import { WebStopShareConfirmMarkdown } from "./settings";
import * as util from "./util";
type OV<V> = mobx.IObservableValue<V>;
type OArr<V> = mobx.IObservableArray<V>;
type OMap<K,V> = mobx.ObservableMap<K,V>;
type OMap<K, V> = mobx.ObservableMap<K, V>;
@mobxReact.observer
class WebShareView extends React.Component<{}, {}> {
shareCopied : OV<string> = mobx.observable.box(null, {name: "sw-shareCopied"});
shareCopied: OV<string> = mobx.observable.box(null, { name: "sw-shareCopied" });
@boundMethod
closeView() : void {
closeView(): void {
GlobalModel.showSessionView();
}
@boundMethod
viewInContext(screen : Screen) {
viewInContext(screen: Screen) {
GlobalModel.historyViewModel.closeView();
GlobalCommandRunner.lineView(screen.sessionId, screen.screenId, screen.selectedLine.get());
}
getSSName(screen : Screen, snames : Record<string, string>) : string {
getSSName(screen: Screen, snames: Record<string, string>): string {
let sessionName = snames[screen.sessionId] ?? "unknown";
return sprintf("#%s[%s]", sessionName, screen.name.get())
return sprintf("#%s[%s]", sessionName, screen.name.get());
}
@boundMethod
copyShareLink(screen : Screen) : void {
copyShareLink(screen: Screen): void {
let shareLink = screen.getWebShareUrl();
if (shareLink == null) {
return;
@ -50,20 +49,20 @@ class WebShareView extends React.Component<{}, {}> {
mobx.action(() => {
this.shareCopied.set(null);
})();
}, 600)
}, 600);
}
@boundMethod
openScreenSettings(screen : Screen) : void {
openScreenSettings(screen: Screen): void {
mobx.action(() => {
GlobalModel.screenSettingsModal.set({sessionId: screen.sessionId, screenId: screen.screenId});
GlobalModel.screenSettingsModal.set({ sessionId: screen.sessionId, screenId: screen.screenId });
})();
}
@boundMethod
stopSharing(screen : Screen) : void {
stopSharing(screen: Screen): void {
let message = WebStopShareConfirmMarkdown;
let alertRtn = GlobalModel.showAlert({message: message, confirm: true, markdown: true});
let alertRtn = GlobalModel.showAlert({ message: message, confirm: true, markdown: true });
alertRtn.then((result) => {
if (!result) {
return;
@ -73,30 +72,34 @@ class WebShareView extends React.Component<{}, {}> {
if (crtn.success) {
return;
}
GlobalModel.showAlert({message: crtn.error});
GlobalModel.showAlert({ message: crtn.error });
});
});
}
render() {
let isHidden = (GlobalModel.activeMainView.get() != "webshare");
let isHidden = GlobalModel.activeMainView.get() != "webshare";
if (isHidden) {
return null;
}
let snames = GlobalModel.getSessionNames();
let screenList = GlobalModel.getWebSharedScreens();
let screen : Screen = null;
let screen: Screen = null;
return (
<div className={cn("webshare-view", "alt-view")}>
<div className="close-button" onClick={this.closeView}><i className="fa-sharp fa-solid fa-xmark"></i></div>
<div className="close-button" onClick={this.closeView}>
<i className="fa-sharp fa-solid fa-xmark"></i>
</div>
<div className="alt-title">
<i className="fa-sharp fa-solid fa-share-nodes" style={{marginRight: 10}}/>
<i className="fa-sharp fa-solid fa-share-nodes" style={{ marginRight: 10 }} />
WEB SHARING<If condition={screenList.length > 0}> ({screenList.length})</If>
</div>
<If condition={screenList.length == 0}>
<div className="no-content">
No Active Web Shares.<br/>
Share a screen using the "web share" toggle in screen/tab settings <i className="fa-sharp fa-solid fa-gear"/>.
No Active Web Shares.
<br />
Share a screen using the "web share" toggle in screen/tab settings{" "}
<i className="fa-sharp fa-solid fa-gear" />.
</div>
</If>
<If condition={screenList.length > 0}>
@ -104,7 +107,7 @@ class WebShareView extends React.Component<{}, {}> {
<For each="screen" of={screenList}>
<div key={screen.screenId} className="webshare-item">
<If condition={this.shareCopied.get() == screen.screenId}>
<div className="copied-indicator"/>
<div className="copied-indicator" />
</If>
<div className="webshare-vic">
<span className="webshare-vic-link" onClick={() => this.viewInContext(screen)}>
@ -112,28 +115,41 @@ class WebShareView extends React.Component<{}, {}> {
</span>
</div>
<div className="actions">
<a href={util.makeExternLink(screen.getWebShareUrl())} target="_blank" className="button is-prompt-green is-outlined is-small a-block">
<a
href={util.makeExternLink(screen.getWebShareUrl())}
target="_blank"
className="button is-prompt-green is-outlined is-small a-block"
>
<span>open in browser</span>
<span className="icon">
<i className="fa-sharp fa-solid fa-up-right-from-square"/>
<i className="fa-sharp fa-solid fa-up-right-from-square" />
</span>
</a>
<div className="button is-prompt-green is-outlined is-small" onClick={() => this.copyShareLink(screen)}>
<div
className="button is-prompt-green is-outlined is-small"
onClick={() => this.copyShareLink(screen)}
>
<span>copy link</span>
<span className="icon">
<i className="fa-sharp fa-solid fa-copy"/>
<i className="fa-sharp fa-solid fa-copy" />
</span>
</div>
<div className="button is-prompt-green is-outlined is-small" onClick={() => this.openScreenSettings(screen)}>
<div
className="button is-prompt-green is-outlined is-small"
onClick={() => this.openScreenSettings(screen)}
>
<span>open settings</span>
<span className="icon">
<i className="fa-sharp fa-solid fa-cog"/>
<i className="fa-sharp fa-solid fa-cog" />
</span>
</div>
<div className="button is-prompt-danger is-outlined is-small ml-4" onClick={() => this.stopSharing(screen)}>
<div
className="button is-prompt-danger is-outlined is-small ml-4"
onClick={() => this.stopSharing(screen)}
>
<span>stop sharing</span>
<span className="icon">
<i className="fa-sharp fa-solid fa-trash"/>
<i className="fa-sharp fa-solid fa-trash" />
</span>
</div>
</div>
@ -143,8 +159,13 @@ class WebShareView extends React.Component<{}, {}> {
</If>
<div className="alt-help">
<div className="help-entry">
Currently limited to a maximum of 3 screens, each with up to 50 commands.<br/>
Contact us on <a target="_blank" href="https://discord.gg/XfvZ334gwU"><i className="fa-brands fa-discord"/> Discord</a> to get a higher limit.
Currently limited to a maximum of 3 screens, each with up to 50 commands.
<br />
Contact us on{" "}
<a target="_blank" href="https://discord.gg/XfvZ334gwU">
<i className="fa-brands fa-discord" /> Discord
</a>{" "}
to get a higher limit.
</div>
</div>
</div>
@ -152,5 +173,4 @@ class WebShareView extends React.Component<{}, {}> {
}
}
export {WebShareView};
export { WebShareView };

View File

@ -1,32 +1,32 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import {sprintf} from "sprintf-js";
import {boundMethod} from "autobind-decorator";
import {If, For, When, Otherwise, Choose} from "tsx-control-statements/components";
import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import { If, For, When, Otherwise, Choose } from "tsx-control-statements/components";
import cn from "classnames";
import {WebShareModel, getTermPtyData} from "./webshare-model";
import { WebShareModel, getTermPtyData } from "./webshare-model";
import * as T from "./types";
import {isBlank} from "./util";
import {PluginModel} from "./plugins";
import { isBlank } from "./util";
import { PluginModel } from "./plugins";
import * as lineutil from "./lineutil";
import * as util from "./util";
import {windowWidthToCols, windowHeightToRows, termHeightFromRows, termWidthFromCols} from "./textmeasure";
import {debounce, throttle} from "throttle-debounce";
import {LinesView} from "./linesview";
import {Toggle} from "./elements";
import {SimpleBlobRendererModel, SimpleBlobRenderer} from "./simplerenderer";
import { windowWidthToCols, windowHeightToRows, termHeightFromRows, termWidthFromCols } from "./textmeasure";
import { debounce, throttle } from "throttle-debounce";
import { LinesView } from "./linesview";
import { Toggle } from "./elements";
import { SimpleBlobRendererModel, SimpleBlobRenderer } from "./simplerenderer";
type OV<V> = mobx.IObservableValue<V>;
type OArr<V> = mobx.IObservableArray<V>;
type OMap<K,V> = mobx.ObservableMap<K,V>;
type OMap<K, V> = mobx.ObservableMap<K, V>;
let foo = LinesView;
// TODO reshare
// TODO document.visibility API to disconnect websocket: document.addEventListener("visibilitychange", () => { document.hidden });
function makeFullRemoteRef(ownerName : string, remoteRef : string, name : string) : string {
function makeFullRemoteRef(ownerName: string, remoteRef: string, name: string): string {
if (isBlank(ownerName) && isBlank(name)) {
return remoteRef;
}
@ -39,7 +39,7 @@ function makeFullRemoteRef(ownerName : string, remoteRef : string, name : string
return ownerName + ":" + remoteRef + ":" + name;
}
function getShortVEnv(venvDir : string) : string {
function getShortVEnv(venvDir: string): string {
if (isBlank(venvDir)) {
return "";
}
@ -47,10 +47,10 @@ function getShortVEnv(venvDir : string) : string {
if (lastSlash == -1) {
return venvDir;
}
return venvDir.substr(lastSlash+1);
return venvDir.substr(lastSlash + 1);
}
function replaceHomePath(path : string, homeDir : string) : string {
function replaceHomePath(path: string, homeDir: string): string {
if (path == homeDir) {
return "~";
}
@ -60,77 +60,100 @@ function replaceHomePath(path : string, homeDir : string) : string {
return path;
}
function getCwdStr(remote : T.WebRemote, state : Record<string, string>) : string {
function getCwdStr(remote: T.WebRemote, state: Record<string, string>): string {
if (state == null || isBlank(state.cwd)) {
return "~";
}
let cwd = state.cwd;
if (remote && remote.homedir) {
cwd = replaceHomePath(cwd, remote.homedir)
cwd = replaceHomePath(cwd, remote.homedir);
}
return cwd;
}
function getRemoteStr(remote : T.WebRemote) : string {
function getRemoteStr(remote: T.WebRemote): string {
if (remote == null) {
return "(invalid remote)";
}
let remoteRef = (!isBlank(remote.alias) ? remote.alias : remote.canonicalname);
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 : Record<string, string>}, {}> {
class Prompt extends React.Component<{ remote: T.WebRemote; festate: Record<string, string> }, {}> {
render() {
let {remote, festate} = this.props;
let { remote, festate } = this.props;
let remoteStr = getRemoteStr(remote);
let cwd = getCwdStr(remote, festate);
let isRoot = !!remote.isroot;
let remoteColorClass = (isRoot ? "color-red" : "color-green");
let remoteTitle : string = null;
let remoteColorClass = isRoot ? "color-red" : "color-green";
let remoteTitle: string = null;
if (remote && remote.canonicalname) {
remoteTitle = remote.canonicalname;
}
let cwdElem = (<span title="current directory" className="term-prompt-cwd"><i className="fa-solid fa-sharp fa-folder-open"/>{cwd}</span>);
let remoteElem = (<span title={remoteTitle} className={cn("term-prompt-remote", remoteColorClass)}>[{remoteStr}] </span>);
let rootIndicatorElem = (<span className="term-prompt-end">{isRoot ? "#" : "$"}</span>);
let cwdElem = (
<span title="current directory" className="term-prompt-cwd">
<i className="fa-solid fa-sharp fa-folder-open" />
{cwd}
</span>
);
let remoteElem = (
<span title={remoteTitle} className={cn("term-prompt-remote", remoteColorClass)}>
[{remoteStr}]{" "}
</span>
);
let rootIndicatorElem = <span className="term-prompt-end">{isRoot ? "#" : "$"}</span>;
let branchElem = null;
let pythonElem = null;
if (!isBlank(festate["PROMPTVAR_GITBRANCH"])) {
let branchName = festate["PROMPTVAR_GITBRANCH"];
branchElem = (<span title="current git branch" className="term-prompt-branch"><i className="fa-sharp fa-solid fa-code-branch"/>{branchName} </span>);
branchElem = (
<span title="current git branch" className="term-prompt-branch">
<i className="fa-sharp fa-solid fa-code-branch" />
{branchName}{" "}
</span>
);
}
if (!isBlank(festate["VIRTUAL_ENV"])) {
let venvDir = festate["VIRTUAL_ENV"];
let venv = getShortVEnv(venvDir);
pythonElem = (<span title="python venv" className="term-prompt-python"><i className="fa-brands fa-python"/>{venv} </span>);
pythonElem = (
<span title="python venv" className="term-prompt-python">
<i className="fa-brands fa-python" />
{venv}{" "}
</span>
);
}
return (
<span className="term-prompt">{remoteElem} {pythonElem}{branchElem}{cwdElem} {rootIndicatorElem}</span>
<span className="term-prompt">
{remoteElem} {pythonElem}
{branchElem}
{cwdElem} {rootIndicatorElem}
</span>
);
}
}
@mobxReact.observer
class LineAvatar extends React.Component<{line : T.WebLine, cmd : T.WebCmd}, {}> {
class LineAvatar extends React.Component<{ line: T.WebLine; cmd: T.WebCmd }, {}> {
render() {
let {line, cmd} = this.props;
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");
let status = cmd != null ? cmd.status : "done";
let rtnstate = cmd != null ? cmd.rtnstate : false;
let isComment = line.linetype == "text";
return (
<div className={cn("avatar", "num-"+lineNumStr.length, "status-" + status, {"rtnstate": rtnstate})}>
<div className={cn("avatar", "num-" + lineNumStr.length, "status-" + status, { rtnstate: rtnstate })}>
{lineNumStr}
<If condition={status == "hangup" || status == "error"}>
<i className="fa-sharp fa-solid fa-triangle-exclamation status-icon"/>
<i className="fa-sharp fa-solid fa-triangle-exclamation status-icon" />
</If>
<If condition={status == "detached"}>
<i className="fa-sharp fa-solid fa-rotate status-icon"/>
<i className="fa-sharp fa-solid fa-rotate status-icon" />
</If>
<If condition={isComment}>
<i className="fa-sharp fa-solid fa-comment comment-icon"/>
<i className="fa-sharp fa-solid fa-comment comment-icon" />
</If>
</div>
);
@ -138,48 +161,60 @@ class LineAvatar extends React.Component<{line : T.WebLine, cmd : T.WebCmd}, {}>
}
@mobxReact.observer
class WebLineCmdView extends React.Component<{line : T.WebLine, cmd : T.WebCmd, topBorder : boolean, width: number, onHeightChange : T.LineHeightChangeCallbackType, staticRender : boolean, visible : OV<boolean>}, {}> {
lineRef : React.RefObject<any> = React.createRef();
isCmdExpanded : OV<boolean> = mobx.observable.box(false, {name: "cmd-expanded"});
isOverflow : OV<boolean> = mobx.observable.box(false, {name: "line-overflow"});
cmdTextRef : React.RefObject<any> = React.createRef();
copiedIndicator : OV<boolean> = mobx.observable.box(false, {name: "copiedIndicator"});
lastHeight : number;
class WebLineCmdView extends React.Component<
{
line: T.WebLine;
cmd: T.WebCmd;
topBorder: boolean;
width: number;
onHeightChange: T.LineHeightChangeCallbackType;
staticRender: boolean;
visible: OV<boolean>;
},
{}
> {
lineRef: React.RefObject<any> = React.createRef();
isCmdExpanded: OV<boolean> = mobx.observable.box(false, { name: "cmd-expanded" });
isOverflow: OV<boolean> = mobx.observable.box(false, { name: "line-overflow" });
cmdTextRef: React.RefObject<any> = React.createRef();
copiedIndicator: OV<boolean> = mobx.observable.box(false, { name: "copiedIndicator" });
lastHeight: number;
componentDidMount() : void {
componentDidMount(): void {
this.checkCmdText();
this.componentDidUpdate();
}
componentDidUpdate() : void {
componentDidUpdate(): void {
this.handleHeightChange();
}
renderSimple() {
let {line, cmd, topBorder} = this.props;
let height : number = 0;
let { line, cmd, topBorder } = this.props;
let height: number = 0;
if (isBlank(line.renderer) || line.renderer == "terminal") {
height = this.getTerminalRendererHeight(cmd);
}
else {
let {line, width} = this.props;
} else {
let { line, width } = this.props;
let usedRows = WebShareModel.getUsedRows(lineutil.getWebRendererContext(line), line, cmd, width);
height = 36 + usedRows;
}
let mainCn = cn(
"line",
"line-cmd",
{"top-border": topBorder},
);
let mainCn = cn("line", "line-cmd", { "top-border": topBorder });
return (
<div ref={this.lineRef} className={mainCn} data-lineid={line.lineid} data-linenum={line.linenum} style={{height: height}}>
<LineAvatar line={line} cmd={null}/>
<div
ref={this.lineRef}
className={mainCn}
data-lineid={line.lineid}
data-linenum={line.linenum}
style={{ height: height }}
>
<LineAvatar line={line} cmd={null} />
</div>
);
}
getTerminalRendererHeight(cmd : T.WebCmd) : number {
let {line, width} = this.props;
getTerminalRendererHeight(cmd: T.WebCmd): number {
let { line, width } = this.props;
let height = 42; // height of zero height terminal
let usedRows = WebShareModel.getUsedRows(lineutil.getWebRendererContext(line), line, cmd, width);
if (usedRows > 0) {
@ -189,13 +224,13 @@ class WebLineCmdView extends React.Component<{line : T.WebLine, cmd : T.WebCmd,
}
@boundMethod
handleExpandCmd() : void {
handleExpandCmd(): void {
mobx.action(() => {
this.isCmdExpanded.set(true);
})();
}
renderCmdText(cmd : T.WebCmd, remote : T.WebRemote) : any {
renderCmdText(cmd: T.WebCmd, remote: T.WebRemote): any {
if (cmd == null) {
return (
<div className="metapart-mono cmdtext">
@ -208,7 +243,7 @@ class WebLineCmdView extends React.Component<{line : T.WebLine, cmd : T.WebCmd,
<React.Fragment>
<div key="meta2" className="meta meta-line2">
<div className="metapart-mono cmdtext">
<Prompt remote={cmd.remote} festate={cmd.festate}/>
<Prompt remote={cmd.remote} festate={cmd.festate} />
</div>
</div>
<div key="meta3" className="meta meta-line3 cmdtext-expanded-wrapper">
@ -221,12 +256,14 @@ class WebLineCmdView extends React.Component<{line : T.WebLine, cmd : T.WebCmd,
return (
<div key="meta2" className="meta meta-line2" ref={this.cmdTextRef}>
<div className="metapart-mono cmdtext">
<Prompt remote={cmd.remote} festate={cmd.festate}/>
<Prompt remote={cmd.remote} festate={cmd.festate} />
<span> </span>
<span>{lineutil.getSingleLineCmdText(cmd.cmdstr)}</span>
</div>
<If condition={this.isOverflow.get() || isMultiLine}>
<div className="cmdtext-overflow" onClick={this.handleExpandCmd}>...&#x25BC;</div>
<div className="cmdtext-overflow" onClick={this.handleExpandCmd}>
...&#x25BC;
</div>
</If>
</div>
);
@ -241,11 +278,11 @@ class WebLineCmdView extends React.Component<{line : T.WebLine, cmd : T.WebCmd,
let metaChild = metaElem.firstChild;
let children = metaChild.childNodes;
let childWidth = 0;
for (let i=0; i<children.length; i++) {
for (let i = 0; i < children.length; i++) {
let ch = children[i];
childWidth += ch.offsetWidth;
}
let isOverflow = (childWidth > metaElemWidth);
let isOverflow = childWidth > metaElemWidth;
if (isOverflow != this.isOverflow.get()) {
mobx.action(() => {
this.isOverflow.set(isOverflow);
@ -254,8 +291,8 @@ class WebLineCmdView extends React.Component<{line : T.WebLine, cmd : T.WebCmd,
}
@boundMethod
handleHeightChange() : void {
let {line} = this.props;
handleHeightChange(): void {
let { line } = this.props;
let curHeight = 0;
let curWidth = 0;
let elem = this.lineRef.current;
@ -273,12 +310,12 @@ class WebLineCmdView extends React.Component<{line : T.WebLine, cmd : T.WebCmd,
}
@boundMethod
handleClick() : void {
handleClick(): void {
WebShareModel.setSelectedLine(this.props.line.linenum);
}
renderMetaWrap() {
let {line, cmd} = this.props;
let { line, cmd } = this.props;
let formattedTime = lineutil.getLineDateTimeStr(line.ts);
let termOpts = cmd.termopts;
let remote = cmd.remote;
@ -289,7 +326,10 @@ class WebLineCmdView extends React.Component<{line : T.WebLine, cmd : T.WebCmd,
<div className="ts">{formattedTime}</div>
<div>&nbsp;</div>
<If condition={!isBlank(renderer) && renderer != "terminal"}>
<div className="renderer"><i className="fa-sharp fa-solid fa-fill"/>{renderer}&nbsp;</div>
<div className="renderer">
<i className="fa-sharp fa-solid fa-fill" />
{renderer}&nbsp;
</div>
</If>
<div className="termopts">
({termOpts.rows}x{termOpts.cols})
@ -300,14 +340,14 @@ class WebLineCmdView extends React.Component<{line : T.WebLine, cmd : T.WebCmd,
);
}
copyAllowed() : boolean {
return (navigator.clipboard != null);
copyAllowed(): boolean {
return navigator.clipboard != null;
}
@boundMethod
clickCopy() : void {
clickCopy(): void {
if (this.copyAllowed()) {
let {cmd} = this.props;
let { cmd } = this.props;
navigator.clipboard.writeText(cmd.cmdstr);
}
mobx.action(() => {
@ -320,7 +360,7 @@ class WebLineCmdView extends React.Component<{line : T.WebLine, cmd : T.WebCmd,
}, 600);
}
getRendererOpts(cmd : T.WebCmd) : T.RendererOpts {
getRendererOpts(cmd: T.WebCmd): T.RendererOpts {
return {
maxSize: WebShareModel.getMaxContentSize(),
idealSize: WebShareModel.getIdealContentSize(),
@ -329,26 +369,25 @@ class WebLineCmdView extends React.Component<{line : T.WebLine, cmd : T.WebCmd,
};
}
makeRendererModelInitializeParams() : T.RendererModelInitializeParams {
let {line, cmd} = this.props;
makeRendererModelInitializeParams(): T.RendererModelInitializeParams {
let { line, cmd } = this.props;
let context = lineutil.getWebRendererContext(line);
let savedHeight = WebShareModel.getContentHeight(context);
if (savedHeight == null) {
if (line.contentheight != null && line.contentheight != -1) {
savedHeight = line.contentheight;
}
else {
} else {
savedHeight = 0;
}
}
let api = {
saveHeight: (height : number) => {
saveHeight: (height: number) => {
WebShareModel.setContentHeight(lineutil.getWebRendererContext(line), height);
},
onFocusChanged: (focus : boolean) => {
onFocusChanged: (focus: boolean) => {
// nothing
},
dataHandler: (data : string, model : T.RendererModel) => {
dataHandler: (data: string, model: T.RendererModel) => {
// nothing
},
};
@ -361,54 +400,94 @@ class WebLineCmdView extends React.Component<{line : T.WebLine, cmd : T.WebCmd,
api: api,
};
}
render() {
let {line, cmd, topBorder, staticRender, visible} = this.props;
let { line, cmd, topBorder, staticRender, visible } = this.props;
let isVisible = visible.get();
if (staticRender || !isVisible) {
return this.renderSimple();
}
let model = WebShareModel;
let isSelected = mobx.computed(() => (model.getSelectedLine() == line.linenum), {name: "computed-isSelected"}).get();
let isServerSelected = mobx.computed(() => (model.getServerSelectedLine() == line.linenum), {name: "computed-isServerSelected"}).get();
let rendererPlugin : T.RendererPluginType = null;
let isNoneRenderer = (line.renderer == "none");
let isSelected = mobx
.computed(() => model.getSelectedLine() == line.linenum, { name: "computed-isSelected" })
.get();
let isServerSelected = mobx
.computed(() => model.getServerSelectedLine() == line.linenum, { name: "computed-isServerSelected" })
.get();
let rendererPlugin: T.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);
let mainCn = cn("web-line line line-cmd", {"top-border": topBorder});
let visObs = mobx.observable.box(true, {name: "visObs"});
let mainCn = cn("web-line line line-cmd", { "top-border": topBorder });
let visObs = mobx.observable.box(true, { name: "visObs" });
let width = this.props.width;
if (width == 0) {
width = 1024;
}
let isExpanded = this.isCmdExpanded.get();
return (
<div ref={this.lineRef} className={mainCn} data-lineid={line.lineid} data-linenum={line.linenum} onClick={this.handleClick}>
<div
ref={this.lineRef}
className={mainCn}
data-lineid={line.lineid}
data-linenum={line.linenum}
onClick={this.handleClick}
>
<If condition={this.copiedIndicator.get()}>
<div key="copied" className="copied-indicator">
<div>copied</div>
</div>
</If>
<div key="focus" className={cn("focus-indicator", {"selected": isSelected || isServerSelected}, {"active": isSelected})}/>
<div className={cn("line-header", {"is-expanded": isExpanded})}>
<LineAvatar line={line} cmd={cmd}/>
<div
key="focus"
className={cn(
"focus-indicator",
{ selected: isSelected || isServerSelected },
{ active: isSelected }
)}
/>
<div className={cn("line-header", { "is-expanded": isExpanded })}>
<LineAvatar line={line} cmd={cmd} />
{this.renderMetaWrap()}
<If condition={this.copyAllowed()}>
<div key="copy" title="Copy Command" className={cn("line-icon copy-icon")} onClick={this.clickCopy} style={{marginLeft: 5}}>
<i className="fa-sharp fa-solid fa-copy"/>
<div
key="copy"
title="Copy Command"
className={cn("line-icon copy-icon")}
onClick={this.clickCopy}
style={{ marginLeft: 5 }}
>
<i className="fa-sharp fa-solid fa-copy" />
</div>
</If>
</div>
<If condition={rendererPlugin == null && !isNoneRenderer}>
<TerminalRenderer line={line} cmd={cmd} width={width} staticRender={staticRender} visible={visible} onHeightChange={this.handleHeightChange}/>
<TerminalRenderer
line={line}
cmd={cmd}
width={width}
staticRender={staticRender}
visible={visible}
onHeightChange={this.handleHeightChange}
/>
</If>
<If condition={rendererPlugin != null}>
<SimpleBlobRenderer rendererContainer={WebShareModel} lineId={line.lineid} plugin={rendererPlugin} onHeightChange={this.handleHeightChange} initParams={this.makeRendererModelInitializeParams()}/>
<SimpleBlobRenderer
rendererContainer={WebShareModel}
lineId={line.lineid}
plugin={rendererPlugin}
onHeightChange={this.handleHeightChange}
initParams={this.makeRendererModelInitializeParams()}
/>
</If>
<If condition={cmd && cmd.rtnstate}>
<div key="rtnstate" className="cmd-rtnstate" style={{visibility: ((cmd.status == "done") ? "visible" : "hidden")}}>
<div
key="rtnstate"
className="cmd-rtnstate"
style={{ visibility: cmd.status == "done" ? "visible" : "hidden" }}
>
<If condition={isBlank(cmd.rtnstatestr)}>
<div className="cmd-rtnstate-label">state unchanged</div>
<div className="cmd-rtnstate-sep"></div>
@ -426,17 +505,19 @@ class WebLineCmdView extends React.Component<{line : T.WebLine, cmd : T.WebCmd,
}
@mobxReact.observer
class WebLineTextView extends React.Component<{line : T.WebLine, cmd : T.WebCmd, topBorder : boolean}, {}> {
class WebLineTextView extends React.Component<{ line: T.WebLine; cmd: T.WebCmd; topBorder: boolean }, {}> {
render() {
let {line, topBorder} = this.props;
let { line, topBorder } = this.props;
let model = WebShareModel;
let isSelected = mobx.computed(() => (model.getSelectedLine() == line.linenum), {name: "computed-isSelected"}).get();
let mainCn = cn("web-line line line-text", {"top-border": topBorder});
let isSelected = mobx
.computed(() => model.getSelectedLine() == line.linenum, { name: "computed-isSelected" })
.get();
let mainCn = cn("web-line line line-text", { "top-border": topBorder });
return (
<div className={mainCn} data-lineid={line.lineid} data-linenum={line.linenum}>
<div key="focus" className={cn("focus-indicator", {"selected active": isSelected})}/>
<div key="focus" className={cn("focus-indicator", { "selected active": isSelected })} />
<div className="line-header">
<LineAvatar line={line} cmd={null}/>
<LineAvatar line={line} cmd={null} />
</div>
<div>
<div>{line.text}</div>
@ -447,10 +528,20 @@ class WebLineTextView extends React.Component<{line : T.WebLine, cmd : T.WebCmd,
}
@mobxReact.observer
class TerminalRenderer extends React.Component<{line : T.WebLine, cmd : T.WebCmd, width : number, staticRender : boolean, visible : OV<boolean>, onHeightChange : () => void}, {}> {
termLoaded : mobx.IObservableValue<boolean> = mobx.observable.box(false, {name: "termrenderer-termLoaded"});
elemRef : React.RefObject<any> = React.createRef();
termRef : React.RefObject<any> = React.createRef();
class TerminalRenderer extends React.Component<
{
line: T.WebLine;
cmd: T.WebCmd;
width: number;
staticRender: boolean;
visible: OV<boolean>;
onHeightChange: () => void;
},
{}
> {
termLoaded: mobx.IObservableValue<boolean> = mobx.observable.box(false, { name: "termrenderer-termLoaded" });
elemRef: React.RefObject<any> = React.createRef();
termRef: React.RefObject<any> = React.createRef();
constructor(props) {
super(props);
@ -466,26 +557,26 @@ class TerminalRenderer extends React.Component<{line : T.WebLine, cmd : T.WebCmd
}
}
getSnapshotBeforeUpdate(prevProps, prevState) : {height : number} {
getSnapshotBeforeUpdate(prevProps, prevState): { height: number } {
let elem = this.elemRef.current;
if (elem == null) {
return {height: 0};
return { height: 0 };
}
return {height: elem.offsetHeight};
return { height: elem.offsetHeight };
}
componentDidUpdate(prevProps, prevState, snapshot : {height : number}) : void {
componentDidUpdate(prevProps, prevState, snapshot: { height: number }): void {
if (this.props.onHeightChange == null) {
return;
}
let {line} = this.props;
let { line } = this.props;
let curHeight = 0;
let elem = this.elemRef.current;
if (elem != null) {
curHeight = elem.offsetHeight;
}
if (snapshot == null) {
snapshot = {height: 0};
snapshot = { height: 0 };
}
if (snapshot.height != curHeight) {
this.props.onHeightChange();
@ -494,8 +585,8 @@ class TerminalRenderer extends React.Component<{line : T.WebLine, cmd : T.WebCmd
this.checkLoad();
}
checkLoad() : void {
let {line, staticRender, visible} = this.props;
checkLoad(): void {
let { line, staticRender, visible } = this.props;
if (staticRender) {
return;
}
@ -503,14 +594,13 @@ class TerminalRenderer extends React.Component<{line : T.WebLine, cmd : T.WebCmd
let curVis = this.termLoaded.get();
if (vis && !curVis) {
this.loadTerminal();
}
else if (!vis && curVis) {
} else if (!vis && curVis) {
this.unloadTerminal(false);
}
}
loadTerminal() : void {
let {line, cmd} = this.props;
loadTerminal(): void {
let { line, cmd } = this.props;
if (cmd == null) {
return;
}
@ -523,8 +613,8 @@ class TerminalRenderer extends React.Component<{line : T.WebLine, cmd : T.WebCmd
mobx.action(() => this.termLoaded.set(true))();
}
unloadTerminal(unmount : boolean) : void {
let {line} = this.props;
unloadTerminal(unmount: boolean): void {
let { line } = this.props;
WebShareModel.unloadRenderer(line.lineid);
if (!unmount) {
mobx.action(() => this.termLoaded.set(false))();
@ -534,10 +624,10 @@ class TerminalRenderer extends React.Component<{line : T.WebLine, cmd : T.WebCmd
}
}
}
@boundMethod
clickTermBlock(e : any) {
let {line} = this.props;
clickTermBlock(e: any) {
let { line } = this.props;
let termWrap = WebShareModel.getTermWrap(line.lineid);
if (termWrap != null) {
termWrap.giveFocus();
@ -546,56 +636,82 @@ class TerminalRenderer extends React.Component<{line : T.WebLine, cmd : T.WebCmd
WebShareModel.setSelectedLine(line.linenum);
})();
}
render() {
let {cmd, line, width, staticRender, visible} = this.props;
let { cmd, line, width, staticRender, visible } = this.props;
let isVisible = visible.get(); // for reaction
let usedRows = WebShareModel.getUsedRows(lineutil.getWebRendererContext(line), line, cmd, width);
let termHeight = termHeightFromRows(usedRows, WebShareModel.getTermFontSize());
let termLoaded = this.termLoaded.get();
let isFocused = (WebShareModel.getSelectedLine() == line.linenum);
let isFocused = WebShareModel.getSelectedLine() == line.linenum;
return (
<div ref={this.elemRef} key="term-wrap" className={cn("terminal-wrapper", {"cmd-done": !lineutil.cmdStatusIsRunning(cmd.status)}, {"zero-height": (termHeight == 0)})}>
<div
ref={this.elemRef}
key="term-wrap"
className={cn(
"terminal-wrapper",
{ "cmd-done": !lineutil.cmdStatusIsRunning(cmd.status) },
{ "zero-height": termHeight == 0 }
)}
>
<If condition={!isFocused}>
<div key="term-block" className="term-block" onClick={this.clickTermBlock}></div>
</If>
<div key="term-connectelem" className="terminal-connectelem" ref={this.termRef} data-lineid={line.lineid} style={{height: termHeight}}></div>
<If condition={!termLoaded}><div key="term-loading" className="terminal-loading-message">...</div></If>
<div
key="term-connectelem"
className="terminal-connectelem"
ref={this.termRef}
data-lineid={line.lineid}
style={{ height: termHeight }}
></div>
<If condition={!termLoaded}>
<div key="term-loading" className="terminal-loading-message">
...
</div>
</If>
</div>
);
}
}
@mobxReact.observer
class WebLineView extends React.Component<{line : T.WebLine, cmd : T.WebCmd, topBorder : boolean, width : number, onHeightChange : T.LineHeightChangeCallbackType, staticRender : boolean, visible : OV<boolean>}, {}> {
class WebLineView extends React.Component<
{
line: T.WebLine;
cmd: T.WebCmd;
topBorder: boolean;
width: number;
onHeightChange: T.LineHeightChangeCallbackType;
staticRender: boolean;
visible: OV<boolean>;
},
{}
> {
render() {
let {line} = this.props;
let { line } = this.props;
if (line.linetype == "text") {
return <WebLineTextView {...this.props}/>
return <WebLineTextView {...this.props} />;
}
if (line.linetype == "cmd") {
return <WebLineCmdView {...this.props}/>
return <WebLineCmdView {...this.props} />;
}
return (
<div className="web-line line">invalid linetype "{line.linetype}"</div>
);
return <div className="web-line line">invalid linetype "{line.linetype}"</div>;
}
}
@mobxReact.observer
class WebScreenView extends React.Component<{}, {}> {
viewRef : React.RefObject<any> = React.createRef();
width : OV<number> = mobx.observable.box(0, {name: "WebScreenView-width"});
handleResize_debounced : () => void;
rszObs : ResizeObserver;
viewRef: React.RefObject<any> = React.createRef();
width: OV<number> = mobx.observable.box(0, { name: "WebScreenView-width" });
handleResize_debounced: () => void;
rszObs: ResizeObserver;
constructor(props : any) {
constructor(props: any) {
super(props);
this.handleResize_debounced = debounce(1000, this.handleResize.bind(this));
}
componentDidMount() : void {
componentDidMount(): void {
if (this.viewRef.current != null) {
let viewElem = this.viewRef.current;
this.rszObs = new ResizeObserver(this.handleResize_debounced.bind(this));
@ -610,16 +726,16 @@ class WebScreenView extends React.Component<{}, {}> {
}
}
handleResize() : void {
handleResize(): void {
let viewElem = this.viewRef.current;
if (viewElem == null) {
return;
}
let width = viewElem.offsetWidth;
let height = viewElem.offsetHeight;
WebShareModel.setLastScreenSize({width, height});
WebShareModel.setLastScreenSize({ width, height });
if (width != this.width.get()) {
WebShareModel.resizeWindow({width: width, height: height});
WebShareModel.resizeWindow({ width: width, height: height });
mobx.action(() => {
this.width.set(width);
})();
@ -627,15 +743,24 @@ class WebScreenView extends React.Component<{}, {}> {
}
@boundMethod
buildLineComponent(lineProps : T.LineFactoryProps) : JSX.Element {
let line : T.WebLine = (lineProps.line as T.WebLine);
buildLineComponent(lineProps: T.LineFactoryProps): JSX.Element {
let line: T.WebLine = lineProps.line as T.WebLine;
let cmd = WebShareModel.getCmdById(lineProps.line.lineid);
return (
<WebLineView key={line.lineid} line={line} cmd={cmd} topBorder={lineProps.topBorder} width={lineProps.width} onHeightChange={lineProps.onHeightChange} staticRender={lineProps.staticRender} visible={lineProps.visible}/>
<WebLineView
key={line.lineid}
line={line}
cmd={cmd}
topBorder={lineProps.topBorder}
width={lineProps.width}
onHeightChange={lineProps.onHeightChange}
staticRender={lineProps.staticRender}
visible={lineProps.visible}
/>
);
}
renderEmpty() : any {
renderEmpty(): any {
return (
<div className="web-screen-view" ref={this.viewRef}>
<div className="web-lines lines">
@ -644,7 +769,7 @@ class WebScreenView extends React.Component<{}, {}> {
</div>
);
}
render() {
let fullScreen = WebShareModel.fullScreen.get();
if (fullScreen == null || fullScreen.lines.length == 0) {
@ -652,7 +777,13 @@ class WebScreenView extends React.Component<{}, {}> {
}
return (
<div className="web-screen-view" ref={this.viewRef}>
<LinesView screen={WebShareModel} width={this.width.get()} lines={fullScreen.lines} renderMode="normal" lineFactory={this.buildLineComponent}/>
<LinesView
screen={WebShareModel}
width={this.width.get()}
lines={fullScreen.lines}
renderMode="normal"
lineFactory={this.buildLineComponent}
/>
</div>
);
}
@ -661,9 +792,9 @@ class WebScreenView extends React.Component<{}, {}> {
@mobxReact.observer
class WebShareMain extends React.Component<{}, {}> {
renderCopy() {
return (<div className="footer-copy">&copy; 2023 Dashborg Inc</div>);
return <div className="footer-copy">&copy; 2023 Dashborg Inc</div>;
}
render() {
let screen = WebShareModel.fullScreen.get();
let errMessage = WebShareModel.errMessage.get();
@ -675,27 +806,36 @@ class WebShareMain extends React.Component<{}, {}> {
<div id="main">
<div className="logo-header">
<div className="logo-text">
<a target="_blank" href="https://www.getprompt.dev">[prompt]</a>
<a target="_blank" href="https://www.getprompt.dev">
[prompt]
</a>
</div>
<div className="flex-spacer"/>
<a href="https://getprompt.dev/download/" target="_blank" className="download-button button is-link">
<div className="flex-spacer" />
<a
href="https://getprompt.dev/download/"
target="_blank"
className="download-button button is-link"
>
<span>Download Prompt</span>
<span className="icon is-small">
<i className="fa-sharp fa-solid fa-cloud-arrow-down"/>
<i className="fa-sharp fa-solid fa-cloud-arrow-down" />
</span>
</a>
</div>
<div className="webshare-controls">
<div className="screen-sharename">{shareName}</div>
<div className="flex-spacer"/>
<div className="flex-spacer" />
<div className="sync-control">
<div>Sync Selection</div>
<Toggle checked={WebShareModel.syncSelectedLine.get()} onChange={(val) => WebShareModel.setSyncSelectedLine(val)}/>
<Toggle
checked={WebShareModel.syncSelectedLine.get()}
onChange={(val) => WebShareModel.setSyncSelectedLine(val)}
/>
</div>
</div>
<div className="prompt-content">
<If condition={screen != null}>
<WebScreenView/>
<WebScreenView />
</If>
<If condition={errMessage != null}>
<div className="err-message">{WebShareModel.errMessage.get()}</div>
@ -703,10 +843,10 @@ class WebShareMain extends React.Component<{}, {}> {
</div>
<div className="prompt-footer">
{this.renderCopy()}
<div className="flex-spacer"/>
<div className="flex-spacer" />
<a target="_blank" href="https://discord.gg/XfvZ334gwU" className="button is-link is-small">
<span className="icon is-small">
<i className="fa-brands fa-discord"/>
<i className="fa-brands fa-discord" />
</span>
<span>Discord</span>
</a>
@ -716,4 +856,4 @@ class WebShareMain extends React.Component<{}, {}> {
}
}
export {WebShareMain};
export { WebShareMain };

View File

@ -1,13 +1,13 @@
import * as mobx from "mobx";
import {sprintf} from "sprintf-js";
import {boundMethod} from "autobind-decorator";
import {handleJsonFetchResponse, isModKeyPress, base64ToArray} from "./util";
import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import { handleJsonFetchResponse, isModKeyPress, base64ToArray } from "./util";
import * as T from "./types";
import {TermWrap} from "./term";
import { TermWrap } from "./term";
import * as lineutil from "./lineutil";
import * as util from "./util";
import {windowWidthToCols, windowHeightToRows, termWidthFromCols, termHeightFromRows} from "./textmeasure";
import {WebShareWSControl} from "./webshare-ws";
import { windowWidthToCols, windowHeightToRows, termWidthFromCols, termHeightFromRows } from "./textmeasure";
import { WebShareWSControl } from "./webshare-ws";
// @ts-ignore
let PROMPT_DEV = __PROMPT_DEV__;
@ -22,15 +22,15 @@ let PROMPT_WSAPI_ENDPOINT = __PROMPT_WSAPI_ENDPOINT__;
type OV<V> = mobx.IObservableValue<V>;
type OArr<V> = mobx.IObservableArray<V>;
type OMap<K,V> = mobx.ObservableMap<K,V>;
type OMap<K, V> = mobx.ObservableMap<K, V>;
type CV<V> = mobx.IComputedValue<V>;
type PtyListener = {
receiveData(ptyPos : number, data : Uint8Array, reason? : string);
receiveData(ptyPos: number, data: Uint8Array, reason?: string);
};
function isBlank(s : string) {
return (s == null || s == "");
function isBlank(s: string) {
return s == null || s == "";
}
function getBaseUrl() {
@ -42,25 +42,25 @@ function getBaseWSUrl() {
}
class WebShareModelClass {
viewKey : string;
screenId : string;
errMessage : OV<string> = mobx.observable.box(null, {name: "errMessage"});
fullScreen : OV<T.WebFullScreen> = mobx.observable.box(null, {name: "webScreen"});
terminals : Record<string, TermWrap> = {}; // lineid => TermWrap
renderers : Record<string, T.RendererModel> = {}; // lineid => RendererModel
contentHeightCache : Record<string, number> = {}; // lineid => height
wsControl : WebShareWSControl;
anchor : {anchorLine : number, anchorOffset : number} = {anchorLine: 0, anchorOffset: 0};
selectedLine : OV<number> = mobx.observable.box(0, {name: "selectedLine"});
syncSelectedLine : OV<boolean> = mobx.observable.box(true, {name: "syncSelectedLine"});
lastScreenSize : T.WindowSize = null;
activePtyFetch : Record<string, boolean> = {}; // lineid -> active
localPtyOffsetMap : Record<string, number> = {};
remotePtyOffsetMap : Record<string, number> = {};
activeUpdateFetch : boolean = false;
remoteScreenVts : number = 0;
isDev : boolean = PROMPT_DEV;
viewKey: string;
screenId: string;
errMessage: OV<string> = mobx.observable.box(null, { name: "errMessage" });
fullScreen: OV<T.WebFullScreen> = mobx.observable.box(null, { name: "webScreen" });
terminals: Record<string, TermWrap> = {}; // lineid => TermWrap
renderers: Record<string, T.RendererModel> = {}; // lineid => RendererModel
contentHeightCache: Record<string, number> = {}; // lineid => height
wsControl: WebShareWSControl;
anchor: { anchorLine: number; anchorOffset: number } = { anchorLine: 0, anchorOffset: 0 };
selectedLine: OV<number> = mobx.observable.box(0, { name: "selectedLine" });
syncSelectedLine: OV<boolean> = mobx.observable.box(true, { name: "syncSelectedLine" });
lastScreenSize: T.WindowSize = null;
activePtyFetch: Record<string, boolean> = {}; // lineid -> active
localPtyOffsetMap: Record<string, number> = {};
remotePtyOffsetMap: Record<string, number> = {};
activeUpdateFetch: boolean = false;
remoteScreenVts: number = 0;
isDev: boolean = PROMPT_DEV;
constructor() {
let pathName = window.location.pathname;
let screenMatch = pathName.match(/\/share\/([a-f0-9-]+)/);
@ -73,17 +73,22 @@ class WebShareModelClass {
this.screenId = urlParams.get("screenid");
}
setTimeout(() => this.loadFullScreenData(false), 10);
this.wsControl = new WebShareWSControl(getBaseWSUrl(), this.screenId, this.viewKey, this.wsMessageCallback.bind(this));
this.wsControl = new WebShareWSControl(
getBaseWSUrl(),
this.screenId,
this.viewKey,
this.wsMessageCallback.bind(this)
);
document.addEventListener("keydown", this.docKeyDownHandler.bind(this));
}
setErrMessage(msg : string) : void {
setErrMessage(msg: string): void {
mobx.action(() => {
this.errMessage.set(msg);
})();
}
setSyncSelectedLine(val : boolean) : void {
setSyncSelectedLine(val: boolean): void {
mobx.action(() => {
this.syncSelectedLine.set(val);
if (val) {
@ -95,55 +100,55 @@ class WebShareModelClass {
})();
}
setLastScreenSize(winSize : T.WindowSize) {
setLastScreenSize(winSize: T.WindowSize) {
if (winSize == null || winSize.height == 0 || winSize.width == 0) {
return;
}
this.lastScreenSize = winSize;
}
getMaxContentSize() : T.WindowSize {
getMaxContentSize(): T.WindowSize {
if (this.lastScreenSize == null) {
let width = termWidthFromCols(80, WebShareModel.getTermFontSize());
let height = termHeightFromRows(25, WebShareModel.getTermFontSize());
return {width, height};
return { width, height };
}
let winSize = this.lastScreenSize;
let width = util.boundInt(winSize.width-50, 100, 5000);
let height = util.boundInt(winSize.height-100, 100, 5000);
return {width, height};
}
getIdealContentSize() : T.WindowSize {
if (this.lastScreenSize == null) {
let width = termWidthFromCols(80, WebShareModel.getTermFontSize());
let height = termHeightFromRows(25, WebShareModel.getTermFontSize());
return {width, height};
}
let winSize = this.lastScreenSize;
let width = util.boundInt(Math.ceil((winSize.width-50)*0.7), 100, 5000);
let height = util.boundInt(Math.ceil((winSize.height-100)*0.5), 100, 5000);
return {width, height};
let width = util.boundInt(winSize.width - 50, 100, 5000);
let height = util.boundInt(winSize.height - 100, 100, 5000);
return { width, height };
}
getSelectedLine() : number {
getIdealContentSize(): T.WindowSize {
if (this.lastScreenSize == null) {
let width = termWidthFromCols(80, WebShareModel.getTermFontSize());
let height = termHeightFromRows(25, WebShareModel.getTermFontSize());
return { width, height };
}
let winSize = this.lastScreenSize;
let width = util.boundInt(Math.ceil((winSize.width - 50) * 0.7), 100, 5000);
let height = util.boundInt(Math.ceil((winSize.height - 100) * 0.5), 100, 5000);
return { width, height };
}
getSelectedLine(): number {
return this.selectedLine.get();
}
getServerSelectedLine() : number {
getServerSelectedLine(): number {
let fullScreen = this.fullScreen.get();
if (fullScreen != null) {
return fullScreen.screen.selectedline;
}
}
setSelectedLine(lineNum : number) : void {
setSelectedLine(lineNum: number): void {
mobx.action(() => {
this.selectedLine.set(lineNum);
})();
}
updateSelectedLineIndex(delta : number) : void {
updateSelectedLineIndex(delta: number): void {
let fullScreen = this.fullScreen.get();
if (fullScreen == null) {
return;
@ -160,20 +165,20 @@ class WebShareModelClass {
this.setSelectedLine(lines[lineIndex].linenum);
}
setAnchorFields(anchorLine : number, anchorOffset : number, reason : string) : void {
setAnchorFields(anchorLine: number, anchorOffset: number, reason: string): void {
this.anchor.anchorLine = anchorLine;
this.anchor.anchorOffset = anchorOffset;
}
getAnchor() : {anchorLine : number, anchorOffset : number} {
getAnchor(): { anchorLine: number; anchorOffset: number } {
return this.anchor;
}
getTermFontSize() : number {
getTermFontSize(): number {
return 12;
}
resizeWindow(winSize : T.WindowSize) : void {
resizeWindow(winSize: T.WindowSize): void {
let cols = windowWidthToCols(winSize.width, this.getTermFontSize());
for (let lineId in this.terminals) {
let termWrap = this.terminals[lineId];
@ -181,8 +186,8 @@ class WebShareModelClass {
}
}
mergeLine(fullScreen : T.WebFullScreen, newLine : T.WebLine) {
for (let i=0; i<fullScreen.lines.length; i++) {
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;
@ -196,15 +201,15 @@ class WebShareModelClass {
fullScreen.lines.push(newLine);
}
removeLine(fullScreen : T.WebFullScreen, lineId : string) {
for (let i=0; i<fullScreen.lines.length; i++) {
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++) {
for (let i = 0; i < fullScreen.cmds.length; i++) {
let cmd = fullScreen.cmds[i];
if (cmd.lineid == lineId) {
fullScreen.cmds.splice(i, 1);
@ -214,15 +219,15 @@ class WebShareModelClass {
this.unloadRenderer(lineId);
}
setCmdDone(lineId : string) : void {
setCmdDone(lineId: string): void {
let termWrap = this.getTermWrap(lineId);
if (termWrap != null) {
termWrap.cmdDone();
}
}
mergeCmd(fullScreen : T.WebFullScreen, newCmd : T.WebCmd) {
for (let i=0; i<fullScreen.cmds.length; i++) {
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) {
let wasRunning = lineutil.cmdStatusIsRunning(cmd.status);
@ -237,7 +242,7 @@ class WebShareModelClass {
fullScreen.cmds.push(newCmd);
}
mergeUpdate(msg : T.WebFullScreen) {
mergeUpdate(msg: T.WebFullScreen) {
if (msg.screenid != this.screenId) {
console.log("bad WebFullScreen update, wrong screenid", msg.screenid);
return;
@ -272,10 +277,10 @@ class WebShareModelClass {
}
}
this.handleCmdPtyMap(msg.cmdptymap);
})();
})();
}
handleCmdPtyMap(ptyMap : Record<string, number>) {
handleCmdPtyMap(ptyMap: Record<string, number>) {
if (ptyMap == null) {
return;
}
@ -289,7 +294,7 @@ class WebShareModelClass {
}
}
runPtyFetch(lineId : string) {
runPtyFetch(lineId: string) {
let prtn = this.checkFetchPtyData(lineId, false);
let ptyListener = this.getPtyListener(lineId);
if (ptyListener != null) {
@ -302,7 +307,7 @@ class WebShareModelClass {
}
}
getPtyListener(lineId : string) {
getPtyListener(lineId: string) {
let termWrap = this.getTermWrap(lineId);
if (termWrap != null) {
return termWrap;
@ -314,7 +319,7 @@ class WebShareModelClass {
return null;
}
receivePtyData(lineId : string, ptyPos : number, data : Uint8Array, reason? : string) : void {
receivePtyData(lineId: string, ptyPos: number, data: Uint8Array, reason?: string): void {
let termWrap = this.getTermWrap(lineId);
if (termWrap != null) {
termWrap.receiveData(ptyPos, data, reason);
@ -325,7 +330,7 @@ class WebShareModelClass {
}
}
checkFetchPtyData(lineId : string, reload : boolean) : Promise<T.PtyDataType> {
checkFetchPtyData(lineId: string, reload: boolean): Promise<T.PtyDataType> {
let lineNum = this.getLineNumFromId(lineId);
if (this.activePtyFetch[lineId]) {
// console.log("check-fetch", lineNum, "already running");
@ -342,34 +347,44 @@ class WebShareModelClass {
let remotePtyOffset = this.remotePtyOffsetMap[lineId];
if (ptyOffset >= remotePtyOffset) {
// up to date
return Promise.resolve({pos: ptyOffset, data: new Uint8Array(0)});
return Promise.resolve({ pos: ptyOffset, data: new Uint8Array(0) });
}
this.activePtyFetch[lineId] = true;
let viewKey = WebShareModel.viewKey;
// console.log("fetch pty", lineNum, "pos=" + ptyOffset);
let usp = new URLSearchParams({screenid: this.screenId, viewkey: viewKey, lineid: lineId, pos: String(ptyOffset)});
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) => {
let dataArr = new Uint8Array(buf);
let newOffset = ptyOffset + dataArr.length;
// console.log("fetch pty success", lineNum, "len=" + dataArr.length, "pos => " + newOffset);
this.localPtyOffsetMap[lineId] = newOffset;
return {pos: ptyOffset, data: dataArr};
}).finally(() => {
this.activePtyFetch[lineId] = false;
let usp = new URLSearchParams({
screenid: this.screenId,
viewkey: viewKey,
lineid: lineId,
pos: String(ptyOffset),
});
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) => {
let dataArr = new Uint8Array(buf);
let newOffset = ptyOffset + dataArr.length;
// console.log("fetch pty success", lineNum, "len=" + dataArr.length, "pos => " + newOffset);
this.localPtyOffsetMap[lineId] = newOffset;
return { pos: ptyOffset, data: dataArr };
})
.finally(() => {
this.activePtyFetch[lineId] = false;
});
}
wsMessageCallback(msg : any) {
wsMessageCallback(msg: any) {
if (msg.type == "webscreen:update") {
// console.log("[ws] update vts", msg.vts);
if (msg.vts > this.remoteScreenVts) {
@ -384,7 +399,7 @@ class WebShareModelClass {
console.log("[ws] unhandled message", msg);
}
setWebFullScreen(screen : T.WebFullScreen) {
setWebFullScreen(screen: T.WebFullScreen) {
// console.log("got initial screen", "vts=" + screen.vts);
mobx.action(() => {
if (screen.lines == null) {
@ -398,13 +413,12 @@ class WebShareModelClass {
this.fullScreen.set(screen);
this.wsControl.reconnect(true);
if (this.syncSelectedLine.get()) {
this.selectedLine.set(screen.screen.selectedline);
this.selectedLine.set(screen.screen.selectedline);
}
})();
}
loadTerminalRenderer(elem : Element, line : T.WebLine, cmd : T.WebCmd, width : number) : void {
loadTerminalRenderer(elem: Element, line: T.WebLine, cmd: T.WebCmd, width: number): void {
let lineId = cmd.lineid;
let termWrap = this.getTermWrap(lineId);
if (termWrap != null) {
@ -421,14 +435,16 @@ class WebShareModelClass {
termContext: termContext,
usedRows: usedRows,
termOpts: cmd.termopts,
winSize: {height: 0, width: width},
winSize: { height: 0, width: width },
dataHandler: null,
focusHandler: (focus : boolean) => this.setTermFocus(line.linenum, focus),
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); },
onUpdateContentHeight: (termContext: T.RendererContext, height: number) => {
this.setContentHeight(termContext, height);
},
});
this.terminals[lineId] = termWrap;
if (this.localPtyOffsetMap[lineId] == null) {
@ -441,7 +457,7 @@ class WebShareModelClass {
return;
}
termCustomKeyHandler(e : any, termWrap : TermWrap) : boolean {
termCustomKeyHandler(e: any, termWrap: TermWrap): boolean {
if (e.type != "keydown" || isModKeyPress(e)) {
return false;
}
@ -466,20 +482,19 @@ class WebShareModelClass {
return false;
}
setTermFocus(lineNum : number, focus : boolean) : void {
}
setTermFocus(lineNum: number, focus: boolean): void {}
getContentHeight(context : T.RendererContext) : number {
getContentHeight(context: T.RendererContext): number {
let key = context.lineId;
return this.contentHeightCache[key];
}
setContentHeight(context : T.RendererContext, height : number) : void {
setContentHeight(context: T.RendererContext, height: number): void {
let key = context.lineId;
this.contentHeightCache[key] = height;
}
unloadRenderer(lineId : string) : void {
unloadRenderer(lineId: string): void {
let rmodel = this.renderers[lineId];
if (rmodel != null) {
rmodel.dispose();
@ -493,7 +508,7 @@ class WebShareModelClass {
delete this.localPtyOffsetMap[lineId];
}
getUsedRows(context : T.RendererContext, line : T.WebLine, cmd : T.WebCmd, width : number) : number {
getUsedRows(context: T.RendererContext, line: T.WebLine, cmd: T.WebCmd, width: number): number {
if (cmd == null) {
return 0;
}
@ -511,27 +526,27 @@ class WebShareModelClass {
if (line.contentheight != null && line.contentheight != -1) {
return line.contentheight;
}
return (lineutil.cmdStatusIsRunning(cmd.status) ? 1 : 0);
return lineutil.cmdStatusIsRunning(cmd.status) ? 1 : 0;
}
return termWrap.getUsedRows();
}
getTermWrap(lineId : string) : TermWrap {
getTermWrap(lineId: string): TermWrap {
return this.terminals[lineId];
}
getRenderer(lineId : string) : T.RendererModel {
getRenderer(lineId: string): T.RendererModel {
return this.renderers[lineId];
}
registerRenderer(lineId : string, renderer : T.RendererModel) {
registerRenderer(lineId: string, renderer: T.RendererModel) {
this.renderers[lineId] = renderer;
if (this.localPtyOffsetMap[lineId] == null) {
this.localPtyOffsetMap[lineId] = 0;
}
}
checkUpdateScreenData() : void {
checkUpdateScreenData(): void {
let fullScreen = this.fullScreen.get();
if (fullScreen == null) {
return;
@ -543,7 +558,7 @@ class WebShareModelClass {
this.loadFullScreenData(true);
}
loadFullScreenData(update : boolean) : void {
loadFullScreenData(update: boolean): void {
if (isBlank(this.screenId)) {
this.setErrMessage("No ScreenId Specified, Cannot Load.");
return;
@ -558,7 +573,7 @@ class WebShareModelClass {
}
// console.log("running screen-data update");
this.activeUpdateFetch = true;
let urlParams : Record<string, string> = {screenid: this.screenId, viewkey: this.viewKey};
let urlParams: Record<string, string> = { screenid: this.screenId, viewkey: this.viewKey };
if (update) {
let fullScreen = this.fullScreen.get();
if (fullScreen != null) {
@ -567,28 +582,31 @@ class WebShareModelClass {
}
let usp = new URLSearchParams(urlParams);
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) => {
let screen : T.WebFullScreen = data;
if (update) {
this.mergeUpdate(screen);
}
else {
this.setWebFullScreen(screen);
}
setTimeout(() => this.checkUpdateScreenData(), 300);
}).catch((err) => {
this.errMessage.set("Cannot get screen: " + err.message);
}).finally(() => {
this.activeUpdateFetch = false;
});
fetch(url, { method: "GET", mode: "cors", cache: "no-cache" })
.then((resp) => handleJsonFetchResponse(url, resp))
.then((data) => {
let screen: T.WebFullScreen = data;
if (update) {
this.mergeUpdate(screen);
} else {
this.setWebFullScreen(screen);
}
setTimeout(() => this.checkUpdateScreenData(), 300);
})
.catch((err) => {
this.errMessage.set("Cannot get screen: " + err.message);
})
.finally(() => {
this.activeUpdateFetch = false;
});
}
getLineNumFromId(lineId : string) : number {
getLineNumFromId(lineId: string): number {
let fullScreen = this.fullScreen.get();
if (fullScreen == null) {
return -1;
}
for (let i=0; i<fullScreen.lines.length; i++) {
for (let i = 0; i < fullScreen.lines.length; i++) {
let line = fullScreen.lines[i];
if (line.lineid == lineId) {
return line.linenum;
@ -597,12 +615,12 @@ class WebShareModelClass {
return -1;
}
getLineIndex(lineNum : number) : number {
getLineIndex(lineNum: number): number {
let fullScreen = this.fullScreen.get();
if (fullScreen == null) {
return -1;
}
for (let i=0; i<fullScreen.lines.length; i++) {
for (let i = 0; i < fullScreen.lines.length; i++) {
let line = fullScreen.lines[i];
if (line.linenum == lineNum) {
return i;
@ -611,7 +629,7 @@ class WebShareModelClass {
return -1;
}
getNumLines() : number {
getNumLines(): number {
let fullScreen = this.fullScreen.get();
if (fullScreen == null) {
return 0;
@ -619,7 +637,7 @@ class WebShareModelClass {
return fullScreen.lines.length;
}
getCmdById(lineId : string) : T.WebCmd {
getCmdById(lineId: string): T.WebCmd {
let fullScreen = this.fullScreen.get();
if (fullScreen == null) {
return null;
@ -632,7 +650,7 @@ class WebShareModelClass {
return null;
}
docKeyDownHandler(e : any) : void {
docKeyDownHandler(e: any): void {
if (isModKeyPress(e)) {
return;
}
@ -645,17 +663,17 @@ class WebShareModelClass {
}
}
function getTermPtyData(termContext : T.TermContextUnion) : Promise<T.PtyDataType> {
function getTermPtyData(termContext: T.TermContextUnion): Promise<T.PtyDataType> {
if ("remoteId" in termContext) {
throw new Error("remote term ptydata is not supported in webshare");
}
return WebShareModel.checkFetchPtyData(termContext.lineId, true);
}
let WebShareModel : WebShareModelClass = null;
let WebShareModel: WebShareModelClass = null;
if ((window as any).WebShareModel == null) {
WebShareModel = new WebShareModelClass();
(window as any).WebShareModel = WebShareModel;
}
export {WebShareModel, getTermPtyData};
export { WebShareModel, getTermPtyData };

View File

@ -1,50 +1,50 @@
import * as mobx from "mobx";
import {sprintf} from "sprintf-js";
import {boundMethod} from "autobind-decorator";
import {WebShareWSMessage} from "./types";
import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import { WebShareWSMessage } from "./types";
import dayjs from "dayjs";
class WebShareWSControl {
wsConn : any;
open : mobx.IObservableValue<boolean>;
opening : boolean = false;
reconnectTimes : number = 0;
msgQueue : any[] = [];
messageCallback : (any) => void = null;
screenId : string = null;
viewKey : string = null;
wsUrl : string;
closed : boolean;
constructor(wsUrl : string, screenId : string, viewKey : string, messageCallback : (any) => void) {
wsConn: any;
open: mobx.IObservableValue<boolean>;
opening: boolean = false;
reconnectTimes: number = 0;
msgQueue: any[] = [];
messageCallback: (any) => void = null;
screenId: string = null;
viewKey: string = null;
wsUrl: string;
closed: boolean;
constructor(wsUrl: string, screenId: string, viewKey: string, messageCallback: (any) => void) {
this.wsUrl = wsUrl;
this.messageCallback = messageCallback;
this.screenId = screenId;
this.viewKey = viewKey;
this.open = mobx.observable.box(false, {name: "WSOpen"});
this.open = mobx.observable.box(false, { name: "WSOpen" });
this.closed = true;
setInterval(this.sendPing, 20000);
}
close() : void {
close(): void {
this.closed = true;
if (this.wsConn != null) {
this.wsConn.close();
}
}
log(str : string) {
log(str: string) {
console.log("[wscontrol]", str);
}
@mobx.action
setOpen(val : boolean) {
setOpen(val: boolean) {
mobx.action(() => {
this.open.set(val);
})();
}
connectNow(desc : string) {
connectNow(desc: string) {
this.closed = false;
if (this.open.get()) {
return;
@ -59,7 +59,7 @@ class WebShareWSControl {
// this.wsConn.onerror = this.onerror;
}
reconnect(forceClose? : boolean) {
reconnect(forceClose?: boolean) {
this.closed = false;
if (this.open.get()) {
if (forceClose) {
@ -73,7 +73,7 @@ class WebShareWSControl {
return;
}
let timeoutArr = [0, 0, 5, 5, 15, 30, 60, 300, 3600];
let timeout = timeoutArr[timeoutArr.length-1];
let timeout = timeoutArr[timeoutArr.length - 1];
if (this.reconnectTimes < timeoutArr.length) {
timeout = timeoutArr[this.reconnectTimes];
}
@ -82,16 +82,15 @@ class WebShareWSControl {
}
setTimeout(() => {
this.connectNow(String(this.reconnectTimes));
}, timeout*1000);
}, timeout * 1000);
}
@boundMethod
onclose(event : any) {
onclose(event: any) {
// console.log("close", event);
if (event.wasClean) {
this.log("connection closed");
}
else {
} else {
this.log("connection error/disconnected");
}
if (this.open.get() || this.opening) {
@ -128,7 +127,7 @@ class WebShareWSControl {
}
@boundMethod
onmessage(event : any) {
onmessage(event: any) {
let eventData = null;
if (event.data != null) {
eventData = JSON.parse(event.data);
@ -137,7 +136,7 @@ class WebShareWSControl {
return;
}
if (eventData.type == "ping") {
this.wsConn.send(JSON.stringify({type: "pong", stime: Date.now()}));
this.wsConn.send(JSON.stringify({ type: "pong", stime: Date.now() }));
return;
}
if (eventData.type == "pong") {
@ -151,8 +150,7 @@ class WebShareWSControl {
if (this.messageCallback) {
try {
this.messageCallback(eventData);
}
catch (e) {
} catch (e) {
this.log("[error] messageCallback " + e);
}
}
@ -163,17 +161,17 @@ class WebShareWSControl {
if (!this.open.get()) {
return;
}
this.wsConn.send(JSON.stringify({type: "ping", stime: Date.now()}));
this.wsConn.send(JSON.stringify({ type: "ping", stime: Date.now() }));
}
sendMessage(data : any) {
sendMessage(data: any) {
if (!this.open.get()) {
return;
}
this.wsConn.send(JSON.stringify(data));
}
pushMessage(data : any) {
pushMessage(data: any) {
if (!this.open.get()) {
this.msgQueue.push(data);
return;
@ -182,9 +180,9 @@ class WebShareWSControl {
}
sendWebShareInit() {
let pk : WebShareWSMessage = {"type": "webshare", screenid: this.screenId, viewkey: this.viewKey};
let pk: WebShareWSMessage = { type: "webshare", screenid: this.screenId, viewkey: this.viewKey };
this.pushMessage(pk);
}
}
export {WebShareWSControl};
export { WebShareWSControl };

View File

@ -1,7 +1,7 @@
body.prompt-webshare #main {
display: flex;
flex-direction: column;
.logo-header {
display: flex;
flex-direction: row;
@ -97,9 +97,9 @@ body.prompt-webshare #main {
display: block;
}
}
color: white;
#app {
color: white;
}

View File

@ -1,10 +1,10 @@
import * as mobx from "mobx";
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";
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";
import * as textmeasure from "./textmeasure";
// @ts-ignore
@ -23,8 +23,7 @@ document.addEventListener("DOMContentLoaded", () => {
let isFontLoaded = document.fonts.check("12px 'JetBrains Mono'");
if (isFontLoaded) {
root.render(reactElem);
}
else {
} else {
document.fonts.ready.then(() => {
root.render(reactElem);
});

View File

@ -1,50 +1,50 @@
import * as mobx from "mobx";
import {sprintf} from "sprintf-js";
import {boundMethod} from "autobind-decorator";
import {WatchScreenPacketType} from "./types";
import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import { WatchScreenPacketType } from "./types";
import dayjs from "dayjs";
class WSControl {
wsConn : any;
open : mobx.IObservableValue<boolean>;
opening : boolean = false;
reconnectTimes : number = 0;
msgQueue : any[] = [];
clientId : string;
messageCallback : (any) => void = null;
watchSessionId : string = null;
watchScreenId : string = null;
wsLog : mobx.IObservableArray<string> = mobx.observable.array([], {name: "wsLog"})
authKey : string;
baseHostPort : string;
constructor(baseHostPort : string, clientId : string, authKey : string, messageCallback : (any) => void) {
wsConn: any;
open: mobx.IObservableValue<boolean>;
opening: boolean = false;
reconnectTimes: number = 0;
msgQueue: any[] = [];
clientId: string;
messageCallback: (any) => void = null;
watchSessionId: string = null;
watchScreenId: string = null;
wsLog: mobx.IObservableArray<string> = mobx.observable.array([], { name: "wsLog" });
authKey: string;
baseHostPort: string;
constructor(baseHostPort: string, clientId: string, authKey: string, messageCallback: (any) => void) {
this.baseHostPort = baseHostPort;
this.messageCallback = messageCallback;
this.clientId = clientId;
this.authKey = authKey;
this.open = mobx.observable.box(false, {name: "WSOpen"});
this.open = mobx.observable.box(false, { name: "WSOpen" });
setInterval(this.sendPing, 5000);
}
log(str : string) {
log(str: string) {
mobx.action(() => {
let ts = dayjs().format("YYYY-MM-DD HH:mm:ss");
this.wsLog.push("[" + ts + "] " + str);
if (this.wsLog.length > 50) {
this.wsLog.splice(0, this.wsLog.length-50);
this.wsLog.splice(0, this.wsLog.length - 50);
}
})();
}
@mobx.action
setOpen(val : boolean) {
setOpen(val: boolean) {
mobx.action(() => {
this.open.set(val);
})();
}
connectNow(desc : string) {
connectNow(desc: string) {
if (this.open.get()) {
return;
}
@ -58,7 +58,7 @@ class WSControl {
// this.wsConn.onerror = this.onerror;
}
reconnect(forceClose? : boolean) {
reconnect(forceClose?: boolean) {
if (this.open.get()) {
if (forceClose) {
this.wsConn.close(); // this will force a reconnect
@ -80,16 +80,15 @@ class WSControl {
}
setTimeout(() => {
this.connectNow(String(this.reconnectTimes));
}, timeout*1000);
}, timeout * 1000);
}
@boundMethod
onclose(event : any) {
onclose(event: any) {
// console.log("close", event);
if (event.wasClean) {
this.log("connection closed");
}
else {
} else {
this.log("connection error/disconnected");
}
if (this.open.get() || this.opening) {
@ -124,7 +123,7 @@ class WSControl {
}
@boundMethod
onmessage(event : any) {
onmessage(event: any) {
let eventData = null;
if (event.data != null) {
eventData = JSON.parse(event.data);
@ -133,7 +132,7 @@ class WSControl {
return;
}
if (eventData.type == "ping") {
this.wsConn.send(JSON.stringify({type: "pong", stime: Date.now()}));
this.wsConn.send(JSON.stringify({ type: "pong", stime: Date.now() }));
return;
}
if (eventData.type == "pong") {
@ -147,8 +146,7 @@ class WSControl {
if (this.messageCallback) {
try {
this.messageCallback(eventData);
}
catch (e) {
} catch (e) {
console.log("[error] messageCallback", e);
}
}
@ -159,17 +157,17 @@ class WSControl {
if (!this.open.get()) {
return;
}
this.wsConn.send(JSON.stringify({type: "ping", stime: Date.now()}));
this.wsConn.send(JSON.stringify({ type: "ping", stime: Date.now() }));
}
sendMessage(data : any) {
sendMessage(data: any) {
if (!this.open.get()) {
return;
}
this.wsConn.send(JSON.stringify(data));
}
pushMessage(data : any) {
pushMessage(data: any) {
if (!this.open.get()) {
this.msgQueue.push(data);
return;
@ -177,8 +175,14 @@ class WSControl {
this.sendMessage(data);
}
sendWatchScreenPacket(connect : boolean) {
let pk : WatchScreenPacketType = {"type": "watchscreen", connect: connect, sessionid: null, screenid: null, authkey: this.authKey};
sendWatchScreenPacket(connect: boolean) {
let pk: WatchScreenPacketType = {
type: "watchscreen",
connect: connect,
sessionid: null,
screenid: null,
authkey: this.authKey,
};
if (this.watchSessionId != null) {
pk.sessionid = this.watchSessionId;
}
@ -189,11 +193,11 @@ class WSControl {
}
// these params can be null. (null, null) means stop watching
watchScreen(sessionId : string, screenId : string) {
watchScreen(sessionId: string, screenId: string) {
this.watchSessionId = sessionId;
this.watchScreenId = screenId;
this.sendWatchScreenPacket(false);
}
}
export {WSControl};
export { WSControl };

View File

@ -1,5 +1,5 @@
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const LodashModuleReplacementPlugin = require("lodash-webpack-plugin");
const path = require("path");
module.exports = {
@ -23,18 +23,20 @@ module.exports = {
[
"@babel/preset-env",
{
targets: "defaults and not ie > 0 and not op_mini all and not op_mob > 0 and not kaios > 0 and not and_qq > 0 and not and_uc > 0 and not baidu > 0",
targets:
"defaults and not ie > 0 and not op_mini all and not op_mob > 0 and not kaios > 0 and not and_qq > 0 and not and_uc > 0 and not baidu > 0",
},
],
"@babel/preset-react",
"@babel/preset-typescript"],
"@babel/preset-typescript",
],
plugins: [
["@babel/transform-runtime", {"regenerator": true}],
["@babel/transform-runtime", { regenerator: true }],
"@babel/plugin-transform-react-jsx",
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose": true }],
["@babel/plugin-proposal-private-methods", { "loose": true }],
["@babel/plugin-proposal-private-property-in-object", { "loose": true }],
["@babel/plugin-proposal-decorators", { legacy: true }],
["@babel/plugin-proposal-class-properties", { loose: true }],
["@babel/plugin-proposal-private-methods", { loose: true }],
["@babel/plugin-proposal-private-property-in-object", { loose: true }],
"babel-plugin-jsx-control-statements",
],
},
@ -42,26 +44,19 @@ module.exports = {
},
{
test: /\.css$/,
use: [
"style-loader",
"css-loader",
],
use: ["style-loader", "css-loader"],
},
{
test: /\.less$/,
use: [
{loader: MiniCssExtractPlugin.loader},
"css-loader",
"less-loader"
]
use: [{ loader: MiniCssExtractPlugin.loader }, "css-loader", "less-loader"],
},
]
],
},
plugins: [
new MiniCssExtractPlugin({filename: "[name].css", ignoreOrder: true}),
new MiniCssExtractPlugin({ filename: "[name].css", ignoreOrder: true }),
new LodashModuleReplacementPlugin(),
],
resolve: {
extensions: ['.ts', '.tsx', '.js', '.mjs', '.cjs', '.wasm', '.json', '.less', '.css']
extensions: [".ts", ".tsx", ".js", ".mjs", ".cjs", ".wasm", ".json", ".less", ".css"],
},
};

View File

@ -1,7 +1,7 @@
const webpack = require('webpack');
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
const path = require('path');
const webpack = require("webpack");
const merge = require("webpack-merge");
const common = require("./webpack.common.js");
const path = require("path");
const CopyPlugin = require("copy-webpack-plugin");
const VERSION = require("./version.js");
@ -18,7 +18,7 @@ var merged = merge.merge(common, {
},
port: 9000,
headers: {
'Cache-Control': 'no-store',
"Cache-Control": "no-store",
},
},
watchOptions: {
@ -34,4 +34,3 @@ var definePlugin = new webpack.DefinePlugin({
merged.plugins.push(definePlugin);
module.exports = merged;

View File

@ -9,10 +9,10 @@ module.exports = {
target: "electron-main",
output: {
path: path.resolve(__dirname, "dist-dev"),
filename: "[name].js"
filename: "[name].js",
},
externals: {
"fs": "require('fs')",
fs: "require('fs')",
"fs-ext": "require('fs-ext')",
},
module: {
@ -27,31 +27,33 @@ module.exports = {
[
"@babel/preset-env",
{
targets: "defaults and not ie > 0 and not op_mini all and not op_mob > 0 and not kaios > 0 and not and_qq > 0 and not and_uc > 0 and not baidu > 0",
targets:
"defaults and not ie > 0 and not op_mini all and not op_mob > 0 and not kaios > 0 and not and_qq > 0 and not and_uc > 0 and not baidu > 0",
},
],
"@babel/preset-react",
"@babel/preset-typescript"],
"@babel/preset-typescript",
],
plugins: [
["@babel/transform-runtime", {"regenerator": true}],
["@babel/transform-runtime", { regenerator: true }],
"@babel/plugin-transform-react-jsx",
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose": true }],
["@babel/plugin-proposal-private-methods", { "loose": true }],
["@babel/plugin-proposal-private-property-in-object", { "loose": true }],
["@babel/plugin-proposal-decorators", { legacy: true }],
["@babel/plugin-proposal-class-properties", { loose: true }],
["@babel/plugin-proposal-private-methods", { loose: true }],
["@babel/plugin-proposal-private-property-in-object", { loose: true }],
"babel-plugin-jsx-control-statements",
],
},
},
},
]
],
},
plugins: [
new CopyPlugin({
patterns: [{from: "src/preload.js", to: "preload.js"}],
patterns: [{ from: "src/preload.js", to: "preload.js" }],
}),
],
resolve: {
extensions: ['.ts', '.tsx', '.js']
extensions: [".ts", ".tsx", ".js"],
},
}
};

View File

@ -1,8 +1,8 @@
const webpack = require('webpack');
const merge = require('webpack-merge');
const common = require('./webpack.electron.js');
const webpack = require("webpack");
const merge = require("webpack-merge");
const common = require("./webpack.electron.js");
const moment = require("dayjs");
const VERSION = require('./version.js');
const VERSION = require("./version.js");
const path = require("path");
function makeBuildStr() {
@ -17,7 +17,7 @@ let merged = merge.merge(common, {
mode: "production",
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].js"
filename: "[name].js",
},
devtool: "source-map",
optimization: {
@ -25,10 +25,12 @@ let merged = merge.merge(common, {
},
});
merged.plugins.push(new webpack.DefinePlugin({
__PROMPT_DEV__: "false",
__PROMPT_VERSION__: JSON.stringify(VERSION),
__PROMPT_BUILD__: JSON.stringify(BUILD),
}));
merged.plugins.push(
new webpack.DefinePlugin({
__PROMPT_DEV__: "false",
__PROMPT_VERSION__: JSON.stringify(VERSION),
__PROMPT_BUILD__: JSON.stringify(BUILD),
})
);
module.exports = merged;

View File

@ -1,6 +1,6 @@
const webpack = require('webpack');
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
const webpack = require("webpack");
const merge = require("webpack-merge");
const common = require("./webpack.common.js");
const moment = require("dayjs");
const path = require("path");
const VERSION = require("./version.js");
@ -15,7 +15,7 @@ const BUILD = makeBuildStr();
let BundleAnalyzerPlugin = null;
if (process.env.WEBPACK_ANALYZE) {
BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
}
let merged = merge.merge(common, {
@ -33,13 +33,12 @@ let merged = merge.merge(common, {
if (BundleAnalyzerPlugin != null) {
merged.plugins.push(new BundleAnalyzerPlugin());
}
merged.plugins.push(new webpack.DefinePlugin({
__PROMPT_DEV__: "false",
__PROMPT_VERSION__: JSON.stringify(VERSION),
__PROMPT_BUILD__: JSON.stringify(BUILD),
}));
merged.plugins.push(
new webpack.DefinePlugin({
__PROMPT_DEV__: "false",
__PROMPT_VERSION__: JSON.stringify(VERSION),
__PROMPT_BUILD__: JSON.stringify(BUILD),
})
);
module.exports = merged;

View File

@ -1,7 +1,7 @@
const webpack = require('webpack');
const merge = require('webpack-merge');
const common = require('./webpack.share.js');
const path = require('path');
const webpack = require("webpack");
const merge = require("webpack-merge");
const common = require("./webpack.share.js");
const path = require("path");
const CopyPlugin = require("copy-webpack-plugin");
const VERSION = require("./version.js");
@ -18,10 +18,10 @@ var merged = merge.merge(common, {
},
port: 9001,
headers: {
'Cache-Control': 'no-store',
"Cache-Control": "no-store",
},
devMiddleware: {
publicPath: '/dist-dev/'
publicPath: "/dist-dev/",
},
allowedHosts: "all",
hot: false,
@ -42,4 +42,3 @@ var definePlugin = new webpack.DefinePlugin({
merged.plugins.push(definePlugin);
module.exports = merged;

View File

@ -1,5 +1,5 @@
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const LodashModuleReplacementPlugin = require("lodash-webpack-plugin");
const path = require("path");
module.exports = {
@ -23,18 +23,20 @@ module.exports = {
[
"@babel/preset-env",
{
targets: "defaults and not ie > 0 and not op_mini all and not op_mob > 0 and not kaios > 0 and not and_qq > 0 and not and_uc > 0 and not baidu > 0",
targets:
"defaults and not ie > 0 and not op_mini all and not op_mob > 0 and not kaios > 0 and not and_qq > 0 and not and_uc > 0 and not baidu > 0",
},
],
"@babel/preset-react",
"@babel/preset-typescript"],
"@babel/preset-typescript",
],
plugins: [
["@babel/transform-runtime", {"regenerator": true}],
["@babel/transform-runtime", { regenerator: true }],
"@babel/plugin-transform-react-jsx",
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose": true }],
["@babel/plugin-proposal-private-methods", { "loose": true }],
["@babel/plugin-proposal-private-property-in-object", { "loose": true }],
["@babel/plugin-proposal-decorators", { legacy: true }],
["@babel/plugin-proposal-class-properties", { loose: true }],
["@babel/plugin-proposal-private-methods", { loose: true }],
["@babel/plugin-proposal-private-property-in-object", { loose: true }],
"babel-plugin-jsx-control-statements",
],
},
@ -42,26 +44,19 @@ module.exports = {
},
{
test: /\.css$/,
use: [
"style-loader",
"css-loader",
],
use: ["style-loader", "css-loader"],
},
{
test: /\.less$/,
use: [
{loader: MiniCssExtractPlugin.loader},
"css-loader",
"less-loader"
]
use: [{ loader: MiniCssExtractPlugin.loader }, "css-loader", "less-loader"],
},
]
],
},
plugins: [
new MiniCssExtractPlugin({filename: "[name].css", ignoreOrder: true}),
new MiniCssExtractPlugin({ filename: "[name].css", ignoreOrder: true }),
new LodashModuleReplacementPlugin(),
],
resolve: {
extensions: ['.ts', '.tsx', '.js', '.mjs', '.cjs', '.wasm', '.json', '.less', '.css']
extensions: [".ts", ".tsx", ".js", ".mjs", ".cjs", ".wasm", ".json", ".less", ".css"],
},
};

View File

@ -1,6 +1,6 @@
const webpack = require('webpack');
const merge = require('webpack-merge');
const common = require('./webpack.share.js');
const webpack = require("webpack");
const merge = require("webpack-merge");
const common = require("./webpack.share.js");
const moment = require("dayjs");
const path = require("path");
const VERSION = require("./version.js");
@ -25,12 +25,14 @@ let merged = merge.merge(common, {
},
});
merged.plugins.push(new webpack.DefinePlugin({
__PROMPT_DEV__: "false",
__PROMPT_VERSION__: JSON.stringify(VERSION),
__PROMPT_BUILD__: JSON.stringify(BUILD),
__PROMPT_API_ENDPOINT__: JSON.stringify("https://share.getprompt.dev/api"),
__PROMPT_WSAPI_ENDPOINT__: JSON.stringify("wss://wsapi.getprompt.dev"),
}));
merged.plugins.push(
new webpack.DefinePlugin({
__PROMPT_DEV__: "false",
__PROMPT_VERSION__: JSON.stringify(VERSION),
__PROMPT_BUILD__: JSON.stringify(BUILD),
__PROMPT_API_ENDPOINT__: JSON.stringify("https://share.getprompt.dev/api"),
__PROMPT_WSAPI_ENDPOINT__: JSON.stringify("wss://wsapi.getprompt.dev"),
})
);
module.exports = merged;