Pe 179 refactoring UI code (#25)

* refactored main JS files

* modals have been refactored

* lines and renderer refactored

* all refactoring done. ready for reskinning
This commit is contained in:
anandamarsh 2023-09-23 21:05:06 -07:00 committed by GitHub
parent faeab0790a
commit a25c7b4286
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 3927 additions and 3403 deletions

View File

@ -12,7 +12,6 @@ node_modules/.bin/webpack --watch --config webpack.dev.js
node_modules/.bin/webpack --config webpack.dev.js
```
```bash
# @scripthaus command webpack-electron-watch
# @scripthaus cd :playbook
@ -64,7 +63,7 @@ node_modules/.bin/webpack --config webpack.share.prod.js
```bash
# @scripthaus command typecheck
# @scripthaus cd :playbook
node_modules/.bin/tsc --jsx preserve --noEmit --esModuleInterop --target ES5 --experimentalDecorators --downlevelIteration src/prompt.ts
node_modules/.bin/tsc --jsx preserve --noEmit --esModuleInterop --target ES5 --experimentalDecorators --downlevelIteration src/index.ts
```
```bash

View File

@ -1,3 +1,44 @@
@import "../index.less";
.info-message {
position: relative;
font-weight: normal;
font-size: 12px;
color: @term-white;
.message-content {
position: absolute;
display: none;
flex-direction: row;
align-items: flex-start;
top: -6px;
left: -6px;
padding: 5px;
border: 1px solid #777;
background-color: #444;
border-radius: 5px;
z-index: 5;
overflow: hidden;
.info-icon {
margin-right: 5px;
width: 12px;
flex-shrink: 0;
}
.info-children {
flex: 1 0 0;
overflow: hidden;
}
}
&:hover {
.message-content {
display: flex;
}
}
}
.cmdstr-code {
position: relative;
display: flex;

View File

@ -1,13 +1,14 @@
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 cn from "classnames";
import { If, For, When, Otherwise, Choose } from "tsx-control-statements/components";
import type { RemoteType } from "./types";
import { If } from "tsx-control-statements/components";
import type { RemoteType } from "../types";
import "./common.less";
type OV<V> = mobx.IObservableValue<V>;

View File

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

641
src/index.less Normal file
View File

@ -0,0 +1,641 @@
@term-black: #000000;
@term-red: #cc0000;
@term-green: #4e9a06;
@term-yellow: #c4a000;
@term-blue: #3465a4;
@term-magenta: #75507b;
@term-cyan: #06989a;
@term-white: #d3d7cf;
@term-bright-black: #555753;
@term-bright-red: #ef2929;
@term-bright-green: #8ae234;
@term-bright-yellow: #fce94f;
@term-bright-blue: #32afff;
@term-bright-magenta: #ad7fa8;
@term-bright-cyan: #34e2e2;
@term-bright-white: #ffffff;
@tab-black: rgb(0, 0, 0);
@tab-red: rgb(205, 49, 49);
@tab-green: rgb(0, 128, 0);
@tab-orange: rgb(255, 199, 6);
@tab-yellow: rgb(229, 229, 16);
@tab-blue: rgb(0, 71, 171);
@tab-magenta: rgb(188, 63, 188);
@tab-cyan: rgb(17, 168, 205);
@tab-white: rgb(249, 249, 249);
@tab-black-text: #333;
@tab-white-text: #d7d7d7;
@prompt-green: rgb(0, 177, 10);
@soft-blue: #729fcf;
@active-menu-color: rgb(0, 71, 171);
@import "utils.less";
// global settings / overrides
:root {
--fa-style-family: "Font Awesome 6 Sharp";
}
html,
body,
#app,
#main {
background-color: #000;
height: 100vh;
}
.content {
a:hover {
color: #485fc7;
}
}
body {
overflow: hidden;
}
body::-webkit-scrollbar {
display: none;
}
*::-webkit-scrollbar {
background-color: #777;
width: 5px;
height: 5px;
}
*::-webkit-scrollbar-thumb {
background: white;
}
input[type="checkbox"] {
cursor: pointer;
}
// main layout
#main {
height: 100vh;
display: flex;
flex-direction: column;
.main-content {
display: flex;
flex-direction: row;
background-color: black;
height: 100%;
.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;
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;
}
.terminal-wrapper {
position: relative;
.term-block {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: transparent;
z-index: 10;
}
.xterm-screen {
&::-webkit-scrollbar {
display: none;
}
}
&.focus .xterm {
.xterm-screen {
overflow-y: scroll;
overscroll-behavior: contain;
}
.xterm-viewport {
overscroll-behavior: contain;
}
}
&.focus .xterm-viewport {
&::-webkit-scrollbar {
background-color: #777;
width: 5px;
height: 5px;
}
&::-webkit-scrollbar-thumb {
background: white;
}
}
.xterm-viewport {
&::-webkit-scrollbar {
background-color: #222;
width: 5px;
height: 5px;
}
&::-webkit-scrollbar-thumb {
background: #555;
}
}
}
body .xterm .xterm-viewport {
overflow-y: auto;
width: calc(100% + 5px);
}
.checkbox-toggle {
position: relative;
display: inline-block;
width: 40px;
height: 23px;
input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
content: "";
cursor: pointer;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: #333;
transition: 0.5s;
border-radius: 33px;
}
.slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: 0.5s;
border-radius: 50%;
}
input:checked + .slider {
background-color: @term-green;
}
input:checked + .slider:before {
transform: translateX(18px);
}
}
.button.is-prompt-green {
background-color: #222;
color: @term-white;
&:hover {
background-color: @term-green;
color: @term-bright-white;
}
}
.button.is-plain,
.button.is-prompt-cancel {
background-color: #222;
color: @term-white;
&:hover {
background-color: #666;
color: @term-bright-white;
}
}
.button.is-prompt-danger {
background-color: #222;
color: @term-white;
&:hover {
background-color: @tab-red;
color: @term-bright-white;
}
}
.button.is-inline-height {
height: 22px;
}
.button input.confirm-checkbox {
margin-right: 5px;
}
.copied-indicator {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: white;
opacity: 0;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
pointer-events: none;
.mono-font(12px);
animation-name: fade-in-out;
animation-duration: 0.3s;
}
.loading-spinner {
display: inline-block;
position: absolute;
top: calc(40% - 8px);
left: 30px;
width: 20px;
height: 20px;
div {
box-sizing: border-box;
display: block;
position: absolute;
width: 16px;
height: 16px;
margin: 2px;
border: 2px solid #777;
border-radius: 50%;
animation: loader-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: #777 transparent transparent transparent;
}
div:nth-child(1) {
animation-delay: -0.45s;
}
div:nth-child(2) {
animation-delay: -0.3s;
}
div:nth-child(3) {
animation-delay: -0.15s;
}
}
#measure {
position: absolute;
z-index: -1;
top: -5000px;
.mono {
.mono-font();
}
.pre {
white-space: pre;
}
}
.text-button {
.mono-font(12px, 700);
color: @term-white;
cursor: pointer;
background-color: #171717;
outline: 2px solid #171717;
&: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 {
color: @term-red;
}
}
&.success-button {
color: @term-green;
&:hover {
color: @term-green;
}
}
&.error-button {
color: @term-red;
&:hover {
color: @term-red;
}
}
&.grey-button {
color: #666;
&:hover {
color: #666;
}
}
&.disabled-button {
&:hover,
&:focus {
outline: none;
background-color: #171717;
}
cursor: default;
}
}
.focus-indicator {
position: absolute;
display: none;
width: 5px;
border-radius: 3px;
height: calc(100% - 20px);
top: 10px;
left: 0;
z-index: 8;
&.selected {
display: block;
background-color: #666 !important;
}
&.active,
&.active.selected {
display: block;
background-color: @tab-blue !important;
}
&.active.selected.fg-focus {
display: block;
background-color: @tab-green !important;
}
}
.focus-parent:hover .focus-indicator {
display: block;
background-color: #222;
}
#main .term-prompt {
font-size: 14px;
i {
margin-right: 3px;
}
.term-prompt-branch {
color: @term-white;
}
.term-prompt-python {
color: @term-bright-magenta;
}
.term-prompt-remote {
i {
margin-right: 0;
}
}
.term-prompt-remote {
color: @term-bright-green;
&.color-green {
color: @term-bright-green;
}
&.color-red {
color: @term-bright-red;
}
&.color-blue {
color: @term-bright-blue;
}
&.color-yellow {
color: @term-bright-yellow;
}
&.color-magenta {
color: @term-bright-magenta;
}
&.color-cyan {
color: @term-bright-cyan;
}
&.color-white {
color: @term-bright-white;
}
&.color-orange {
color: @tab-orange;
}
}
.term-prompt-cwd {
color: @term-bright-green;
}
.term-prompt-end {
color: @term-bright-green;
}
}
.remote-status {
font-size: 8px;
margin-right: 5px;
position: relative;
top: -3px;
color: #c4a000;
&.status-init,
&.status-disconnected {
color: #c4a000;
}
&.status-connecting {
color: #c4a000;
}
&.status-connected {
color: #4e9a06;
}
&.status-error {
color: #cc0000;
}
}
@keyframes loader-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes fade-in {
0% {
opacity: 0;
}
80% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fade-in-out {
0% {
opacity: 0;
}
50% {
opacity: 0.5;
}
100% {
opacity: 0;
}
}

View File

@ -2,10 +2,7 @@ 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 { Main } from "./main/Main";
import { loadFonts } from "./util";
import * as DOMPurify from "dompurify";

View File

@ -1,36 +0,0 @@
@keyframes loader-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes fade-in {
0% {
opacity: 0;
}
80% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fade-in-out {
0% {
opacity: 0;
}
50% {
opacity: 0.5;
}
100% {
opacity: 0;
}
}

File diff suppressed because it is too large Load Diff

140
src/main/Main.tsx Normal file
View File

@ -0,0 +1,140 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { If } from "tsx-control-statements/components";
import dayjs from "dayjs";
import type { ContextMenuOpts } from "../types";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel } from "../model";
import { isBlank } from "../util";
import { BookmarksView } from "./bookmarks/bookmarks";
import { WebShareView } from "../webshare/webshare-client-view";
import { HistoryView } from "./history/history";
import { ScreenSettingsModal, SessionSettingsModal, LineSettingsModal, ClientSettingsModal } from "./modals/settings";
import { RemotesModal } from "../remotes/remotes";
import { TosModal } from "./modals/Modals";
import { SessionView } from "./sessionview/SessionView";
import { MainSideBar } from "./sidebar/MainSideBar";
import { DisconnectedModal, ClientStopModal, AlertModal, WelcomeModal } from "./modals/Modals";
dayjs.extend(localizedFormat);
type OV<V> = mobx.IObservableValue<V>;
@mobxReact.observer
class Main extends React.Component<{}, {}> {
dcWait: OV<boolean> = mobx.observable.box(false, { name: "dcWait" });
constructor(props: any) {
super(props);
}
@boundMethod
handleContextMenu(e: any) {
let isInNonTermInput = false;
let activeElem = document.activeElement;
if (activeElem != null && activeElem.nodeName == "TEXTAREA") {
if (!activeElem.classList.contains("xterm-helper-textarea")) {
isInNonTermInput = true;
}
}
if (activeElem != null && activeElem.nodeName == "INPUT" && activeElem.getAttribute("type") == "text") {
isInNonTermInput = true;
}
let opts: ContextMenuOpts = {};
if (isInNonTermInput) {
opts.showCut = true;
}
let sel = window.getSelection();
if (!isBlank(sel.toString())) {
GlobalModel.contextEditMenu(e, opts);
} else {
if (isInNonTermInput) {
GlobalModel.contextEditMenu(e, opts);
}
}
}
@boundMethod
updateDcWait(val: boolean): void {
mobx.action(() => {
this.dcWait.set(val);
})();
}
render() {
let screenSettingsModal = GlobalModel.screenSettingsModal.get();
let sessionSettingsModal = GlobalModel.sessionSettingsModal.get();
let lineSettingsModal = GlobalModel.lineSettingsModal.get();
let clientSettingsModal = GlobalModel.clientSettingsModal.get();
let remotesModal = GlobalModel.remotesModalModel.isOpen();
let disconnected = !GlobalModel.ws.open.get() || !GlobalModel.localServerRunning.get();
let hasClientStop = GlobalModel.getHasClientStop();
let dcWait = this.dcWait.get();
if (disconnected || hasClientStop) {
if (!dcWait) {
setTimeout(() => this.updateDcWait(true), 1500);
}
return (
<div id="main" onContextMenu={this.handleContextMenu}>
<div className="main-content">
<MainSideBar />
<div className="session-view" />
</div>
<If condition={dcWait}>
<If condition={disconnected}>
<DisconnectedModal />
</If>
<If condition={!disconnected && hasClientStop}>
<ClientStopModal />
</If>
</If>
</div>
);
}
if (dcWait) {
setTimeout(() => this.updateDcWait(false), 0);
}
return (
<div id="main" onContextMenu={this.handleContextMenu}>
<div className="main-content">
<MainSideBar />
<SessionView />
<HistoryView />
<BookmarksView />
<WebShareView />
</div>
<AlertModal />
<If condition={GlobalModel.needsTos()}>
<TosModal />
</If>
<If condition={GlobalModel.welcomeModalOpen.get()}>
<WelcomeModal />
</If>
<If condition={screenSettingsModal != null}>
<ScreenSettingsModal
key={screenSettingsModal.sessionId + ":" + screenSettingsModal.screenId}
sessionId={screenSettingsModal.sessionId}
screenId={screenSettingsModal.screenId}
/>
</If>
<If condition={sessionSettingsModal != null}>
<SessionSettingsModal key={sessionSettingsModal} sessionId={sessionSettingsModal} />
</If>
<If condition={lineSettingsModal != null}>
<LineSettingsModal key={String(lineSettingsModal)} linenum={lineSettingsModal} />
</If>
<If condition={clientSettingsModal}>
<ClientSettingsModal />
</If>
<If condition={remotesModal}>
<RemotesModal model={GlobalModel.remotesModalModel} />
</If>
</div>
);
}
}
export { Main };

View File

@ -0,0 +1,99 @@
@import "../../index.less";
.bookmarks-view {
.bookmarks-list {
color: white;
margin: 4px 10px 5px 5px;
.no-bookmarks {
color: @term-white;
padding: 30px 10px 35px 10px;
border-bottom: 1px solid white;
}
}
.bookmark {
border-bottom: 1px solid #777;
padding: 4px 5px 8px 15px;
margin-bottom: 4px;
position: relative;
display: flex;
flex-direction: row;
max-width: 100%;
.focus-indicator {
top: 0;
height: calc(100% - 4px);
}
&.is-editing {
padding-top: 8px;
padding-bottom: 12px;
}
&.pending-delete {
background-color: #522;
}
.bookmark-content,
.bookmark-edit {
flex-grow: 1;
max-width: calc(100% - 50px);
}
label {
color: white;
margin-bottom: 4px;
}
textarea {
width: 80%;
min-width: 50%;
color: white;
background-color: black;
&.mono-font {
.mono-font(12px);
}
}
.bookmark-id-div {
display: none;
position: absolute;
color: #666;
right: 5px;
bottom: 2px;
.mono-font(8px);
}
&:hover .bookmark-id-div {
display: block;
}
.bookmark-controls {
display: flex;
flex-direction: row;
font-size: 16px;
visibility: hidden;
color: @term-white;
.bookmark-control:first-child {
margin-left: 0;
}
.bookmark-control {
margin-left: 10px;
cursor: pointer;
padding: 2px;
&:hover {
color: @term-bright-white;
}
}
}
&:hover .bookmark-controls {
visibility: visible;
}
}
}

View File

@ -1,15 +1,14 @@
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 dayjs from "dayjs";
import { If, For, When, Otherwise, Choose } from "tsx-control-statements/components";
import { If, For } 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 } from "../../model";
import { CmdStrCode, Markdown } from "../../common/common";
import "./bookmarks.less";
@mobxReact.observer
class Bookmark extends React.Component<{ bookmark: BookmarkType }, {}> {

View File

@ -1,57 +1,4 @@
.alt-view {
background-color: #111;
overflow-y: auto;
flex-grow: 1;
.alt-title {
margin: 20px 10px 0px 5px;
padding-left: 10px;
padding-bottom: 12px;
.mono-font(1.5rem);
color: @term-bright-white;
border-bottom: 1px solid white;
}
.alt-list {
color: white;
margin: 4px 10px 5px 5px;
border-bottom: 1px solid white;
}
.no-content {
color: @term-white;
padding: 30px 10px 35px 10px;
border-bottom: 1px solid white;
}
.close-button {
position: absolute;
padding: 4px;
font-size: 24px;
color: #aaa;
right: 15px;
top: 18px;
cursor: pointer;
&:hover {
color: #fff;
}
}
.alt-help {
color: @term-white;
margin-top: 20px;
display: flex;
flex-direction: row;
align-items: center;
font-size: 12px;
margin-bottom: 10px;
.help-entry {
margin-left: 20px;
}
}
}
@import "../../index.less";
.history-view {
color: #ccc;
@ -418,140 +365,3 @@
}
}
}
.webshare-view {
.webshare-item {
padding: 4px 5px 8px 15px;
margin-bottom: 4px;
border-top: 1px solid white;
display: flex;
flex-direction: row;
position: relative;
min-height: 55px;
align-items: center;
&:first-child {
border-top: 0;
}
.webshare-vic {
width: 200px;
.webshare-vic-link {
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
.actions {
display: flex;
flex-direction: row;
gap: 10px;
}
&:hover {
background-color: #222;
}
}
}
.bookmarks-view {
.bookmarks-list {
color: white;
margin: 4px 10px 5px 5px;
.no-bookmarks {
color: @term-white;
padding: 30px 10px 35px 10px;
border-bottom: 1px solid white;
}
}
.bookmark {
border-bottom: 1px solid #777;
padding: 4px 5px 8px 15px;
margin-bottom: 4px;
position: relative;
display: flex;
flex-direction: row;
max-width: 100%;
.focus-indicator {
top: 0;
height: calc(100% - 4px);
}
&.is-editing {
padding-top: 8px;
padding-bottom: 12px;
}
&.pending-delete {
background-color: #522;
}
.bookmark-content,
.bookmark-edit {
flex-grow: 1;
max-width: calc(100% - 50px);
}
label {
color: white;
margin-bottom: 4px;
}
textarea {
width: 80%;
min-width: 50%;
color: white;
background-color: black;
&.mono-font {
.mono-font(12px);
}
}
.bookmark-id-div {
display: none;
position: absolute;
color: #666;
right: 5px;
bottom: 2px;
.mono-font(8px);
}
&:hover .bookmark-id-div {
display: block;
}
.bookmark-controls {
display: flex;
flex-direction: row;
font-size: 16px;
visibility: hidden;
color: @term-white;
.bookmark-control:first-child {
margin-left: 0;
}
.bookmark-control {
margin-left: 10px;
cursor: pointer;
padding: 2px;
&:hover {
color: @term-bright-white;
}
}
}
&:hover .bookmark-controls {
visibility: visible;
}
}
}

View File

@ -5,13 +5,15 @@ import { If, For, When, Otherwise, Choose } from "tsx-control-statements/compone
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 { Line } from "../line/linecomps";
import { CmdStrCode } from "../../common/common";
import "./history.less";
dayjs.extend(customParseFormat);
dayjs.extend(localizedFormat);

View File

@ -5,9 +5,9 @@ import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { If, For, When, Otherwise, Choose } from "tsx-control-statements/components";
import { GlobalModel, GlobalCommandRunner, Session, Cmd, ScreenLines, Screen, getTermPtyData } from "./model";
import { windowWidthToCols, windowHeightToRows, termHeightFromRows, termWidthFromCols } from "./textmeasure";
import { If } from "tsx-control-statements/components";
import { GlobalModel, GlobalCommandRunner, Cmd, getTermPtyData } from "../../model";
import { termHeightFromRows } from "../../textmeasure";
import type {
LineType,
CmdDataType,
@ -21,18 +21,18 @@ import type {
LineHeightChangeCallbackType,
RendererModelInitializeParams,
RendererModel,
} from "./types";
} from "../../types";
import cn from "classnames";
import { TermWrap } from "./term";
import type { LineContainerModel } from "./model";
import { renderCmdText } from "./elements";
import { SimpleBlobRendererModel, SimpleBlobRenderer } from "./simplerenderer";
import { FullRenderer } from "./fullrenderer";
import { isBlank } from "./util";
import { PluginModel } from "./plugins";
import { PtyDataBuffer } from "./ptydata";
import type { LineContainerModel } from "../../model";
import { renderCmdText } from "../../common/common";
import { SimpleBlobRenderer } from "./renderer/simplerenderer";
import { FullRenderer } from "./renderer/fullrenderer";
import { isBlank } from "../../util";
import { PluginModel } from "../../plugins/plugins";
import * as lineutil from "./lineutil";
import "./lines.less";
dayjs.extend(localizedFormat);
type OV<V> = mobx.IObservableValue<V>;

View File

@ -1,3 +1,5 @@
@import "../../index.less";
.line.line-text {
flex-direction: row;
padding-top: 5px;
@ -465,10 +467,6 @@
border-radius: 3px;
}
}
&:hover .cmd-hints {
display: flex;
}
}
.line.line-invalid {

View File

@ -8,10 +8,12 @@ import cn from "classnames";
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { debounce, throttle } from "throttle-debounce";
import * as T from "./types";
import * as util from "./util";
import * as T from "../../types";
import * as util from "../../util";
import * as lineutil from "./lineutil";
import "./lines.less";
dayjs.extend(localizedFormat);
type OV<V> = mobx.IObservableValue<V>;
type OArr<V> = mobx.IObservableArray<V>;

View File

@ -1,7 +1,7 @@
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { isBlank, getDateStr } from "./util";
import { LineType, WebLine, RendererContext } from "./types";
import { isBlank, getDateStr } from "../../util";
import { LineType, WebLine, RendererContext } from "../../types";
dayjs.extend(localizedFormat);

View File

@ -1,25 +1,12 @@
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";
} from "../../../types";
import { debounce, throttle } from "throttle-debounce";
type OV<V> = mobx.IObservableValue<V>;

View File

@ -19,12 +19,12 @@ import type {
LineType,
TermContextUnion,
RendererContainerType,
} from "./types";
import * as T from "./types";
import { PacketDataBuffer } from "./ptydata";
} from "../../../types";
import * as T from "../../../types";
import { PacketDataBuffer } from "../../../ptydata";
import { debounce, throttle } from "throttle-debounce";
import * as util from "./util";
import { GlobalModel } from "./model";
import * as util from "../../../util";
import { GlobalModel } from "../../../model";
type OV<V> = mobx.IObservableValue<V>;
type CV<V> = mobx.IComputedValue<V>;

357
src/main/modals/Modals.tsx Normal file
View File

@ -0,0 +1,357 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { If, For } from "tsx-control-statements/components";
import cn from "classnames";
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel } from "../../model";
import { Markdown } from "../../common/common";
dayjs.extend(localizedFormat);
type OV<V> = mobx.IObservableValue<V>;
@mobxReact.observer
class DisconnectedModal extends React.Component<{}, {}> {
logRef: any = React.createRef();
showLog: mobx.IObservableValue<boolean> = mobx.observable.box(false);
@boundMethod
restartServer() {
GlobalModel.restartLocalServer();
}
@boundMethod
tryReconnect() {
GlobalModel.ws.connectNow("manual");
}
componentDidMount() {
if (this.logRef.current != null) {
this.logRef.current.scrollTop = this.logRef.current.scrollHeight;
}
}
componentDidUpdate() {
if (this.logRef.current != null) {
this.logRef.current.scrollTop = this.logRef.current.scrollHeight;
}
}
@boundMethod
handleShowLog(): void {
mobx.action(() => {
this.showLog.set(!this.showLog.get());
})();
}
render() {
let model = GlobalModel;
let logLine: string = null;
let idx: number = 0;
return (
<div className="prompt-modal disconnected-modal modal is-active">
<div className="modal-background"></div>
<div className="modal-content">
<div className="message-header">
<div className="modal-title">Prompt Client Disconnected</div>
</div>
<If condition={this.showLog.get()}>
<div className="inner-content">
<div className="ws-log" ref={this.logRef}>
<For each="logLine" index="idx" of={GlobalModel.ws.wsLog}>
<div key={idx} className="ws-logline">
{logLine}
</div>
</For>
</div>
</div>
</If>
<footer>
<div className="footer-text-link" style={{ marginLeft: 10 }} onClick={this.handleShowLog}>
<If condition={!this.showLog.get()}>
<i className="fa-sharp fa-solid fa-plus" /> Show Log
</If>
<If condition={this.showLog.get()}>
<i className="fa-sharp fa-solid fa-minus" /> Hide Log
</If>
</div>
<div className="flex-spacer" />
<button onClick={this.tryReconnect} className="button">
<span className="icon">
<i className="fa-sharp fa-solid fa-rotate" />
</span>
<span>Try Reconnect</span>
</button>
<button onClick={this.restartServer} className="button is-danger" style={{ marginLeft: 10 }}>
<span className="icon">
<i className="fa-sharp fa-solid fa-triangle-exclamation" />
</span>
<span>Restart Server</span>
</button>
</footer>
</div>
</div>
);
}
}
@mobxReact.observer
class ClientStopModal extends React.Component<{}, {}> {
@boundMethod
refreshClient() {
GlobalModel.refreshClient();
}
render() {
let model = GlobalModel;
let cdata = model.clientData.get();
let title = "Client Not Ready";
return (
<div className="prompt-modal client-stop-modal modal is-active">
<div className="modal-background"></div>
<div className="modal-content">
<div className="message-header">
<div className="modal-title">[prompt] {title}</div>
</div>
<div className="inner-content">
<If condition={cdata == null}>
<div>Cannot get client data.</div>
</If>
</div>
<footer>
<button onClick={this.refreshClient} className="button">
<span className="icon">
<i className="fa-sharp fa-solid fa-rotate" />
</span>
<span>Hard Refresh Client</span>
</button>
</footer>
</div>
</div>
);
}
}
@mobxReact.observer
class LoadingSpinner extends React.Component<{}, {}> {
render() {
return (
<div className="loading-spinner">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
);
}
}
@mobxReact.observer
class AlertModal extends React.Component<{}, {}> {
@boundMethod
closeModal(): void {
GlobalModel.cancelAlert();
}
@boundMethod
handleOK(): void {
GlobalModel.confirmAlert();
}
render() {
let message = GlobalModel.alertMessage.get();
if (message == null) {
return null;
}
let title = message.title ?? (message.confirm ? "Confirm" : "Alert");
let isConfirm = message.confirm;
return (
<div className="modal prompt-modal is-active alert-modal">
<div className="modal-background" />
<div className="modal-content">
<header>
<p className="modal-title">
<i className="fa-sharp fa-solid fa-triangle-exclamation" /> {title}
</p>
<div className="close-icon">
<i onClick={this.closeModal} className="fa-sharp fa-solid fa-times" />
</div>
</header>
<If condition={message.markdown}>
<Markdown text={message.message} extraClassName="inner-content" />
</If>
<If condition={!message.markdown}>
<div className="inner-content content">
<p>{message.message}</p>
</div>
</If>
<footer>
<If condition={isConfirm}>
<div onClick={this.closeModal} className="button is-prompt-cancel is-outlined is-small">
Cancel
</div>
<div onClick={this.handleOK} className="button is-prompt-green is-outlined is-small">
OK
</div>
</If>
<If condition={!isConfirm}>
<div onClick={this.handleOK} className="button is-prompt-green is-small">
OK
</div>
</If>
</footer>
</div>
</div>
);
}
}
@mobxReact.observer
class WelcomeModal extends React.Component<{}, {}> {
totalPages: number = 3;
pageNum: OV<number> = mobx.observable.box(1, { name: "welcome-pagenum" });
@boundMethod
closeModal(): void {
mobx.action(() => {
GlobalModel.welcomeModalOpen.set(false);
})();
}
@boundMethod
goNext(): void {
mobx.action(() => {
this.pageNum.set(this.pageNum.get() + 1);
})();
}
@boundMethod
goPrev(): void {
mobx.action(() => {
this.pageNum.set(this.pageNum.get() - 1);
})();
}
renderDot(num: number): any {
if (num == this.pageNum.get()) {
return <i key={String(num)} className="fa-sharp fa-solid fa-circle" />;
}
return <i key={String(num)} className="fa-sharp fa-regular fa-circle" />;
}
renderDots(): any {
let elems: any = [];
for (let i = 1; i <= this.totalPages; i++) {
let elem = this.renderDot(i);
elems.push(elem);
}
return elems;
}
render() {
let pageNum = this.pageNum.get();
return (
<div className={cn("modal welcome-modal prompt-modal is-active")}>
<div className="modal-background" />
<div className="modal-content">
<header>
<div className="modal-title">welcome to [prompt]</div>
<div className="close-icon">
<i onClick={this.closeModal} className="fa-sharp fa-solid fa-times" />
</div>
</header>
<div className={cn("inner-content content", { "is-hidden": pageNum != 1 })}>
<p>
Prompt is a new terminal to help save you time and keep your command-line life organized.
Here's a couple quick tips to get your started!
</p>
</div>
<footer>
<If condition={pageNum > 1}>
<button className={cn("button is-dark prev-button is-small")} onClick={this.goPrev}>
<span className="icon is-small">
<i className="fa-sharp fa-regular fa-angle-left" />
</span>
<span>Prev</span>
</button>
</If>
<If condition={pageNum == 1}>
<div className="prev-spacer" />
</If>
<div className="flex-spacer" />
<div className="dots">{this.renderDots()}</div>
<div className="flex-spacer" />
<If condition={pageNum < this.totalPages}>
<button className="button is-dark next-button is-small" onClick={this.goNext}>
<span>Next</span>
<span className="icon is-small">
<i className="fa-sharp fa-regular fa-angle-right" />
</span>
</button>
</If>
<If condition={pageNum == this.totalPages}>
<button className="button is-dark next-button is-small" onClick={this.closeModal}>
<span>Done</span>
</button>
</If>
</footer>
</div>
</div>
);
}
}
@mobxReact.observer
class TosModal extends React.Component<{}, {}> {
@boundMethod
acceptTos(): void {
GlobalCommandRunner.clientAcceptTos();
}
render() {
return (
<div className={cn("modal tos-modal prompt-modal is-active")}>
<div className="modal-background" />
<div className="modal-content">
<header>
<div className="modal-title">Welcome to [prompt]</div>
</header>
<div className="inner-content">
<div className="content">
<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>
.
</p>
<p>
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.commandline.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>
</footer>
</div>
</div>
);
}
}
export { WelcomeModal, LoadingSpinner, ClientStopModal, AlertModal, DisconnectedModal, TosModal };

View File

@ -1,3 +1,5 @@
@import "../../index.less";
// modal css (also includes settings-field)
.modal {
@ -167,253 +169,6 @@
}
}
.modal.prompt-modal.remotes-modal {
.modal-content {
min-width: 850px;
}
.inner-content {
display: flex;
flex-direction: row;
align-items: stretch;
padding: 0;
max-height: 80vh;
.remotes-menu {
flex: 0 0 200px;
min-height: 450px;
border-right: 1px solid #666;
overflow-y: auto;
height: 100px;
.remote-menu-item {
border-top: 1px solid #666;
padding: 5px;
display: flex;
flex-direction: row;
cursor: pointer;
&.add-remote {
font-size: 13px;
padding: 10px 5px 10px 5px;
}
&:hover {
background-color: #333;
}
&.is-selected {
background-color: @active-menu-color;
.remote-name .remote-name-secondary {
color: white;
}
}
&:first-child {
border-top: 0;
}
.remote-status-light {
width: 15px;
margin-top: -2px;
}
.remote-name {
flex-grow: 1;
.remote-name-primary {
font-size: 12px;
font-weight: bold;
}
.remote-name-secondary {
font-size: 11px;
color: #777;
}
}
}
}
.remote-detail {
padding: 10px;
flex-grow: 1;
font-size: 12px;
display: flex;
flex-direction: column;
.settings-field {
margin-top: 5px;
}
* {
flex-shrink: 0;
}
.detail-subtitle {
font-size: 18px;
margin-bottom: 10px;
margin-top: 10px;
}
.title {
color: white;
padding-bottom: 8px;
margin-bottom: 0;
border-bottom: 1px solid #777;
}
.terminal-wrapper {
margin-left: 0;
margin-bottom: 0;
&.has-message {
margin-top: 0;
}
box-shadow: none;
border: 1px solid #777;
}
.action-buttons {
display: flex;
flex-direction: row;
gap: 10px;
margin-top: 2px;
}
.remote-message {
margin-top: 5px;
padding: 8px;
border-radius: 5px 5px 0 0;
background-color: #333;
i.fa-check {
color: @term-green;
}
&.is-ok {
}
.message-row {
display: flex;
flex-direction: row;
align-items: center;
}
.remote-status {
position: relative;
top: -1px;
}
.button {
height: 22px;
}
}
.settings-field {
.update-auth-button {
visibility: hidden;
}
&:hover {
.update-auth-button {
visibility: visible;
}
.hide-hover {
display: none;
}
}
}
&.auth-editing,
&.create-remote {
.settings-field.align-top {
align-items: flex-start;
.settings-label {
margin-top: 8px;
}
.settings-input {
align-items: flex-start;
}
}
.settings-label {
display: flex;
flex-direction: row;
align-items: center;
width: 135px;
}
.settings-field .settings-input .undo-icon {
cursor: pointer;
font-size: 18px;
margin-left: 5px;
}
.editremote-dropdown .dropdown-trigger button {
width: 120px;
justify-content: flex-start;
}
.settings-field .raw-input {
width: 120px;
}
.settings-input input {
width: 250px;
}
.dropdown .dropdown-trigger button {
font-size: 12px;
}
.dropdown .dropdown-item {
font-size: 12px;
padding: 5px 5px 5px 12px;
}
.settings-input {
.info-message {
margin-left: 22px;
}
}
.settings-label {
.info-message {
margin-right: 15px;
}
}
}
}
}
.terminal-wrapper {
position: relative;
background-color: #000;
padding: 2px 10px 5px 4px;
margin: 5px 5px 10px 5px;
box-shadow: 0 0 1px 1px rgba(255, 255, 255, 0.3);
&.focus {
box-shadow: 0 0 3px 3px rgba(255, 255, 255, 0.3);
}
.term-tag {
position: absolute;
top: 0;
right: 0;
background-color: @term-red;
color: white;
z-index: 110;
padding: 4px;
.mono-font(10px);
}
}
}
.modal.welcome-modal {
footer {
.prev-button {

View File

@ -1,24 +1,18 @@
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 { If, For } 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 * as util from "./util";
import { GlobalModel, GlobalCommandRunner, TabColors } from "../../model";
import { Toggle, InlineSettingsTextEdit, SettingsError, InfoMessage } from "../../common/common";
import { LineType, RendererPluginType, ClientDataType, CommandRtnType } from "../../types";
import { PluginModel } from "../../plugins/plugins";
import * as util from "../../util";
import "./modals.less";
type OV<V> = mobx.IObservableValue<V>;
type OArr<V> = mobx.IObservableArray<V>;
type OMap<K, V> = mobx.ObservableMap<K, V>;
type CV<V> = mobx.IComputedValue<V>;
const RemotePtyRows = 8;
const RemotePtyCols = 80;
const APITokenSentinel = "--apitoken--";
// @ts-ignore
const VERSION = __PROMPT_VERSION__;

View File

@ -0,0 +1,192 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { If } from "tsx-control-statements/components";
import cn from "classnames";
import dayjs from "dayjs";
import type { RemoteType, RemoteInstanceType, RemotePtrType } from "../../types";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel, GlobalCommandRunner } from "../../model";
import { Prompt } from "../line/linecomps";
import { renderCmdText } from "../../common/common";
import { TextAreaInput } from "./TextareaInput";
import { InfoMsg } from "./InfoMsg";
import { HistoryInfo } from "./HistoryInfo";
import "./sessionview.less";
dayjs.extend(localizedFormat);
const TDots = "⋮";
type OV<V> = mobx.IObservableValue<V>;
@mobxReact.observer
class CmdInput extends React.Component<{}, {}> {
cmdInputRef: React.RefObject<any> = React.createRef();
@boundMethod
onInfoToggle(): void {
GlobalModel.inputModel.toggleInfoMsg();
return;
}
componentDidMount() {
this.updateCmdInputHeight();
}
updateCmdInputHeight() {
let elem = this.cmdInputRef.current;
if (elem == null) {
return;
}
let height = elem.offsetHeight;
if (height == GlobalModel.inputModel.cmdInputHeight) {
return;
}
mobx.action(() => {
GlobalModel.inputModel.cmdInputHeight.set(height);
})();
}
componentDidUpdate(prevProps, prevState, snapshot: {}): void {
this.updateCmdInputHeight();
}
@boundMethod
handleInnerHeightUpdate(): void {
this.updateCmdInputHeight();
}
@boundMethod
clickFocusInputHint(): void {
GlobalModel.inputModel.giveFocus();
}
@boundMethod
clickHistoryHint(e: any): void {
e.preventDefault();
e.stopPropagation();
let inputModel = GlobalModel.inputModel;
if (inputModel.historyShow.get()) {
inputModel.resetHistory();
} else {
inputModel.openHistory();
}
}
@boundMethod
clickConnectRemote(remoteId: string): void {
GlobalCommandRunner.connectRemote(remoteId);
}
render() {
let model = GlobalModel;
let inputModel = model.inputModel;
let screen = GlobalModel.getActiveScreen();
let ri: RemoteInstanceType = null;
let rptr: RemotePtrType = null;
if (screen != null) {
ri = screen.getCurRemoteInstance();
rptr = screen.curRemote.get();
}
let remote: RemoteType = null;
let feState: Record<string, string> = null;
if (ri != null) {
remote = GlobalModel.getRemote(ri.remoteid);
feState = ri.festate;
}
let infoShow = inputModel.infoShow.get();
let historyShow = !infoShow && inputModel.historyShow.get();
let infoMsg = inputModel.infoMsg.get();
let hasInfo = infoMsg != null;
let focusVal = inputModel.physicalInputFocused.get();
let inputMode: string = inputModel.inputMode.get();
let textAreaInputKey = screen == null ? "null" : screen.screenId;
return (
<div
ref={this.cmdInputRef}
className={cn(
"cmd-input has-background-black",
{ "has-info": infoShow },
{ "has-history": historyShow }
)}
>
<div key="focus" className={cn("focus-indicator", { active: focusVal })} />
<div key="minmax" onClick={this.onInfoToggle} className="input-minmax-control">
<If condition={infoShow || historyShow}>
<i className="fa-sharp fa-solid fa-chevron-down" />
</If>
<If condition={!(infoShow || historyShow) && hasInfo}>
<i className="fa-sharp fa-solid fa-chevron-up" />
</If>
</div>
<If condition={historyShow}>
<div className="cmd-input-grow-spacer"></div>
<HistoryInfo />
</If>
<InfoMsg key="infomsg" />
<If condition={remote && remote.status != "connected"}>
<div className="remote-status-warning">
WARNING:&nbsp;
<span className="remote-name">[{GlobalModel.resolveRemoteIdToFullRef(remote.remoteid)}]</span>
&nbsp;is {remote.status}
<If condition={remote.status != "connecting"}>
<div
className="button is-prompt-green is-outlined is-small"
onClick={() => this.clickConnectRemote(remote.remoteid)}
>
connect now
</div>
</If>
</div>
</If>
<div key="prompt" className="cmd-input-context">
<div className="has-text-white">
<Prompt rptr={rptr} festate={feState} />
</div>
</div>
<div
key="input"
className={cn(
"cmd-input-field field has-addons",
inputMode != null ? "inputmode-" + inputMode : null
)}
>
<If condition={inputMode != null}>
<div className="control cmd-quick-context">
<div className="button is-static">{inputMode}</div>
</div>
</If>
<TextAreaInput key={textAreaInputKey} onHeightChange={this.handleInnerHeightUpdate} />
<div className="control cmd-exec">
<div onClick={inputModel.uiSubmitCommand} className="button" title="Run Command">
<span className="icon">
<i className="fa-sharp fa-solid fa-rocket" />
</span>
</div>
</div>
<div className="cmd-hints">
<div onClick={inputModel.toggleExpandInput} className="hint-item color-white">
{inputModel.inputExpanded.get() ? "shrink" : "expand"} input ({renderCmdText("E")})
</div>
<If condition={!focusVal}>
<div onClick={this.clickFocusInputHint} className="hint-item color-white">
focus input ({renderCmdText("I")})
</div>
</If>
<If condition={focusVal}>
<div onMouseDown={this.clickHistoryHint} className="hint-item color-green">
<i className={cn("fa-sharp fa-solid", historyShow ? "fa-angle-down" : "fa-angle-up")} />{" "}
{historyShow ? "close history (esc)" : "show history (ctrl-r)"}
</div>
</If>
</div>
</div>
</div>
);
}
}
export { CmdInput };

View File

@ -0,0 +1,234 @@
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 } from "tsx-control-statements/components";
import cn from "classnames";
import dayjs from "dayjs";
import type { HistoryItem, HistoryQueryOpts } from "../../types";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel } from "../../model";
import { isBlank } from "../../util";
import "./sessionview.less";
dayjs.extend(localizedFormat);
const TDots = "⋮";
function truncateWithTDots(str: string, maxLen: number): string {
if (str == null) {
return null;
}
if (str.length <= maxLen) {
return str;
}
return str.slice(0, maxLen - 1) + TDots;
}
@mobxReact.observer
class HistoryInfo extends React.Component<{}, {}> {
lastClickHNum: string = null;
lastClickTs: number = 0;
containingText: mobx.IObservableValue<string> = mobx.observable.box("");
componentDidMount() {
let inputModel = GlobalModel.inputModel;
let hitem = inputModel.getHistorySelectedItem();
if (hitem == null) {
hitem = inputModel.getFirstHistoryItem();
}
if (hitem != null) {
inputModel.scrollHistoryItemIntoView(hitem.historynum);
}
}
@boundMethod
handleItemClick(hitem: HistoryItem) {
let inputModel = GlobalModel.inputModel;
let selItem = inputModel.getHistorySelectedItem();
if (this.lastClickHNum == hitem.historynum && selItem != null && selItem.historynum == hitem.historynum) {
inputModel.grabSelectedHistoryItem();
return;
}
inputModel.giveFocus();
inputModel.setHistorySelectionNum(hitem.historynum);
let now = Date.now();
this.lastClickHNum = hitem.historynum;
this.lastClickTs = now;
setTimeout(() => {
if (this.lastClickTs == now) {
this.lastClickHNum = null;
this.lastClickTs = 0;
}
}, 3000);
}
renderRemote(hitem: HistoryItem): any {
if (hitem.remote == null || isBlank(hitem.remote.remoteid)) {
return sprintf("%-15s ", "");
}
let r = GlobalModel.getRemote(hitem.remote.remoteid);
if (r == null) {
return sprintf("%-15s ", "???");
}
let rname = "";
if (!isBlank(r.remotealias)) {
rname = r.remotealias;
} else {
rname = r.remotecanonicalname;
}
if (!isBlank(hitem.remote.name)) {
rname = rname + ":" + hitem.remote.name;
}
let rtn = sprintf("%-15s ", "[" + truncateWithTDots(rname, 13) + "]");
return rtn;
}
renderHInfoText(
hitem: HistoryItem,
opts: HistoryQueryOpts,
isSelected: boolean,
snames: Record<string, string>,
scrNames: Record<string, string>
): string {
let remoteStr = "";
if (!opts.limitRemote) {
remoteStr = this.renderRemote(hitem);
}
let selectedStr = isSelected ? "*" : " ";
let lineNumStr = hitem.linenum > 0 ? "(" + hitem.linenum + ")" : "";
if (isBlank(opts.queryType) || opts.queryType == "screen") {
return selectedStr + sprintf("%7s", lineNumStr) + " " + remoteStr;
}
if (opts.queryType == "session") {
let screenStr = "";
if (!isBlank(hitem.screenid)) {
let scrName = scrNames[hitem.screenid];
if (scrName != null) {
screenStr = "[" + truncateWithTDots(scrName, 15) + "]";
}
}
return selectedStr + sprintf("%17s", screenStr) + sprintf("%7s", lineNumStr) + " " + remoteStr;
}
if (opts.queryType == "global") {
let sessionStr = "";
if (!isBlank(hitem.sessionid)) {
let sessionName = snames[hitem.sessionid];
if (sessionName != null) {
sessionStr = "#" + truncateWithTDots(sessionName, 15);
}
}
let screenStr = "";
if (!isBlank(hitem.screenid)) {
let scrName = scrNames[hitem.screenid];
if (scrName != null) {
screenStr = "[" + truncateWithTDots(scrName, 13) + "]";
}
}
let ssStr = sessionStr + screenStr;
return (
selectedStr +
sprintf("%15s ", sessionStr) +
" " +
sprintf("%15s", screenStr) +
sprintf("%7s", lineNumStr) +
" " +
remoteStr
);
}
return "-";
}
renderHItem(
hitem: HistoryItem,
opts: HistoryQueryOpts,
isSelected: boolean,
snames: Record<string, string>,
scrNames: Record<string, string>
): any {
let lines = hitem.cmdstr.split("\n");
let line: string = "";
let idx = 0;
let infoText = this.renderHInfoText(hitem, opts, isSelected, snames, scrNames);
let infoTextSpacer = sprintf("%" + infoText.length + "s", "");
return (
<div
key={hitem.historynum}
className={cn(
"history-item",
{ "is-selected": isSelected },
{ "history-haderror": hitem.haderror },
"hnum-" + hitem.historynum
)}
onClick={() => this.handleItemClick(hitem)}
>
<div className="history-line">
{infoText} {lines[0]}
</div>
<For each="line" index="idx" of={lines.slice(1)}>
<div key={idx} className="history-line">
{infoTextSpacer} {line}
</div>
</For>
</div>
);
}
@boundMethod
handleClose() {
GlobalModel.inputModel.toggleInfoMsg();
}
render() {
let inputModel = GlobalModel.inputModel;
let idx: number = 0;
let selItem = inputModel.getHistorySelectedItem();
let hitems = inputModel.getFilteredHistoryItems();
hitems = hitems.slice().reverse();
let hitem: HistoryItem = null;
let opts = inputModel.historyQueryOpts.get();
let snames: Record<string, string> = {};
let scrNames: Record<string, string> = {};
if (opts.queryType == "global") {
scrNames = GlobalModel.getScreenNames();
snames = GlobalModel.getSessionNames();
} else if (opts.queryType == "session") {
scrNames = GlobalModel.getScreenNames();
}
return (
<div className="cmd-history">
<div className="history-title">
<div>history</div>
<div className="spacer"></div>
<div className="history-opt">[for {opts.queryType} &#x2318;S]</div>
<div className="spacer"></div>
<div className="history-opt">[containing '{opts.queryStr}']</div>
<div className="spacer"></div>
<div className="history-opt">[{opts.limitRemote ? "this" : "any"} remote &#x2318;R]</div>
<div className="grow-spacer"></div>
<div className="history-clickable-opt" onClick={this.handleClose}>
(ESC)
</div>
<div className="spacer"></div>
</div>
<div
className={cn(
"history-items",
{ "show-remotes": !opts.limitRemote },
{ "show-sessions": opts.queryType == "global" }
)}
>
<If condition={hitems.length == 0}>[no history]</If>
<If condition={hitems.length > 0}>
<For each="hitem" index="idx" of={hitems}>
{this.renderHItem(hitem, opts, hitem == selItem, snames, scrNames)}
</For>
</If>
</div>
</div>
);
}
}
export { HistoryInfo };

View File

@ -0,0 +1,113 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import { If, For } from "tsx-control-statements/components";
import cn from "classnames";
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel } from "../../model";
import { makeExternLink } from "../../util";
import "./sessionview.less";
dayjs.extend(localizedFormat);
@mobxReact.observer
class InfoMsg extends React.Component<{}, {}> {
getAfterSlash(s: string): string {
if (s.startsWith("^/")) {
return s.substr(1);
}
let slashIdx = s.lastIndexOf("/");
if (slashIdx == s.length - 1) {
slashIdx = s.lastIndexOf("/", slashIdx - 1);
}
if (slashIdx == -1) {
return s;
}
return s.substr(slashIdx + 1);
}
hasSpace(s: string): boolean {
return s.indexOf(" ") != -1;
}
handleCompClick(s: string): void {
// TODO -> complete to this completion
}
render() {
let model = GlobalModel;
let inputModel = model.inputModel;
let infoMsg = inputModel.infoMsg.get();
let infoShow = inputModel.infoShow.get();
let line: string = null;
let istr: string = null;
let idx: number = 0;
let titleStr = null;
let remoteEditKey = "inforemoteedit";
if (infoMsg != null) {
titleStr = infoMsg.infotitle;
}
let activeScreen = model.getActiveScreen();
return (
<div className="cmd-input-info" style={{ display: infoShow ? "block" : "none" }}>
<If condition={infoMsg && infoMsg.infotitle != null}>
<div key="infotitle" className="info-title">
{titleStr}
</div>
</If>
<If condition={infoMsg && infoMsg.infomsg != null}>
<div key="infomsg" className="info-msg">
<If condition={infoMsg.infomsghtml}>
<span dangerouslySetInnerHTML={{ __html: infoMsg.infomsg }} />
</If>
<If condition={!infoMsg.infomsghtml}>{infoMsg.infomsg}</If>
</div>
</If>
<If condition={infoMsg && infoMsg.websharelink && activeScreen != null}>
<div key="infomsg" className="info-msg">
started sharing screen at{" "}
<a target="_blank" href={makeExternLink(activeScreen.getWebShareUrl())}>
[link]
</a>
</div>
</If>
<If condition={infoMsg && infoMsg.infolines != null}>
<div key="infolines" className="info-lines">
<For index="idx" each="line" of={infoMsg.infolines}>
<div key={idx}>{line == "" ? " " : line}</div>
</For>
</div>
</If>
<If condition={infoMsg && infoMsg.infocomps != null && infoMsg.infocomps.length > 0}>
<div key="infocomps" className="info-comps">
<For each="istr" index="idx" of={infoMsg.infocomps}>
<div
onClick={() => this.handleCompClick(istr)}
key={idx}
className={cn(
"info-comp",
{ "has-space": this.hasSpace(istr) },
{ "metacmd-comp": istr.startsWith("^") }
)}
>
{this.getAfterSlash(istr)}
</div>
</For>
<If condition={infoMsg.infocompsmore}>
<div key="more" className="info-comp no-select">
...
</div>
</If>
</div>
</If>
<If condition={infoMsg && infoMsg.infoerror != null}>
<div key="infoerror" className="info-error">
[error] {infoMsg.infoerror}
</div>
</If>
</div>
);
}
}
export { InfoMsg };

View File

@ -0,0 +1,457 @@
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 } from "tsx-control-statements/components";
import cn from "classnames";
import { debounce } from "throttle-debounce";
import dayjs from "dayjs";
import type { LineType, RenderModeType, LineFactoryProps } from "../../types";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel, GlobalCommandRunner, Session, ScreenLines, Screen } from "../../model";
import { Line } from "../line/linecomps";
import { renderCmdText } from "../../common/common";
import { LinesView } from "../line/linesview";
import { CmdInput } from "./CmdInput";
import "./sessionview.less";
import "./tabs.less";
dayjs.extend(localizedFormat);
type OV<V> = mobx.IObservableValue<V>;
@mobxReact.observer
class SessionView extends React.Component<{}, {}> {
render() {
let model = GlobalModel;
let session = model.getActiveSession();
if (session == null) {
return <div className="session-view">(no active session)</div>;
}
let activeScreen = session.getActiveScreen();
let cmdInputHeight = model.inputModel.cmdInputHeight.get();
if (cmdInputHeight == 0) {
cmdInputHeight = 110;
}
let isHidden = GlobalModel.activeMainView.get() != "session";
return (
<div className={cn("session-view", { "is-hidden": isHidden })} data-sessionid={session.sessionId}>
<ScreenView screen={activeScreen} />
<ScreenTabs session={session} />
<div style={{ height: cmdInputHeight }}></div>
<CmdInput />
</div>
);
}
}
@mobxReact.observer
class ScreenView extends React.Component<{ screen: Screen }, {}> {
render() {
let { screen } = this.props;
if (screen == null) {
return <div className="screen-view">(no screen found)</div>;
}
let fontSize = GlobalModel.termFontSize.get();
return (
<div className="screen-view" data-screenid={screen.screenId}>
<ScreenWindowView key={screen.screenId + ":" + fontSize} screen={screen} />
</div>
);
}
}
// screen is not null
@mobxReact.observer
class ScreenWindowView extends React.Component<{ screen: Screen }, {}> {
rszObs: any;
windowViewRef: React.RefObject<any>;
width: mobx.IObservableValue<number> = mobx.observable.box(0, { name: "sw-view-width" });
height: mobx.IObservableValue<number> = mobx.observable.box(0, { name: "sw-view-height" });
setSize_debounced: (width: number, height: number) => void;
renderMode: OV<RenderModeType> = mobx.observable.box("normal", { name: "renderMode" });
shareCopied: OV<boolean> = mobx.observable.box(false, { name: "sw-shareCopied" });
constructor(props: any) {
super(props);
this.setSize_debounced = debounce(1000, this.setSize.bind(this));
this.windowViewRef = React.createRef();
}
setSize(width: number, height: number): void {
let { screen } = this.props;
if (screen == null) {
return;
}
if (width == null || height == null || width == 0 || height == 0) {
return;
}
mobx.action(() => {
this.width.set(width);
this.height.set(height);
screen.screenSizeCallback({ height: height, width: width });
})();
}
componentDidMount() {
let wvElem = this.windowViewRef.current;
if (wvElem != null) {
let width = wvElem.offsetWidth;
let height = wvElem.offsetHeight;
this.setSize(width, height);
this.rszObs = new ResizeObserver(this.handleResize.bind(this));
this.rszObs.observe(wvElem);
}
}
componentWillUnmount() {
if (this.rszObs) {
this.rszObs.disconnect();
}
}
handleResize(entries: any) {
if (entries.length == 0) {
return;
}
let entry = entries[0];
let width = entry.target.offsetWidth;
let height = entry.target.offsetHeight;
mobx.action(() => {
this.setSize_debounced(width, height);
})();
}
getScreenLines(): ScreenLines {
let { screen } = this.props;
let win = GlobalModel.getScreenLinesById(screen.screenId);
if (win == null) {
win = GlobalModel.loadScreenLines(screen.screenId);
}
return win;
}
getWindowViewStyle(): any {
return { position: "absolute", width: "100%", height: "100%", overflowX: "hidden" };
}
@boundMethod
toggleRenderMode() {
let renderMode = this.renderMode.get();
mobx.action(() => {
this.renderMode.set(renderMode == "normal" ? "collapsed" : "normal");
})();
}
renderError(message: string, fade: boolean) {
let { screen } = this.props;
return (
<div
className="window-view"
style={this.getWindowViewStyle()}
ref={this.windowViewRef}
data-screenid={screen.screenId}
>
<div key="lines" className="lines"></div>
<div key="window-empty" className={cn("window-empty", { "should-fade": fade })}>
<div>{message}</div>
</div>
</div>
);
}
@boundMethod
copyShareLink(): void {
let { screen } = this.props;
let shareLink = screen.getWebShareUrl();
if (shareLink == null) {
return;
}
navigator.clipboard.writeText(shareLink);
mobx.action(() => {
this.shareCopied.set(true);
})();
setTimeout(() => {
mobx.action(() => {
this.shareCopied.set(false);
})();
}, 600);
}
@boundMethod
openScreenSettings(): void {
let { screen } = this.props;
mobx.action(() => {
GlobalModel.screenSettingsModal.set({ sessionId: screen.sessionId, screenId: screen.screenId });
})();
}
@boundMethod
buildLineComponent(lineProps: LineFactoryProps): JSX.Element {
let { screen } = this.props;
let { line, ...restProps } = lineProps;
let realLine: LineType = line as LineType;
return <Line key={realLine.lineid} screen={screen} line={realLine} {...restProps} />;
}
render() {
let { screen } = this.props;
let win = this.getScreenLines();
if (win == null || !win.loaded.get()) {
return this.renderError("...", true);
}
if (win.loadError.get() != null) {
return this.renderError(sprintf("(%s)", win.loadError.get()), false);
}
if (this.width.get() == 0) {
return this.renderError("", false);
}
let cdata = GlobalModel.clientData.get();
if (cdata == null) {
return this.renderError("loading client data", true);
}
let idx = 0;
let line: LineType = null;
let session = GlobalModel.getSessionById(screen.sessionId);
let isActive = screen.isActive();
let selectedLine = screen.getSelectedLine();
let lines = win.getNonArchivedLines();
let renderMode = this.renderMode.get();
return (
<div className="window-view" style={this.getWindowViewStyle()} ref={this.windowViewRef}>
<div
key="rendermode-tag"
className={cn("rendermode-tag", { "is-active": isActive })}
style={{ display: "none" }}
>
<div className="render-mode" onClick={this.toggleRenderMode}>
<If condition={renderMode == "normal"}>
<i title="collapse" className="fa-sharp fa-solid fa-arrows-to-line" />
</If>
<If condition={renderMode == "collapsed"}>
<i title="expand" className="fa-sharp fa-solid fa-arrows-from-line" />
</If>
</div>
</div>
<If condition={screen.isWebShared()}>
<div key="share-tag" className="share-tag">
<If condition={this.shareCopied.get()}>
<div className="copied-indicator" />
</If>
<div className="share-tag-title">
<i title="archived" className="fa-sharp fa-solid fa-share-nodes" /> web shared
</div>
<div className="share-tag-link">
<div className="button is-prompt-green is-outlined is-small" onClick={this.copyShareLink}>
<span>copy link</span>
<span className="icon">
<i className="fa-sharp fa-solid fa-copy" />
</span>
</div>
<div
className="button is-prompt-green is-outlined is-small"
onClick={this.openScreenSettings}
>
<span>open settings</span>
<span className="icon">
<i className="fa-sharp fa-solid fa-cog" />
</span>
</div>
</div>
</div>
</If>
<If condition={lines.length > 0}>
<LinesView
screen={screen}
width={this.width.get()}
lines={lines}
renderMode={renderMode}
lineFactory={this.buildLineComponent}
/>
</If>
<If condition={lines.length == 0}>
<div key="window-empty" className="window-empty">
<div>
<code>
[session="{session.name.get()}" screen="{screen.name.get()}"]
</code>
</div>
</div>
</If>
</div>
);
}
}
@mobxReact.observer
class ScreenTabs extends React.Component<{ session: Session }, {}> {
tabsRef: React.RefObject<any> = React.createRef();
lastActiveScreenId: string = null;
scrolling: OV<boolean> = mobx.observable.box(false, { name: "screentabs-scrolling" });
stopScrolling_debounced: () => void;
constructor(props: any) {
super(props);
this.stopScrolling_debounced = debounce(1500, this.stopScrolling.bind(this));
}
@boundMethod
handleNewScreen() {
let { session } = this.props;
GlobalCommandRunner.createNewScreen();
}
@boundMethod
handleSwitchScreen(screenId: string) {
let { session } = this.props;
if (session == null) {
return;
}
if (session.activeScreenId.get() == screenId) {
return;
}
let screen = session.getScreenById(screenId);
if (screen == null) {
return;
}
GlobalCommandRunner.switchScreen(screenId);
}
componentDidMount(): void {
this.componentDidUpdate();
}
componentDidUpdate(): void {
let { session } = this.props;
let activeScreenId = session.activeScreenId.get();
if (activeScreenId != this.lastActiveScreenId && this.tabsRef.current) {
let tabElem = this.tabsRef.current.querySelector(
sprintf('.screen-tab[data-screenid="%s"]', activeScreenId)
);
if (tabElem != null) {
tabElem.scrollIntoView();
}
}
this.lastActiveScreenId = activeScreenId;
}
stopScrolling(): void {
mobx.action(() => {
this.scrolling.set(false);
})();
}
@boundMethod
handleScroll() {
if (!this.scrolling.get()) {
mobx.action(() => {
this.scrolling.set(true);
})();
}
this.stopScrolling_debounced();
}
@boundMethod
openScreenSettings(e: any, screen: Screen): void {
e.preventDefault();
e.stopPropagation();
mobx.action(() => {
GlobalModel.screenSettingsModal.set({ sessionId: screen.sessionId, screenId: screen.screenId });
})();
}
renderTab(screen: Screen, activeScreenId: string, index: number): any {
let tabIndex = null;
if (index + 1 <= 9) {
tabIndex = <div className="tab-index">{renderCmdText(String(index + 1))}</div>;
}
let settings = (
<div onClick={(e) => this.openScreenSettings(e, screen)} title="Settings" className="tab-gear">
<i className="fa-sharp fa-solid fa-gear" />
</div>
);
let archived = screen.archived.get() ? (
<i title="archived" className="fa-sharp fa-solid fa-box-archive" />
) : null;
let webShared = screen.isWebShared() ? (
<i title="shared to web" className="fa-sharp fa-solid fa-share-nodes web-share-icon" />
) : null;
return (
<div
key={screen.screenId}
data-screenid={screen.screenId}
className={cn(
"screen-tab",
{ "is-active": activeScreenId == screen.screenId, "is-archived": screen.archived.get() },
"color-" + screen.getTabColor()
)}
onClick={() => this.handleSwitchScreen(screen.screenId)}
onContextMenu={(event) => this.openScreenSettings(event, screen)}
>
<div className="tab-name">
{archived}
{webShared}
{screen.name.get()}
</div>
{tabIndex}
{settings}
</div>
);
}
render() {
let { session } = this.props;
if (session == null) {
return null;
}
let screen: Screen = null;
let index = 0;
let showingScreens = [];
let activeScreenId = session.activeScreenId.get();
let screens = GlobalModel.getSessionScreens(session.sessionId);
for (let screen of screens) {
if (!screen.archived.get() || activeScreenId == screen.screenId) {
showingScreens.push(screen);
}
}
showingScreens.sort((a, b) => {
let aidx = a.screenIdx.get();
let bidx = b.screenIdx.get();
if (aidx < bidx) {
return -1;
}
if (aidx > bidx) {
return 1;
}
return 0;
});
return (
<div className="screen-tabs-container">
<div
className={cn("screen-tabs", { scrolling: this.scrolling.get() })}
ref={this.tabsRef}
onScroll={this.handleScroll}
>
<For each="screen" index="index" of={showingScreens}>
{this.renderTab(screen, activeScreenId, index)}
</For>
<div key="new-screen" className="screen-tab new-screen" onClick={this.handleNewScreen}>
<i className="fa-sharp fa-solid fa-plus" />
</div>
</div>
<div className="cmd-hints">
<div className="hint-item color-green">move left {renderCmdText("[")}</div>
<div className="hint-item color-green">move right {renderCmdText("]")}</div>
<div className="hint-item color-green">new tab {renderCmdText("T")}</div>
</div>
</div>
);
}
}
export { SessionView };

View File

@ -0,0 +1,573 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { GlobalModel, GlobalCommandRunner } from "../../model";
import { getMonoFontSize } from "../../textmeasure";
import { isModKeyPress, hasNoModifiers } from "../../util";
import "./sessionview.less";
function pageSize(div: any): number {
if (div == null) {
return 300;
}
let size = div.clientHeight;
if (size > 500) {
size = size - 100;
} else if (size > 200) {
size = size - 30;
}
return size;
}
@mobxReact.observer
class TextAreaInput extends React.Component<{ onHeightChange: () => void }, {}> {
lastTab: boolean = false;
lastHistoryUpDown: boolean = false;
lastTabCurLine: mobx.IObservableValue<string> = mobx.observable.box(null);
lastFocusType: string = null;
mainInputRef: React.RefObject<any>;
historyInputRef: React.RefObject<any>;
controlRef: React.RefObject<any>;
lastHeight: number = 0;
constructor(props) {
super(props);
this.mainInputRef = React.createRef();
this.historyInputRef = React.createRef();
this.controlRef = React.createRef();
}
setFocus(): void {
let inputModel = GlobalModel.inputModel;
if (inputModel.historyShow.get()) {
this.historyInputRef.current.focus();
} else {
this.mainInputRef.current.focus();
}
}
getTextAreaMaxCols(): number {
let taElem = this.mainInputRef.current;
if (taElem == null) {
return 0;
}
let cs = window.getComputedStyle(taElem);
let padding = parseFloat(cs.paddingLeft) + parseFloat(cs.paddingRight);
let borders = parseFloat(cs.borderLeft) + parseFloat(cs.borderRight);
let contentWidth = taElem.clientWidth - padding - borders;
let fontSize = getMonoFontSize(parseInt(cs.fontSize));
let maxCols = Math.floor(contentWidth / fontSize.width);
return maxCols;
}
checkHeight(shouldFire: boolean): void {
let elem = this.controlRef.current;
if (elem == null) {
return;
}
let curHeight = elem.offsetHeight;
if (this.lastHeight == curHeight) {
return;
}
this.lastHeight = curHeight;
if (shouldFire && this.props.onHeightChange != null) {
this.props.onHeightChange();
}
}
componentDidMount() {
let activeScreen = GlobalModel.getActiveScreen();
if (activeScreen != null) {
let focusType = activeScreen.focusType.get();
if (focusType == "input") {
this.setFocus();
}
this.lastFocusType = focusType;
}
this.checkHeight(false);
}
componentDidUpdate() {
let activeScreen = GlobalModel.getActiveScreen();
if (activeScreen != null) {
let focusType = activeScreen.focusType.get();
if (this.lastFocusType != focusType && focusType == "input") {
this.setFocus();
}
this.lastFocusType = focusType;
}
let inputModel = GlobalModel.inputModel;
if (inputModel.forceCursorPos.get() != null) {
if (this.mainInputRef.current != null) {
this.mainInputRef.current.selectionStart = inputModel.forceCursorPos.get();
this.mainInputRef.current.selectionEnd = inputModel.forceCursorPos.get();
}
mobx.action(() => inputModel.forceCursorPos.set(null))();
}
if (inputModel.forceInputFocus) {
inputModel.forceInputFocus = false;
this.setFocus();
}
this.checkHeight(true);
}
getLinePos(elem: any): { numLines: number; linePos: number } {
let numLines = elem.value.split("\n").length;
let linePos = elem.value.substr(0, elem.selectionStart).split("\n").length;
return { numLines, linePos };
}
@mobx.action
@boundMethod
onKeyDown(e: any) {
mobx.action(() => {
if (isModKeyPress(e)) {
return;
}
let model = GlobalModel;
let inputModel = model.inputModel;
let win = model.getScreenLinesForActiveScreen();
let ctrlMod = e.getModifierState("Control") || e.getModifierState("Meta") || e.getModifierState("Shift");
let curLine = inputModel.getCurLine();
let lastTab = this.lastTab;
this.lastTab = e.code == "Tab";
let lastHist = this.lastHistoryUpDown;
this.lastHistoryUpDown = false;
if (e.code == "Tab") {
e.preventDefault();
if (lastTab) {
GlobalModel.submitCommand(
"_compgen",
null,
[curLine],
{ comppos: String(curLine.length), compshow: "1", nohist: "1" },
true
);
return;
} else {
GlobalModel.submitCommand(
"_compgen",
null,
[curLine],
{ comppos: String(curLine.length), nohist: "1" },
true
);
return;
}
}
if (e.code == "Enter") {
e.preventDefault();
if (!ctrlMod) {
if (GlobalModel.inputModel.isEmpty()) {
let activeWindow = GlobalModel.getScreenLinesForActiveScreen();
let activeScreen = GlobalModel.getActiveScreen();
if (activeScreen != null && activeWindow != null && activeWindow.lines.length > 0) {
activeScreen.setSelectedLine(0);
GlobalCommandRunner.screenSelectLine("E");
}
return;
} else {
setTimeout(() => GlobalModel.inputModel.uiSubmitCommand(), 0);
return;
}
}
e.target.setRangeText("\n", e.target.selectionStart, e.target.selectionEnd, "end");
GlobalModel.inputModel.setCurLine(e.target.value);
return;
}
if (e.code == "Escape") {
e.preventDefault();
e.stopPropagation();
let inputModel = GlobalModel.inputModel;
inputModel.toggleInfoMsg();
if (inputModel.inputMode.get() != null) {
inputModel.resetInputMode();
}
return;
}
if (e.code == "KeyE" && e.getModifierState("Meta")) {
e.preventDefault();
e.stopPropagation();
let inputModel = GlobalModel.inputModel;
inputModel.toggleExpandInput();
}
if (e.code == "KeyC" && e.getModifierState("Control")) {
e.preventDefault();
inputModel.resetInput();
return;
}
if (e.code == "KeyU" && e.getModifierState("Control")) {
e.preventDefault();
this.controlU();
return;
}
if (e.code == "KeyP" && e.getModifierState("Control")) {
e.preventDefault();
this.controlP();
return;
}
if (e.code == "KeyN" && e.getModifierState("Control")) {
e.preventDefault();
this.controlN();
return;
}
if (e.code == "KeyW" && e.getModifierState("Control")) {
e.preventDefault();
this.controlW();
return;
}
if (e.code == "KeyY" && e.getModifierState("Control")) {
e.preventDefault();
this.controlY();
return;
}
if (e.code == "KeyR" && e.getModifierState("Control")) {
e.preventDefault();
inputModel.openHistory();
return;
}
if ((e.code == "ArrowUp" || e.code == "ArrowDown") && hasNoModifiers(e)) {
if (!inputModel.isHistoryLoaded()) {
if (e.code == "ArrowUp") {
this.lastHistoryUpDown = true;
inputModel.loadHistory(false, 1, "screen");
}
return;
}
// invisible history movement
let linePos = this.getLinePos(e.target);
if (e.code == "ArrowUp") {
if (!lastHist && linePos.linePos > 1) {
// regular arrow
return;
}
e.preventDefault();
inputModel.moveHistorySelection(1);
this.lastHistoryUpDown = true;
return;
}
if (e.code == "ArrowDown") {
if (!lastHist && linePos.linePos < linePos.numLines) {
// regular arrow
return;
}
e.preventDefault();
inputModel.moveHistorySelection(-1);
this.lastHistoryUpDown = true;
return;
}
}
if (e.code == "PageUp" || e.code == "PageDown") {
e.preventDefault();
let infoScroll = inputModel.hasScrollingInfoMsg();
if (infoScroll) {
let div = document.querySelector(".cmd-input-info");
let amt = pageSize(div);
scrollDiv(div, e.code == "PageUp" ? -amt : amt);
}
}
// console.log(e.code, e.keyCode, e.key, event.which, ctrlMod, e);
})();
}
@boundMethod
onChange(e: any) {
mobx.action(() => {
GlobalModel.inputModel.setCurLine(e.target.value);
})();
}
@boundMethod
onHistoryKeyDown(e: any) {
let inputModel = GlobalModel.inputModel;
if (e.code == "Escape") {
e.preventDefault();
inputModel.resetHistory();
return;
}
if (e.code == "Enter") {
e.preventDefault();
inputModel.grabSelectedHistoryItem();
return;
}
if (e.code == "KeyG" && e.getModifierState("Control")) {
e.preventDefault();
inputModel.resetInput();
return;
}
if (e.code == "KeyC" && e.getModifierState("Control")) {
e.preventDefault();
inputModel.resetInput();
return;
}
if (
e.code == "KeyR" &&
(e.getModifierState("Meta") || e.getModifierState("Control")) &&
!e.getModifierState("Shift")
) {
e.preventDefault();
let opts = mobx.toJS(inputModel.historyQueryOpts.get());
if (opts.limitRemote) {
opts.limitRemote = false;
opts.limitRemoteInstance = false;
} else {
opts.limitRemote = true;
opts.limitRemoteInstance = true;
}
inputModel.setHistoryQueryOpts(opts);
return;
}
if (e.code == "KeyS" && (e.getModifierState("Meta") || e.getModifierState("Control"))) {
e.preventDefault();
let opts = mobx.toJS(inputModel.historyQueryOpts.get());
let htype = opts.queryType;
if (htype == "screen") {
htype = "session";
} else if (htype == "session") {
htype = "global";
} else {
htype = "screen";
}
inputModel.setHistoryType(htype);
return;
}
if (e.code == "Tab") {
e.preventDefault();
return;
}
if (e.code == "ArrowUp" || e.code == "ArrowDown") {
e.preventDefault();
inputModel.moveHistorySelection(e.code == "ArrowUp" ? 1 : -1);
return;
}
if (e.code == "PageUp" || e.code == "PageDown") {
e.preventDefault();
inputModel.moveHistorySelection(e.code == "PageUp" ? 10 : -10);
return;
}
if (e.code == "KeyP" && e.getModifierState("Control")) {
e.preventDefault();
inputModel.moveHistorySelection(1);
return;
}
if (e.code == "KeyN" && e.getModifierState("Control")) {
e.preventDefault();
inputModel.moveHistorySelection(-1);
return;
}
}
@boundMethod
controlU() {
if (this.mainInputRef.current == null) {
return;
}
let selStart = this.mainInputRef.current.selectionStart;
let value = this.mainInputRef.current.value;
if (selStart > value.length) {
return;
}
let cutValue = value.substr(0, selStart);
let restValue = value.substr(selStart);
let cmdLineUpdate = { cmdline: restValue, cursorpos: 0 };
console.log("ss", selStart, value, "[" + cutValue + "]", "[" + restValue + "]");
navigator.clipboard.writeText(cutValue);
GlobalModel.inputModel.updateCmdLine(cmdLineUpdate);
}
@boundMethod
controlP() {
let inputModel = GlobalModel.inputModel;
if (!inputModel.isHistoryLoaded()) {
this.lastHistoryUpDown = true;
inputModel.loadHistory(false, 1, "screen");
return;
}
inputModel.moveHistorySelection(1);
this.lastHistoryUpDown = true;
}
@boundMethod
controlN() {
let inputModel = GlobalModel.inputModel;
inputModel.moveHistorySelection(-1);
this.lastHistoryUpDown = true;
}
@boundMethod
controlW() {
if (this.mainInputRef.current == null) {
return;
}
let selStart = this.mainInputRef.current.selectionStart;
let value = this.mainInputRef.current.value;
if (selStart > value.length) {
return;
}
let cutSpot = selStart - 1;
let initial = true;
for (; cutSpot >= 0; cutSpot--) {
let ch = value[cutSpot];
console.log(cutSpot, "[" + ch + "]");
if (ch == " " && initial) {
continue;
}
initial = false;
if (ch == " ") {
cutSpot++;
break;
}
}
let cutValue = value.slice(cutSpot, selStart);
let prevValue = value.slice(0, cutSpot);
let restValue = value.slice(selStart);
let cmdLineUpdate = { cmdline: prevValue + restValue, cursorpos: prevValue.length };
console.log(
"ss",
selStart,
value,
"prev[" + prevValue + "]",
"cut[" + cutValue + "]",
"rest[" + restValue + "]"
);
console.log(" ", cmdLineUpdate);
navigator.clipboard.writeText(cutValue);
GlobalModel.inputModel.updateCmdLine(cmdLineUpdate);
}
@boundMethod
controlY() {
if (this.mainInputRef.current == null) {
return;
}
let pastePromise = navigator.clipboard.readText();
pastePromise.then((clipText) => {
clipText = clipText ?? "";
let selStart = this.mainInputRef.current.selectionStart;
let selEnd = this.mainInputRef.current.selectionEnd;
let value = this.mainInputRef.current.value;
if (selStart > value.length || selEnd > value.length) {
return;
}
let newValue = value.substr(0, selStart) + clipText + value.substr(selEnd);
let cmdLineUpdate = { cmdline: newValue, cursorpos: selStart + clipText.length };
GlobalModel.inputModel.updateCmdLine(cmdLineUpdate);
});
}
@boundMethod
handleHistoryInput(e: any) {
let inputModel = GlobalModel.inputModel;
mobx.action(() => {
let opts = mobx.toJS(inputModel.historyQueryOpts.get());
opts.queryStr = e.target.value;
inputModel.setHistoryQueryOpts(opts);
})();
}
@boundMethod
handleMainFocus(e: any) {
let inputModel = GlobalModel.inputModel;
if (inputModel.historyShow.get()) {
e.preventDefault();
if (this.historyInputRef.current != null) {
this.historyInputRef.current.focus();
}
return;
}
inputModel.setPhysicalInputFocused(true);
}
@boundMethod
handleMainBlur(e: any) {
if (document.activeElement == this.mainInputRef.current) {
return;
}
GlobalModel.inputModel.setPhysicalInputFocused(false);
}
@boundMethod
handleHistoryFocus(e: any) {
let inputModel = GlobalModel.inputModel;
if (!inputModel.historyShow.get()) {
e.preventDefault();
if (this.mainInputRef.current != null) {
this.mainInputRef.current.focus();
}
return;
}
inputModel.setPhysicalInputFocused(true);
}
@boundMethod
handleHistoryBlur(e: any) {
if (document.activeElement == this.historyInputRef.current) {
return;
}
GlobalModel.inputModel.setPhysicalInputFocused(false);
}
render() {
let model = GlobalModel;
let inputModel = model.inputModel;
let curLine = inputModel.getCurLine();
let fcp = inputModel.forceCursorPos.get(); // for reaction
let displayLines = 1;
let numLines = curLine.split("\n").length;
let maxCols = this.getTextAreaMaxCols();
let longLine = false;
if (maxCols != 0 && curLine.length >= maxCols - 4) {
longLine = true;
}
if (numLines > 1 || longLine || inputModel.inputExpanded.get()) {
displayLines = 5;
}
let disabled = inputModel.historyShow.get();
if (disabled) {
displayLines = 1;
}
let activeScreen = GlobalModel.getActiveScreen();
if (activeScreen != null) {
activeScreen.focusType.get(); // for reaction
}
let computedHeight = displayLines * 24 + 14 + 2; // 24 = height of line, 14 = padding, 2 = border
return (
<div className="control cmd-input-control is-expanded" ref={this.controlRef}>
<textarea
key="main"
ref={this.mainInputRef}
spellCheck="false"
autoComplete="off"
autoCorrect="off"
id="main-cmd-input"
onFocus={this.handleMainFocus}
onBlur={this.handleMainBlur}
style={{ height: computedHeight, minHeight: computedHeight }}
value={curLine}
onKeyDown={this.onKeyDown}
onChange={this.onChange}
className={cn("textarea", { "display-disabled": disabled })}
></textarea>
<input
key="history"
ref={this.historyInputRef}
spellCheck="false"
autoComplete="off"
autoCorrect="off"
className="history-input"
type="text"
onFocus={this.handleHistoryFocus}
onKeyDown={this.onHistoryKeyDown}
onChange={this.handleHistoryInput}
value={inputModel.historyQueryOpts.get().queryStr}
/>
</div>
);
}
}
export { TextAreaInput };

View File

@ -1,3 +1,5 @@
@import "../../index.less";
.cmd-input-info,
.cmd-history {
&::-webkit-scrollbar {
@ -11,45 +13,6 @@
}
}
.info-message {
position: relative;
font-weight: normal;
font-size: 12px;
color: @term-white;
.message-content {
position: absolute;
display: none;
flex-direction: row;
align-items: flex-start;
top: -6px;
left: -6px;
padding: 5px;
border: 1px solid #777;
background-color: #444;
border-radius: 5px;
z-index: 5;
overflow: hidden;
.info-icon {
margin-right: 5px;
width: 12px;
flex-shrink: 0;
}
.info-children {
flex: 1 0 0;
overflow: hidden;
}
}
&:hover {
.message-content {
display: flex;
}
}
}
.cmd-input {
border-radius: 0;
border-top: 4px solid #ccc;

View File

@ -1,3 +1,5 @@
@import "../../index.less";
#main .screen-tabs .screen-tab {
&.color-green {
color: @tab-white-text;

View File

@ -0,0 +1,346 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { If, For } from "tsx-control-statements/components";
import cn from "classnames";
import dayjs from "dayjs";
import type { RemoteType } from "../../types";
import type * as T from "../../types";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel, GlobalCommandRunner, Session } from "../../model";
import { sortAndFilterRemotes, isBlank } from "../../util";
import { RemoteStatusLight } from "../../common/common";
import "./sidebar.less";
dayjs.extend(localizedFormat);
type OV<V> = mobx.IObservableValue<V>;
@mobxReact.observer
class MainSideBar extends React.Component<{}, {}> {
collapsed: mobx.IObservableValue<boolean> = mobx.observable.box(false);
@boundMethod
toggleCollapsed() {
mobx.action(() => {
this.collapsed.set(!this.collapsed.get());
})();
}
handleSessionClick(sessionId: string) {
GlobalCommandRunner.switchSession(sessionId);
}
handleNewSession() {
GlobalCommandRunner.createNewSession();
}
handleNewSharedSession() {
GlobalCommandRunner.openSharedSession();
}
clickLinks() {
mobx.action(() => {
GlobalModel.showLinks.set(!GlobalModel.showLinks.get());
})();
}
remoteDisplayName(remote: RemoteType): any {
if (!isBlank(remote.remotealias)) {
return (
<>
<span>{remote.remotealias}</span>
<span className="small-text"> {remote.remotecanonicalname}</span>
</>
);
}
return <span>{remote.remotecanonicalname}</span>;
}
clickRemote(remote: RemoteType) {
GlobalCommandRunner.showRemote(remote.remoteid);
}
@boundMethod
handleAddRemote(): void {
GlobalCommandRunner.openCreateRemote();
}
@boundMethod
handleHistoryClick(): void {
if (GlobalModel.activeMainView.get() == "history") {
mobx.action(() => {
GlobalModel.activeMainView.set("session");
})();
return;
}
GlobalModel.historyViewModel.reSearch();
}
@boundMethod
handlePlaybookClick(): void {
console.log("playbook click");
return;
}
@boundMethod
handleBookmarksClick(): void {
if (GlobalModel.activeMainView.get() == "bookmarks") {
GlobalModel.showSessionView();
return;
}
GlobalCommandRunner.bookmarksView();
}
@boundMethod
handleWebSharingClick(): void {
if (GlobalModel.activeMainView.get() == "webshare") {
GlobalModel.showSessionView();
return;
}
GlobalModel.showWebShareView();
}
@boundMethod
handleWelcomeClick(): void {
mobx.action(() => {
GlobalModel.welcomeModalOpen.set(true);
})();
}
@boundMethod
handleSettingsClick(): void {
mobx.action(() => {
GlobalModel.clientSettingsModal.set(true);
})();
}
@boundMethod
handleConnectionsClick(): void {
GlobalModel.remotesModalModel.openModal();
}
@boundMethod
openSessionSettings(e: any, session: Session): void {
e.preventDefault();
e.stopPropagation();
mobx.action(() => {
GlobalModel.sessionSettingsModal.set(session.sessionId);
})();
}
render() {
let model = GlobalModel;
let activeSessionId = model.activeSessionId.get();
let activeScreen = model.getActiveScreen();
let activeRemoteId: string = null;
if (activeScreen != null) {
let rptr = activeScreen.curRemote.get();
if (rptr != null && !isBlank(rptr.remoteid)) {
activeRemoteId = rptr.remoteid;
}
}
let session: Session = null;
let remotes = model.remotes ?? [];
let remote: RemoteType = null;
let idx: number = 0;
remotes = sortAndFilterRemotes(remotes);
let sessionList = [];
for (let session of model.sessionList) {
if (!session.archived.get() || session.sessionId == activeSessionId) {
sessionList.push(session);
}
}
let isCollapsed = this.collapsed.get();
let mainView = GlobalModel.activeMainView.get();
return (
<div className={cn("main-sidebar", { collapsed: isCollapsed }, { "is-dev": GlobalModel.isDev })}>
<div className="logo-header">
<h1
className={cn(
"title",
"prompt-logo-small",
{ collapsed: isCollapsed },
{ "is-dev": GlobalModel.isDev }
)}
>
{isCollapsed ? "[p]" : "[prompt]"}
</h1>
</div>
<div className="collapse-container">
<div className="arrow-container" onClick={this.toggleCollapsed}>
<If condition={!isCollapsed}>
<i className="fa-sharp fa-solid fa-angle-left" />
</If>
<If condition={isCollapsed}>
<i className="fa-sharp fa-solid fa-angle-right" />
</If>
</div>
</div>
<div className="menu">
<p className="menu-label">Sessions</p>
<ul className="menu-list session-menu-list">
<If condition={!model.sessionListLoaded.get()}>
<li className="menu-loading-message">
<a>...</a>
</li>
</If>
<If condition={model.sessionListLoaded.get()}>
<For each="session" index="idx" of={sessionList}>
<li key={session.sessionId}>
<a
className={cn({
"is-active": mainView == "session" && activeSessionId == session.sessionId,
})}
onClick={() => this.handleSessionClick(session.sessionId)}
>
<If condition={!session.archived.get()}>
<div className="session-num">
<span className="hotkey">^</span>
{idx + 1}
</div>
</If>
<If condition={session.archived.get()}>
<div className="session-num">
<i title="archived" className="fa-sharp fa-solid fa-box-archive" />
</div>
</If>
<div>{session.name.get()}</div>
<div className="flex-spacer" />
<div
className="session-gear"
onClick={(e) => this.openSessionSettings(e, session)}
>
<i className="fa-sharp fa-solid fa-gear" />
</div>
</a>
</li>
</For>
<li className="new-session">
<a onClick={() => this.handleNewSession()}>
<i className="fa-sharp fa-solid fa-plus" /> New Session
</a>
</li>
</If>
</ul>
<ul className="menu-list" style={{ marginTop: 20 }}>
<li className="menu-history">
<a onClick={this.handleHistoryClick} className={cn({ "is-active": mainView == "history" })}>
<i className="fa-sharp fa-solid fa-clock" /> HISTORY{" "}
<span className="hotkey">&#x2318;H</span>
</a>
</li>
</ul>
<ul className="menu-list">
<li className="menu-bookmarks">
<a
onClick={this.handleBookmarksClick}
className={cn({ "is-active": mainView == "bookmarks" })}
>
<i className="fa-sharp fa-solid fa-bookmark" /> BOOKMARKS{" "}
<span className="hotkey">&#x2318;B</span>
</a>
</li>
</ul>
<p className="menu-label display-none">Playbooks</p>
<ul className="menu-list display-none">
<li key="default">
<a onClick={this.handlePlaybookClick}>
<i className="fa-sharp fa-solid fa-file-lines" /> default
</a>
</li>
<li key="prompt-dev">
<a onClick={this.handlePlaybookClick}>
<i className="fa-sharp fa-solid fa-file-lines" /> prompt-dev
</a>
</li>
</ul>
<div className="spacer"></div>
<If condition={GlobalModel.debugScreen.get() && activeScreen != null}>
<div>
focus={activeScreen.focusType.get()}
<br />
sline={activeScreen.getSelectedLine()}
<br />
termfocus={activeScreen.termLineNumFocus.get()}
<br />
</div>
</If>
<ul className="menu-list" style={{ display: "none" }}>
<li className="menu-bookmarks">
<a
onClick={this.handleWelcomeClick}
className={cn({ "is-active": GlobalModel.welcomeModalOpen.get() })}
>
<i className="fa-sharp fa-solid fa-door-open" /> WELCOME
</a>
</li>
</ul>
<ul className="menu-list">
<li className="menu-settings">
<a onClick={this.handleSettingsClick}>
<i className="fa-sharp fa-solid fa-cog" /> SETTINGS
</a>
</li>
</ul>
<p className="menu-label">
<a onClick={() => this.clickLinks()}>
LINKS{" "}
<i
className={cn(
"fa-sharp fa-solid",
GlobalModel.showLinks.get() ? "fa-angle-down" : "fa-angle-right"
)}
/>
</a>
</p>
<ul className="menu-list" style={{ display: GlobalModel.showLinks.get() ? null : "none" }}>
<li>
<a target="_blank" href="https://docs.getprompt.dev/releasenotes">
<i style={{ width: 20 }} className="fa-sharp fa-solid fa-notes" /> release notes
</a>
</li>
<li>
<a target="_blank" href="https://docs.getprompt.dev/">
<i style={{ width: 20 }} className="fa-sharp fa-solid fa-book" /> documentation
</a>
</li>
<li>
<a target="_blank" href="https://discord.gg/XfvZ334gwU">
<i style={{ width: 20 }} className="fa-brands fa-discord" /> discord
</a>
</li>
</ul>
<p className="menu-label">
<a onClick={this.handleConnectionsClick}>Connections</a>
</p>
<ul className="menu-list remotes-menu-list">
<For each="remote" of={remotes}>
<li key={remote.remoteid} className={cn("remote-menu-item")}>
<a
className={cn({ "is-active": remote.remoteid == activeRemoteId })}
onClick={() => this.clickRemote(remote)}
>
<RemoteStatusLight remote={remote} />
{this.remoteDisplayName(remote)}
</a>
</li>
</For>
<li key="add-remote" className="add-remote">
<a onClick={() => this.handleAddRemote()}>
<i className="fa-sharp fa-solid fa-plus" /> Add Connection
</a>
</li>
</ul>
<div className="bottom-spacer"></div>
</div>
</div>
);
}
}
export { MainSideBar };

View File

@ -1,3 +1,5 @@
@import "../../index.less";
.main-sidebar .logo-header {
display: flex;
flex-direction: row;

View File

@ -1,62 +0,0 @@
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 cn from "classnames";
import { GlobalModel, GlobalCommandRunner } from "./model";
import * as util from "./util";
type OV<V> = mobx.IObservableValue<V>;
class TosModal extends React.Component<{}, {}> {
@boundMethod
acceptTos(): void {
GlobalCommandRunner.clientAcceptTos();
}
render() {
return (
<div className={cn("modal tos-modal prompt-modal is-active")}>
<div className="modal-background" />
<div className="modal-content">
<header>
<div className="modal-title">Welcome to [prompt]</div>
</header>
<div className="inner-content">
<div className="content">
<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>
.
</p>
<p>
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.commandline.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>
</footer>
</div>
</div>
);
}
}
export { TosModal };

View File

@ -72,7 +72,7 @@ import {
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import customParseFormat from "dayjs/plugin/customParseFormat";
import { getRendererContext, cmdStatusIsRunning } from "./lineutil";
import { getRendererContext, cmdStatusIsRunning } from "./main/line/lineutil";
dayjs.extend(customParseFormat);
dayjs.extend(localizedFormat);

View File

@ -1,13 +1,15 @@
import * as React from "react";
import { RendererContext, RendererOpts, LineStateType, RendererModelContainerApi } from "../../types";
import Editor from "@monaco-editor/react";
import { Markdown } from "../../elements";
import { Markdown } from "../../common/common";
import { GlobalModel, GlobalCommandRunner } from "../../model";
import Split from "react-split-it";
import "./split.less";
import loader from "@monaco-editor/loader";
loader.config({ paths: { vs: "./node_modules/monaco-editor/min/vs" } });
import "./split.less";
import "../plugins.less";
function renderCmdText(text: string): any {
return <span>&#x2318;{text}</span>;
}

View File

@ -1,9 +1,9 @@
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 { RendererContext, RendererOpts } from "../types";
import "./plugins.less";
type OV<V> = mobx.IObservableValue<V>;
type CV<V> = mobx.IComputedValue<V>;

View File

@ -5,7 +5,9 @@ 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 { Markdown } from "../common/common";
import "./plugins.less";
type OV<V> = mobx.IObservableValue<V>;
@ -44,7 +46,10 @@ class SimpleMarkdownRenderer extends React.Component<
let dataBlob = this.props.data;
if (dataBlob == null || dataBlob.notFound) {
return (
<div className="renderer-container markdown-renderer" style={{ fontSize: this.props.opts.termFontSize }}>
<div
className="renderer-container markdown-renderer"
style={{ fontSize: this.props.opts.termFontSize }}
>
<div className="load-error-text">
ERROR: file {dataBlob && dataBlob.name ? JSON.stringify(dataBlob.name) : ""} not found
</div>
@ -53,7 +58,10 @@ class SimpleMarkdownRenderer extends React.Component<
}
if (this.markdownError.get() != null) {
return (
<div className="renderer-container markdown-renderer" style={{ fontSize: this.props.opts.termFontSize }}>
<div
className="renderer-container markdown-renderer"
style={{ fontSize: this.props.opts.termFontSize }}
>
<div className="load-error-text">{this.markdownError.get()}</div>
</div>
);

View File

@ -11,9 +11,9 @@ import mustache from "mustache";
import * as DOMPurify from "dompurify";
import { GlobalModel } from "../model";
type OV<V> = mobx.IObservableValue<V>;
import "./plugins.less";
const MaxMustacheSize = 200000;
type OV<V> = mobx.IObservableValue<V>;
@mobxReact.observer
class SimpleMustacheRenderer extends React.Component<

View File

@ -1,18 +1,16 @@
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 * as T from "../types";
import { debounce, throttle } from "throttle-debounce";
import { debounce } from "throttle-debounce";
import { boundMethod } from "autobind-decorator";
import { sprintf } from "sprintf-js";
import { PacketDataBuffer } from "../ptydata";
import { Markdown } from "../elements";
import { Markdown } from "../common/common";
import "./plugins.less";
type OV<V> = mobx.IObservableValue<V>;
type OArr<V> = mobx.IObservableArray<V>;
type OMap<K, V> = mobx.ObservableMap<K, V>;
type OpenAIOutputType = {
model: string;

337
src/plugins/plugins.less Normal file
View File

@ -0,0 +1,337 @@
@import "../index.less";
.alt-view {
background-color: #111;
overflow-y: auto;
flex-grow: 1;
.alt-title {
margin: 20px 10px 0px 5px;
padding-left: 10px;
padding-bottom: 12px;
.mono-font(1.5rem);
color: @term-bright-white;
border-bottom: 1px solid white;
}
.alt-list {
color: white;
margin: 4px 10px 5px 5px;
border-bottom: 1px solid white;
}
.no-content {
color: @term-white;
padding: 30px 10px 35px 10px;
border-bottom: 1px solid white;
}
.close-button {
position: absolute;
padding: 4px;
font-size: 24px;
color: #aaa;
right: 15px;
top: 18px;
cursor: pointer;
&:hover {
color: #fff;
}
}
.alt-help {
color: @term-white;
margin-top: 20px;
display: flex;
flex-direction: row;
align-items: center;
font-size: 12px;
margin-bottom: 10px;
.help-entry {
margin-left: 20px;
}
}
}
.image-renderer {
padding: 10px;
img {
display: block;
}
}
.renderer-container {
.error-container {
color: @term-red;
font-size: 14px;
padding: 5px;
}
.scroller {
overflow: auto;
display: flex;
flex-direction: row;
overscroll-behavior: contain;
}
.dropdown {
background: #dbdbdb;
color: black;
border-radius: 6px 6px 0 0;
font-size: 10px;
padding: 2px 0 5px 5px;
outline: none;
}
}
.renderer-container.code-renderer {
.scroller {
padding-top: 10px;
padding-bottom: 15px;
}
.monaco-editor .monaco-editor-background {
background-color: rgba(255, 255, 255, 0.075) !important;
}
.monaco-editor .scrollbar {
height: 4px !important;
width: 4px !important;
}
.monaco-editor .scrollbar .slider {
background-color: rgba(255, 255, 255) !important;
}
.cmd-hints,
.dropdown {
display: inline-block;
position: relative;
min-width: 6rem;
max-width: 6rem;
margin-right: 8px;
}
.hint-item {
border-radius: 4px 4px 0 0;
padding: 3px 9px 2px 8px;
line-height: 19px;
text-align: center;
}
section {
transition: height 0.3s ease-in-out;
}
.preview {
color: #000;
background-color: rgb(200, 200, 200);
}
.preview:hover {
background-color: white !important;
}
.save-enabled {
color: white;
background-color: #4e9a06;
}
.save-disabled {
color: rgb(52, 52, 52);
background-color: #aaaea7;
cursor: default !important;
}
.save-disabled:hover {
background-color: #aaaea7;
}
.close {
color: white;
background-color: #9e0000;
}
.message {
color: white;
border-radius: 6px;
margin-bottom: 1rem;
padding: 4px 1rem;
max-width: 80vw;
}
.readonly {
.mono-font(12px);
position: absolute;
top: calc(1.5rem + 3px);
right: 10rem;
border-radius: 5px;
background-color: @term-bright-red;
color: white;
z-index: 1;
padding: 0 6px 2px;
}
}
.renderer-container.mustache-renderer {
color: @term-white;
.cmd-hints {
display: inline-block !important;
position: relative;
margin-right: 26px;
}
.hint-item {
border-radius: 4px 4px 0 0;
padding: 3px 9px 2px 8px;
line-height: 15px;
text-align: center;
}
.refresh-button {
color: rgb(52, 52, 52);
background-color: @term-white;
}
}
.renderer-container .content {
padding: 5px;
line-height: 1.5;
width: fit-content;
blockquote {
background-color: #222;
}
code {
background-color: #222;
font-size: 14px;
}
pre {
background-color: #222;
margin: 2px 10px 6px 10px;
padding: 4px 4px 4px 6px;
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: @term-white;
}
}
.openai-renderer {
.openai-message {
display: flex;
flex-direction: row;
justify-content: flex-start;
.openai-role {
color: @term-bright-green;
font-weight: bold;
width: 100px;
}
.openai-role.openai-role-assistant {
color: @term-bright-white;
}
.openai-content-user {
white-space: pre;
color: white;
}
.openai-content-assistant {
color: white;
}
.openai-role-error {
color: @term-bright-red;
}
.openai-content-error {
color: @term-bright-red;
}
}
}
.markdown {
color: @term-white;
margin-bottom: 10px;
code {
background-color: black;
color: white;
.mono-font();
padding: 5px;
}
code.inline {
padding-top: 0;
padding-bottom: 0;
}
.title {
color: white;
margin-top: 16px;
line-height: 1.25;
margin-bottom: 8px;
}
strong {
color: white;
}
a {
color: #32afff;
}
table {
tr th {
color: white;
}
}
ul {
list-style-type: disc;
list-style-position: outside;
margin-left: 16px;
}
ol {
list-style-position: outside;
margin-left: 19px;
}
blockquote {
margin: 4px 10px 4px 10px;
border-radius: 3px;
background-color: #444;
padding: 2px 4px 2px 6px;
}
pre {
background-color: inherit;
margin: 4px 10px 4px 10px;
padding: 2px 4px 2px 6px;
}
.title.is-1 {
font-size: 32px;
border-bottom: 1px solid #777;
padding-bottom: 6px;
}
.title.is-2 {
font-size: 24px;
border-bottom: 1px solid #777;
padding-bottom: 6px;
}
.title.is-3 {
font-size: 20px;
}
.title.is-4 {
font-size: 16px;
}
.title.is-5 {
font-size: 14px;
}
.title.is-6 {
font-size: 14px;
}
}
.markdown > *:first-child {
margin-top: 0 !important;
}

View File

@ -1,10 +1,10 @@
import { RendererPluginType } from "./types";
import { SimpleImageRenderer } from "./view/image";
import { SimpleMarkdownRenderer } from "./view/markdown";
import { SourceCodeRenderer } from "./view/code";
import { SimpleMustacheRenderer } from "./view/mustache";
import { OpenAIRenderer, OpenAIRendererModel } from "./view/openai";
import { isBlank } from "./util";
import { RendererPluginType } from "../types";
import { SimpleImageRenderer } from "./image";
import { SimpleMarkdownRenderer } from "./markdown";
import { SourceCodeRenderer } from "./code";
import { SimpleMustacheRenderer } from "./mustache";
import { OpenAIRenderer, OpenAIRendererModel } from "./openai";
import { isBlank } from "../util";
import { sprintf } from "sprintf-js";
const ImagePlugin: RendererPluginType = {

View File

@ -1,416 +0,0 @@
@term-black: #000000;
@term-red: #cc0000;
@term-green: #4e9a06;
@term-yellow: #c4a000;
@term-blue: #3465a4;
@term-magenta: #75507b;
@term-cyan: #06989a;
@term-white: #d3d7cf;
@term-bright-black: #555753;
@term-bright-red: #ef2929;
@term-bright-green: #8ae234;
@term-bright-yellow: #fce94f;
@term-bright-blue: #32afff;
@term-bright-magenta: #ad7fa8;
@term-bright-cyan: #34e2e2;
@term-bright-white: #ffffff;
@tab-black: rgb(0, 0, 0);
@tab-red: rgb(205, 49, 49);
@tab-green: rgb(0, 128, 0);
@tab-orange: rgb(255, 199, 6);
@tab-yellow: rgb(229, 229, 16);
@tab-blue: rgb(0, 71, 171);
@tab-magenta: rgb(188, 63, 188);
@tab-cyan: rgb(17, 168, 205);
@tab-white: rgb(249, 249, 249);
@tab-black-text: #333;
@tab-white-text: #d7d7d7;
@prompt-green: rgb(0, 177, 10);
@soft-blue: #729fcf;
@active-menu-color: rgb(0, 71, 171);
@import "utils.less";
@import "webshare.less";
@import "views.less";
@import "sidebar.less";
@import "modals.less"; // includes settings
@import "comps.less"; // includes terminal
@import "tabs.less";
@import "cmdinput.less";
@import "lines.less";
// global settings / overrides
:root {
--fa-style-family: "Font Awesome 6 Sharp";
}
html,
body,
#app,
#main {
background-color: #000;
height: 100vh;
}
.content {
a:hover {
color: #485fc7;
}
}
body {
overflow: hidden;
}
body::-webkit-scrollbar {
display: none;
}
*::-webkit-scrollbar {
background-color: #777;
width: 5px;
height: 5px;
}
*::-webkit-scrollbar-thumb {
background: white;
}
input[type="checkbox"] {
cursor: pointer;
}
// main layout
#main {
height: 100vh;
display: flex;
flex-direction: column;
.main-content {
display: flex;
flex-direction: row;
background-color: black;
height: 100%;
.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;
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;
}
.image-renderer {
padding: 10px;
img {
display: block;
}
}
.renderer-container {
.error-container {
color: @term-red;
font-size: 14px;
padding: 5px;
}
.scroller {
overflow: auto;
display: flex;
flex-direction: row;
overscroll-behavior: contain;
}
.dropdown {
background: #dbdbdb;
color: black;
border-radius: 6px 6px 0 0;
font-size: 10px;
padding: 2px 0 5px 5px;
outline: none;
}
}
.renderer-container.code-renderer {
.scroller {
padding-top: 10px;
padding-bottom: 15px;
}
.monaco-editor .monaco-editor-background {
background-color: rgba(255, 255, 255, 0.075) !important;
}
.monaco-editor .scrollbar {
height: 4px !important;
width: 4px !important;
}
.monaco-editor .scrollbar .slider {
background-color: rgba(255, 255, 255) !important;
}
.cmd-hints,
.dropdown {
display: inline-block;
position: relative;
min-width: 6rem;
max-width: 6rem;
margin-right: 8px;
}
.hint-item {
border-radius: 4px 4px 0 0;
padding: 3px 9px 2px 8px;
line-height: 19px;
text-align: center;
}
section {
transition: height 0.3s ease-in-out;
}
.preview {
color: #000;
background-color: rgb(200, 200, 200);
}
.preview:hover {
background-color: white !important;
}
.save-enabled {
color: white;
background-color: #4e9a06;
}
.save-disabled {
color: rgb(52, 52, 52);
background-color: #aaaea7;
cursor: default !important;
}
.save-disabled:hover {
background-color: #aaaea7;
}
.close {
color: white;
background-color: #9e0000;
}
.message {
color: white;
border-radius: 6px;
margin-bottom: 1rem;
padding: 4px 1rem;
max-width: 80vw;
}
.readonly {
.mono-font(12px);
position: absolute;
top: calc(1.5rem + 3px);
right: 10rem;
border-radius: 5px;
background-color: @term-bright-red;
color: white;
z-index: 1;
padding: 0 6px 2px;
}
}
.renderer-container.json-renderer {
padding: 10px;
font-size: 12px;
}
.renderer-container.mustache-renderer {
color: @term-white;
.cmd-hints {
display: inline-block !important;
position: relative;
margin-right: 26px;
}
.hint-item {
border-radius: 4px 4px 0 0;
padding: 3px 9px 2px 8px;
line-height: 15px;
text-align: center;
}
.refresh-button {
color: rgb(52, 52, 52);
background-color: @term-white;
}
}
.renderer-container .content {
padding: 5px;
line-height: 1.5;
width: fit-content;
blockquote {
background-color: #222;
}
code {
background-color: #222;
font-size: 14px;
}
pre {
background-color: #222;
margin: 2px 10px 6px 10px;
padding: 4px 4px 4px 6px;
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: @term-white;
}
}
.openai-renderer {
.openai-message {
display: flex;
flex-direction: row;
justify-content: flex-start;
.openai-role {
color: @term-bright-green;
font-weight: bold;
width: 100px;
}
.openai-role.openai-role-assistant {
color: @term-bright-white;
}
.openai-content-user {
white-space: pre;
color: white;
}
.openai-content-assistant {
color: white;
}
.openai-role-error {
color: @term-bright-red;
}
.openai-content-error {
color: @term-bright-red;
}
}
}

248
src/remotes/remotes.less Normal file
View File

@ -0,0 +1,248 @@
@import "../index.less";
.modal.prompt-modal.remotes-modal {
.modal-content {
min-width: 850px;
}
.inner-content {
display: flex;
flex-direction: row;
align-items: stretch;
padding: 0;
max-height: 80vh;
.remotes-menu {
flex: 0 0 200px;
min-height: 450px;
border-right: 1px solid #666;
overflow-y: auto;
height: 100px;
.remote-menu-item {
border-top: 1px solid #666;
padding: 5px;
display: flex;
flex-direction: row;
cursor: pointer;
&.add-remote {
font-size: 13px;
padding: 10px 5px 10px 5px;
}
&:hover {
background-color: #333;
}
&.is-selected {
background-color: @active-menu-color;
.remote-name .remote-name-secondary {
color: white;
}
}
&:first-child {
border-top: 0;
}
.remote-status-light {
width: 15px;
margin-top: -2px;
}
.remote-name {
flex-grow: 1;
.remote-name-primary {
font-size: 12px;
font-weight: bold;
}
.remote-name-secondary {
font-size: 11px;
color: #777;
}
}
}
}
.remote-detail {
padding: 10px;
flex-grow: 1;
font-size: 12px;
display: flex;
flex-direction: column;
.settings-field {
margin-top: 5px;
}
* {
flex-shrink: 0;
}
.detail-subtitle {
font-size: 18px;
margin-bottom: 10px;
margin-top: 10px;
}
.title {
color: white;
padding-bottom: 8px;
margin-bottom: 0;
border-bottom: 1px solid #777;
}
.terminal-wrapper {
margin-left: 0;
margin-bottom: 0;
&.has-message {
margin-top: 0;
}
box-shadow: none;
border: 1px solid #777;
}
.action-buttons {
display: flex;
flex-direction: row;
gap: 10px;
margin-top: 2px;
}
.remote-message {
margin-top: 5px;
padding: 8px;
border-radius: 5px 5px 0 0;
background-color: #333;
i.fa-check {
color: @term-green;
}
&.is-ok {
}
.message-row {
display: flex;
flex-direction: row;
align-items: center;
}
.remote-status {
position: relative;
top: -1px;
}
.button {
height: 22px;
}
}
.settings-field {
.update-auth-button {
visibility: hidden;
}
&:hover {
.update-auth-button {
visibility: visible;
}
.hide-hover {
display: none;
}
}
}
&.auth-editing,
&.create-remote {
.settings-field.align-top {
align-items: flex-start;
.settings-label {
margin-top: 8px;
}
.settings-input {
align-items: flex-start;
}
}
.settings-label {
display: flex;
flex-direction: row;
align-items: center;
width: 135px;
}
.settings-field .settings-input .undo-icon {
cursor: pointer;
font-size: 18px;
margin-left: 5px;
}
.editremote-dropdown .dropdown-trigger button {
width: 120px;
justify-content: flex-start;
}
.settings-field .raw-input {
width: 120px;
}
.settings-input input {
width: 250px;
}
.dropdown .dropdown-trigger button {
font-size: 12px;
}
.dropdown .dropdown-item {
font-size: 12px;
padding: 5px 5px 5px 12px;
}
.settings-input {
.info-message {
margin-left: 22px;
}
}
.settings-label {
.info-message {
margin-right: 15px;
}
}
}
}
}
.terminal-wrapper {
position: relative;
background-color: #000;
padding: 2px 10px 5px 4px;
margin: 5px 5px 10px 5px;
box-shadow: 0 0 1px 1px rgba(255, 255, 255, 0.3);
&.focus {
box-shadow: 0 0 3px 3px rgba(255, 255, 255, 0.3);
}
.term-tag {
position: absolute;
top: 0;
right: 0;
background-color: @term-red;
color: white;
z-index: 110;
padding: 4px;
.mono-font(10px);
}
}
}

View File

@ -1,16 +1,16 @@
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 { If, For } from "tsx-control-statements/components";
import cn from "classnames";
import { GlobalModel, GlobalCommandRunner, getTermPtyData, RemotesModalModel } from "./model";
import { Toggle, RemoteStatusLight, InlineSettingsTextEdit, InfoMessage } from "./elements";
import { RemoteType, RemoteInputPacketType, RemoteEditType } from "./types";
import * as util from "./util";
import * as textmeasure from "./textmeasure";
import { TermWrap } from "./term";
import { GlobalModel, GlobalCommandRunner, RemotesModalModel } from "../model";
import { Toggle, RemoteStatusLight, InfoMessage } from "../common/common";
import { RemoteType, RemoteEditType } from "../types";
import * as util from "../util";
import * as textmeasure from "../textmeasure";
import "./remotes.less";
type OV<V> = mobx.IObservableValue<V>;
type OArr<V> = mobx.IObservableArray<V>;

View File

@ -7,9 +7,11 @@ import { v4 as uuidv4 } from "uuid";
import dayjs from "dayjs";
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 * as util from "./util";
import { GlobalModel, GlobalCommandRunner, Screen } from "../model";
import { WebStopShareConfirmMarkdown } from "../main/modals/settings";
import * as util from "../util";
import "./webshare.less";
type OV<V> = mobx.IObservableValue<V>;
type OArr<V> = mobx.IObservableArray<V>;

View File

@ -1,3 +1,5 @@
@import "../index.less";
body.prompt-webshare #main {
display: flex;
flex-direction: column;
@ -124,3 +126,42 @@ body.prompt-webshare #main {
}
}
}
.webshare-view {
.webshare-item {
padding: 4px 5px 8px 15px;
margin-bottom: 4px;
border-top: 1px solid white;
display: flex;
flex-direction: row;
position: relative;
min-height: 55px;
align-items: center;
&:first-child {
border-top: 0;
}
.webshare-vic {
width: 200px;
.webshare-vic-link {
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
.actions {
display: flex;
flex-direction: row;
gap: 10px;
}
&:hover {
background-color: #222;
}
}
}

View File

@ -5,7 +5,7 @@ const path = require("path");
module.exports = {
mode: "development",
entry: {
prompt: ["./src/prompt.ts", "./src/prompt.less"],
prompt: ["./src/index.ts", "./src/index.less"],
},
output: {
path: path.resolve(__dirname, "dist"),

View File

@ -5,7 +5,7 @@ const path = require("path");
module.exports = {
mode: "development",
entry: {
webshare: ["./src/webshare.ts", "./src/prompt.less"],
webshare: ["./src/webshare.ts", "./src/index.less"],
},
output: {
path: path.resolve(__dirname, "webshare/dist"),