diff --git a/package-lock.json b/package-lock.json index ed7a356c..72d37f2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { - "name": "Prompt", - "version": "0.3.1", + "name": "waveterm", + "version": "0.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2289,6 +2289,15 @@ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, "emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -2306,6 +2315,40 @@ "strip-ansi": "^7.0.1" } }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, "strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", @@ -2315,6 +2358,23 @@ "ansi-regex": "^6.0.1" } }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + } + } + }, "wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -2325,6 +2385,60 @@ "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } + }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } } } }, @@ -4846,25 +4960,6 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "dev": true }, - "electron": { - "version": "25.4.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-25.4.0.tgz", - "integrity": "sha512-VLTRxDhL4UvQbqM7pTNENnJo62cdAPZT92N+B7BZQ5Xfok1wuVPEewIjBot4K7U3EpLUuHn1veeLzho3ihiP+Q==", - "dev": true, - "requires": { - "@electron/get": "^2.0.0", - "@types/node": "^18.11.18", - "extract-zip": "^2.0.1" - }, - "dependencies": { - "@types/node": { - "version": "18.17.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.1.tgz", - "integrity": "sha512-xlR1jahfizdplZYRU59JlUx9uzF1ARa8jbhM11ccpCJya8kvos5jwdm2ZAgxSCwOl0fq21svP18EVwPBXMQudw==", - "dev": true - } - } - }, "electron-installer-common": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/electron-installer-common/-/electron-installer-common-0.10.3.tgz", @@ -8897,6 +8992,53 @@ } } }, + "raw-loader": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz", + "integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, "rcedit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/rcedit/-/rcedit-3.1.0.tgz", @@ -9897,17 +10039,6 @@ "strip-ansi": "^6.0.1" } }, - "string-width-cjs": { - "version": "npm:string-width@4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, "string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -9925,15 +10056,6 @@ "ansi-regex": "^5.0.1" } }, - "strip-ansi-cjs": { - "version": "npm:strip-ansi@6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, "strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -11050,17 +11172,6 @@ "strip-ansi": "^6.0.0" } }, - "wrap-ansi-cjs": { - "version": "npm:wrap-ansi@7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index ec3e0e52..e9813da2 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "lodash-webpack-plugin": "^0.11.6", "mini-css-extract-plugin": "^2.6.0", "prettier": "^2.8.8", + "raw-loader": "^4.0.2", "react-split-it": "^2.0.0", "style-loader": "^3.3.1", "typescript": "^4.7.3", diff --git a/src/app/app.less b/src/app/app.less index 68608e3b..f082b661 100644 --- a/src/app/app.less +++ b/src/app/app.less @@ -197,7 +197,8 @@ a.a-block { height: 100%; .history-view, - .bookmarks-view { + .bookmarks-view, + .plugins-view { flex-grow: 1; display: flex; flex-direction: column; diff --git a/src/app/app.tsx b/src/app/app.tsx index 003ad822..f60e6da2 100644 --- a/src/app/app.tsx +++ b/src/app/app.tsx @@ -11,6 +11,8 @@ import type { ContextMenuOpts } from "../types/types"; import localizedFormat from "dayjs/plugin/localizedFormat"; import { GlobalModel } from "../model/model"; import { isBlank } from "../util/util"; +import { WorkspaceView } from "./workspace/workspaceview"; +import { PluginsView } from "./pluginsview/pluginsview"; import { BookmarksView } from "./bookmarks/bookmarks"; import { HistoryView } from "./history/history"; import { @@ -21,7 +23,6 @@ import { } from "./common/modals/settings"; import { RemotesModal } from "./connections/connections"; import { TosModal } from "./common/modals/modals"; -import { WorkspaceView } from "../app/workspace/workspaceview"; import { MainSideBar } from "./sidebar/MainSideBar"; import { DisconnectedModal, ClientStopModal, AlertModal, WelcomeModal } from "./common/modals/modals"; import { ErrorBoundary } from "./common/error/errorboundary"; @@ -107,11 +108,13 @@ class App extends React.Component<{}, {}> { if (dcWait) { setTimeout(() => this.updateDcWait(false), 0); } + //console.log(`GlobalModel.activeMainView.get() = ${GlobalModel.activeMainView.get()}`); // @mike - if I remove this, I cant see plugins return (
+ diff --git a/src/app/pluginsview/pluginsview.less b/src/app/pluginsview/pluginsview.less new file mode 100644 index 00000000..e8233ee5 --- /dev/null +++ b/src/app/pluginsview/pluginsview.less @@ -0,0 +1,137 @@ +@import "../../app/common/themes/themes.less"; + +.plugins-view { + background-color: @background-session; + + .header { + margin: 1.5em 1.5em 0.5em; + + .plugins-title { + margin-bottom: 0.5em; + font-weight: bold; + font-size: 1.5em; + } + + .close-button { + position: absolute; + right: 1em; + top: 0.8em; + cursor: pointer; + width: 1.5em; + height: 1.5em; + border-radius: 50%; + svg { + width: 1.5em; + height: 1.5em; + fill: @base-color; + } + } + } + + .body { + display: flex; + .plugins-list { + padding: 1em; + border-right: 1px solid @base-border; + min-height: calc(100vh - 6em); + max-height: calc(100vh - 6em); + overflow-y: auto; + flex: 0 0 auto; + .plugin-summary { + display: block; + padding: 10px; + max-width: 323px; + border-radius: 8px; + margin-bottom: 1em; + border: 1px solid transparent; + &.selected { + border-color: @prompt-green; + } + .plugin-summary-header { + display: flex; + .plugin-summary-icon { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + svg { + width: 32px; + height: 32px; + fill: white; + } + } + .plugin-summary-info { + flex: 1; + margin: 0 12px; + .plugin-summary-title { + } + .plugin-summary-vendor { + font-size: 0.8em; + } + } + } + .plugin-summary-body { + margin-top: 8px; + min-height: 54px; + max-height: 54px; + overflow-y: auto; + } + } + } + .plugins-details { + flex: 1 !important; + padding: 2em; + min-height: calc(100vh - 6em); + max-height: calc(100vh - 6em); + overflow-y: auto; + flex: 0 0 auto; + .plugin-label { + min-width: 100%; + margin-bottom: 1em; + } + .plugin-summary-header { + display: flex; + .plugin-summary-icon { + svg { + width: 48px; + height: 48px; + fill: white; + } + } + .plugin-summary-info { + flex: 1; + margin: 0 12px; + .plugin-summary-title { + font-size: 1.1em; + margin-top: 0.3em; + } + .plugin-summary-vendor { + font-size: 0.9em; + margin-top: 0.4em; + } + } + } + .plugin-summary-body { + margin-top: 1em; + } + .plugin-screenshots-container { + margin-top: 3em; + .plugin-screenshots { + display: flex; + overflow-x: auto; + width: 100%; + white-space: nowrap; + align-items: flex-start; + img { + flex-shrink: 0; + margin-right: 10px; + max-height: 50vh; + } + } + } + .plugin-readme { + margin-top: 3em; + } + } + } +} diff --git a/src/app/pluginsview/pluginsview.tsx b/src/app/pluginsview/pluginsview.tsx new file mode 100644 index 00000000..94e1ceff --- /dev/null +++ b/src/app/pluginsview/pluginsview.tsx @@ -0,0 +1,101 @@ +// Copyright 2023, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import * as React from "react"; +import * as mobxReact from "mobx-react"; +import * as mobx from "mobx"; +import { boundMethod } from "autobind-decorator"; +import { GlobalModel } from "../../model/model"; +import { PluginModel } from "../../plugins/plugins"; +import { ImageDisplay, Markdown } from "../common/common"; + +import { ReactComponent as XmarkIcon } from "../assets/icons/line/xmark.svg"; + +import "./pluginsview.less"; + +@mobxReact.observer +class PluginsView extends React.Component<{}, {}> { + @boundMethod + closeView(): void { + GlobalModel.pluginsModel.closeView(); + } + + render() { + if (GlobalModel.activeMainView.get() !== "plugins") { + return <>; + } + const { pluginsModel } = GlobalModel; + console.log(`rendering details for ${pluginsModel.selectedPlugin.get().name}`); + const PluginList = () => ( +
+ {PluginModel.allPlugins().map((plugin, i) => ( +
pluginsModel.setSelectedPlugin(plugin)} + > +
+
{plugin.getIcon()}
+
+
{plugin.title}
+
{plugin.vendor}
+
+
+
{plugin.summary}
+
+ ))} +
+ ); + + const PluginDetails = () => { + const plugin = pluginsModel.selectedPlugin.get(); + return ( +
+
+
{plugin.getIcon()}
+
+
{plugin.title}
+
{plugin.vendor}
+
+
+
{plugin.summary}
+ {plugin.screenshots && plugin.screenshots.length > 0 && ( +
+
{"Screenshots"}
+
+ {plugin.screenshots.map((path, index) => ( + {`Screenshot + ))} +
+
+ )} + {plugin.readme && ( +
+
{"Readme"}
+ +
+ )} +
+ ); + }; + + return ( +
+
+
Apps
+
+ +
+
+
+ + +
+
+ ); + } +} + +export { PluginsView }; diff --git a/src/app/sidebar/MainSideBar.tsx b/src/app/sidebar/MainSideBar.tsx index 13144253..c5a17173 100644 --- a/src/app/sidebar/MainSideBar.tsx +++ b/src/app/sidebar/MainSideBar.tsx @@ -76,12 +76,19 @@ class MainSideBar extends React.Component<{}, {}> { GlobalCommandRunner.showRemote(remote.remoteid); } + @boundMethod + handlePluginsClick(): void { + if (GlobalModel.activeMainView.get() == "plugins") { + GlobalModel.showSessionView(); + return; + } + GlobalModel.pluginsModel.showPluginsView(); + } + @boundMethod handleHistoryClick(): void { if (GlobalModel.activeMainView.get() == "history") { - mobx.action(() => { - GlobalModel.activeMainView.set("session"); - })(); + GlobalModel.showSessionView(); return; } GlobalModel.historyViewModel.reSearch(); @@ -150,7 +157,6 @@ class MainSideBar extends React.Component<{}, {}> { } return sessionList.map((session, index) => { const isActive = GlobalModel.activeMainView.get() == "session" && activeSessionId == session.sessionId; - /** @TODO: Handle archived sessions and talk to Mike about session settings */ return (
{
- {/*
+
Apps ⌘A -
*/} +
History diff --git a/src/model/model.ts b/src/model/model.ts index f658fb68..5484fc75 100644 --- a/src/model/model.ts +++ b/src/model/model.ts @@ -15,6 +15,7 @@ import { isModKeyPress, } from "../util/util"; import { TermWrap } from "../plugins/terminal/term"; +import { PluginModel } from "../plugins/plugins"; import type { SessionDataType, LineType, @@ -2423,7 +2424,36 @@ class BookmarksModel { return; } } - return; +} + +class PluginsModel { + selectedPlugin: OV = mobx.observable.box(null, { name: "selectedPlugin" }); + + showPluginsView(): void { + mobx.action(() => { + this.reset(); + GlobalModel.activeMainView.set("plugins"); + const allPlugins = PluginModel.allPlugins(); + this.selectedPlugin.set(allPlugins.length > 0 ? allPlugins[0] : null); + })(); + } + + setSelectedPlugin(plugin: RendererPluginType): void { + mobx.action(() => { + this.selectedPlugin.set(plugin); + })(); + } + + reset(): void { + mobx.action(() => { + this.selectedPlugin.set(null); + })(); + } + + closeView(): void { + GlobalModel.showSessionView(); + setTimeout(() => GlobalModel.inputModel.giveFocus(), 50); + } } class RemotesModalModel { @@ -2658,7 +2688,7 @@ class Model { authKey: string; isDev: boolean; platform: string; - activeMainView: OV<"session" | "history" | "bookmarks" | "webshare"> = mobx.observable.box("session", { + activeMainView: OV<"plugins" | "session" | "history" | "bookmarks" | "webshare"> = mobx.observable.box("session", { name: "activeMainView", }); termFontSize: CV; @@ -2684,6 +2714,7 @@ class Model { remotesModalModel: RemotesModalModel; inputModel: InputModel; + pluginsModel: PluginsModel; bookmarksModel: BookmarksModel; historyViewModel: HistoryViewModel; clientData: OV = mobx.observable.box(null, { @@ -2702,6 +2733,7 @@ class Model { ); this.ws.reconnect(); this.inputModel = new InputModel(); + this.pluginsModel = new PluginsModel(); this.bookmarksModel = new BookmarksModel(); this.historyViewModel = new HistoryViewModel(); this.remotesModalModel = new RemotesModalModel(); @@ -3203,7 +3235,9 @@ class Model { this.updateRemotes(update.remotes); } if ("mainview" in update) { - if (update.mainview == "bookmarks") { + if (update.mainview == "plugins") { + this.pluginsModel.showPluginsView(); + } else if (update.mainview == "bookmarks") { this.bookmarksModel.showBookmarksView(update.bookmarks, update.selectedbookmark); } else if (update.mainview == "session") { this.activeMainView.set("session"); diff --git a/src/plugins/code/icon.svg b/src/plugins/code/icon.svg new file mode 100644 index 00000000..be5696e3 --- /dev/null +++ b/src/plugins/code/icon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/plugins/code/meta.json b/src/plugins/code/meta.json new file mode 100644 index 00000000..baf22987 --- /dev/null +++ b/src/plugins/code/meta.json @@ -0,0 +1,5 @@ +{ + "title": "Code Viewer", + "vendor": "Wave", + "summary": "View and Edit source code with syntax highlightng and code completion." +} diff --git a/src/plugins/code/readme.md b/src/plugins/code/readme.md new file mode 100644 index 00000000..8319f338 --- /dev/null +++ b/src/plugins/code/readme.md @@ -0,0 +1,8 @@ +# Code Editor for Wave Terminal + +These instructions are for setting up the build on MacOS. +If you're developing on Linux please use the [Linux Build Instructions](./build-linux.md). + +## Running the Development Version of Wave + +If you install the production version of Wave, you'll see a semi-transparent sidebar, and the data for Wave is stored in the directory ~/prompt. The development version has a red/brown sidebar and stores its data in ~/prompt-dev. This allows the production and development versions to be run simultaneously with no conflicts. If the dev database is corrupted by development bugs, or the schema changes in development it will not affect the production copy. diff --git a/src/plugins/code/screenshots/1.png b/src/plugins/code/screenshots/1.png new file mode 100644 index 00000000..9743341d Binary files /dev/null and b/src/plugins/code/screenshots/1.png differ diff --git a/src/plugins/csv/icon.svg b/src/plugins/csv/icon.svg new file mode 100644 index 00000000..5377e8f8 --- /dev/null +++ b/src/plugins/csv/icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/plugins/csv/meta.json b/src/plugins/csv/meta.json new file mode 100644 index 00000000..63d87873 --- /dev/null +++ b/src/plugins/csv/meta.json @@ -0,0 +1,5 @@ +{ + "title": "CSV Viewer", + "vendor": "Wave", + "summary": "View CSV files inline from within the terminal. I am now trying an animated gif :)" +} diff --git a/src/plugins/csv/screenshots/csvview.gif b/src/plugins/csv/screenshots/csvview.gif new file mode 100644 index 00000000..98c6c8a3 Binary files /dev/null and b/src/plugins/csv/screenshots/csvview.gif differ diff --git a/src/plugins/image/icon.svg b/src/plugins/image/icon.svg new file mode 100644 index 00000000..c1637e15 --- /dev/null +++ b/src/plugins/image/icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/plugins/image/meta.json b/src/plugins/image/meta.json new file mode 100644 index 00000000..7b54c93e --- /dev/null +++ b/src/plugins/image/meta.json @@ -0,0 +1,5 @@ +{ + "title": "ImageViewer", + "vendor": "Wave", + "summary": "View Images inline in the terminal." +} diff --git a/src/plugins/image/screenshots/imagviewlion.gif b/src/plugins/image/screenshots/imagviewlion.gif new file mode 100644 index 00000000..786a6bc3 Binary files /dev/null and b/src/plugins/image/screenshots/imagviewlion.gif differ diff --git a/src/plugins/markdown/icon.svg b/src/plugins/markdown/icon.svg new file mode 100644 index 00000000..2d4ebcd0 --- /dev/null +++ b/src/plugins/markdown/icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/plugins/markdown/meta.json b/src/plugins/markdown/meta.json new file mode 100644 index 00000000..6442c41b --- /dev/null +++ b/src/plugins/markdown/meta.json @@ -0,0 +1,5 @@ +{ + "title": "Markdown", + "vendor": "Wave", + "summary": "Markdown is a lightweight markup language for creating formatted text using a plain-text editor." +} diff --git a/src/plugins/mustache/icon.svg b/src/plugins/mustache/icon.svg new file mode 100644 index 00000000..4e11dd0b --- /dev/null +++ b/src/plugins/mustache/icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/plugins/mustache/meta.json b/src/plugins/mustache/meta.json new file mode 100644 index 00000000..dc4ac3a7 --- /dev/null +++ b/src/plugins/mustache/meta.json @@ -0,0 +1,5 @@ +{ + "title": "Mustache", + "vendor": "Wave", + "summary": "Mustache templates help create html files." +} diff --git a/src/plugins/openai/icon.svg b/src/plugins/openai/icon.svg new file mode 100644 index 00000000..be5696e3 --- /dev/null +++ b/src/plugins/openai/icon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/plugins/openai/meta.json b/src/plugins/openai/meta.json new file mode 100644 index 00000000..eec8cb52 --- /dev/null +++ b/src/plugins/openai/meta.json @@ -0,0 +1,5 @@ +{ + "title": "OpenAI", + "vendor": "Wave", + "summary": "OpenAI plugin allows chatting with OpenAI APIs." +} diff --git a/src/plugins/plugins.ts b/src/plugins/plugins.ts index 0bc4c7a6..a9a8809c 100644 --- a/src/plugins/plugins.ts +++ b/src/plugins/plugins.ts @@ -11,89 +11,156 @@ import { OpenAIRenderer, OpenAIRendererModel } from "./openai/openai"; import { isBlank } from "../util/util"; import { sprintf } from "sprintf-js"; -const ImagePlugin: RendererPluginType = { - name: "image", - rendererType: "simple", - heightType: "pixels", - dataType: "blob", - collapseType: "hide", - globalCss: null, - mimeTypes: ["image/*"], - simpleComponent: SimpleImageRenderer, -}; - -const MarkdownPlugin: RendererPluginType = { - name: "markdown", - rendererType: "simple", - heightType: "pixels", - dataType: "blob", - collapseType: "hide", - globalCss: null, - mimeTypes: ["text/markdown"], - simpleComponent: SimpleMarkdownRenderer, -}; - -const MustachePlugin: RendererPluginType = { - name: "mustache", - rendererType: "simple", - heightType: "pixels", - dataType: "blob", - collapseType: "hide", - globalCss: null, - mimeTypes: ["text/plain"], - simpleComponent: SimpleMustacheRenderer, -}; - -const CodePlugin: RendererPluginType = { - name: "code", - rendererType: "simple", - heightType: "pixels", - dataType: "blob", - collapseType: "hide", - globalCss: null, - mimeTypes: ["text/plain"], - simpleComponent: SourceCodeRenderer, -}; - -const OpenAIPlugin: RendererPluginType = { - name: "openai", - rendererType: "full", - heightType: "pixels", - dataType: "model", - collapseType: "remove", - hidePrompt: true, - globalCss: null, - mimeTypes: ["application/json"], - fullComponent: OpenAIRenderer, - modelCtor: () => new OpenAIRendererModel(), -}; - -const CSVPlugin: RendererPluginType = { - name: "csv", - rendererType: "simple", - heightType: "pixels", - dataType: "blob", - collapseType: "hide", - globalCss: null, - mimeTypes: ["text/csv"], - simpleComponent: CSVRenderer, -}; +// TODO: @mike - I did refactoring with the though that I can move config out of this plugins.ts file to a +// plugins.json file. This way, adding a new plugin would reuire adding an entry to the json config. At a later +// stage, a plugin can become a self-contained-bundle, which would have my_plugin.json into it. it will be easy to +// merge this my_plugin.json into the big plugins.json. I got stuck while defining 'simpleComponent: SimpleImageRenderer' +// in a json definition (something like Java.Reflection can be used to compose a class from its name. will try later) +const PluginConfigs: RendererPluginType[] = [ + { + name: "markdown", + rendererType: "simple", + heightType: "pixels", + dataType: "blob", + collapseType: "hide", + globalCss: null, + mimeTypes: ["text/markdown"], + simpleComponent: SimpleMarkdownRenderer, + }, + { + name: "mustache", + rendererType: "simple", + heightType: "pixels", + dataType: "blob", + collapseType: "hide", + globalCss: null, + mimeTypes: ["text/plain"], + simpleComponent: SimpleMustacheRenderer, + }, + { + name: "code", + rendererType: "simple", + heightType: "pixels", + dataType: "blob", + collapseType: "hide", + globalCss: null, + mimeTypes: ["text/plain"], + simpleComponent: SourceCodeRenderer, + }, + { + name: "openai", + rendererType: "full", + heightType: "pixels", + dataType: "model", + collapseType: "remove", + hidePrompt: true, + globalCss: null, + mimeTypes: ["application/json"], + fullComponent: OpenAIRenderer, + modelCtor: () => new OpenAIRendererModel(), + }, + { + name: "csv", + rendererType: "simple", + heightType: "pixels", + dataType: "blob", + collapseType: "hide", + globalCss: null, + mimeTypes: ["text/csv"], + simpleComponent: CSVRenderer, + }, + { + name: "image", + rendererType: "simple", + heightType: "pixels", + dataType: "blob", + collapseType: "hide", + globalCss: null, + mimeTypes: ["image/*"], + simpleComponent: SimpleImageRenderer, + }, +]; class PluginModelClass { rendererPlugins: RendererPluginType[] = []; - registerRendererPlugin(plugin: RendererPluginType) { - if (isBlank(plugin.name)) { - throw new Error("invalid plugin, no name"); + constructor(pluginConfigs: RendererPluginType[]) { + this.rendererPlugins = pluginConfigs.map((plugin: RendererPluginType): RendererPluginType => { + if (isBlank(plugin.name)) { + throw new Error("invalid plugin, no name"); + } + if (plugin.name == "terminal" || plugin.name == "none") { + throw new Error(sprintf("invalid plugin, name '%s' is reserved", plugin.name)); + } + let existingPlugin = this.getRendererPluginByName(plugin.name); + if (existingPlugin != null) { + throw new Error(sprintf("plugin with name %s already registered", plugin.name)); + } + this.rendererPlugins.push(plugin); + this.loadPluginResources(plugin); + return plugin; + }); + } + + // attach all screenshots. webpack doesnt allow dynamic paths, hence, we have to put static paths for each plugin + attachScreenshots(plugin) { + let screenshotsContext; + let imagePaths = []; + try { + switch (plugin.name) { + case "image": + screenshotsContext = require.context(`../plugins/image/screenshots`, false, /\.(png|jpe?g|gif)$/); + break; + case "markdown": + screenshotsContext = require.context( + `../plugins/markdown/screenshots`, + false, + /\.(png|jpe?g|gif)$/ + ); + break; + case "mustache": + screenshotsContext = require.context( + `../plugins/mustache/screenshots`, + false, + /\.(png|jpe?g|gif)$/ + ); + break; + case "code": + screenshotsContext = require.context(`../plugins/code/screenshots`, false, /\.(png|jpe?g|gif)$/); + break; + case "openai": + screenshotsContext = require.context(`../plugins/openai/screenshots`, false, /\.(png|jpe?g|gif)$/); + break; + case "csv": + screenshotsContext = require.context(`../plugins/csv/screenshots`, false, /\.(png|jpe?g|gif)$/); + break; + default: + return; + } + + imagePaths = screenshotsContext.keys().map(screenshotsContext); + } catch (error) { + console.error(`Failed to load screenshots for plugin ${plugin.name}`); } - if (plugin.name == "terminal" || plugin.name == "none") { - throw new Error(sprintf("invalid plugin, name '%s' is reserved", plugin.name)); - } - let existingPlugin = this.getRendererPluginByName(plugin.name); - if (existingPlugin != null) { - throw new Error(sprintf("plugin with name %s already registered", plugin.name)); - } - this.rendererPlugins.push(plugin); + plugin.screenshots = imagePaths.map((path) => path.default); + } + + // use dynamic import to attach the icon etc. ensure that the 'name' matches the dir the plugin is in + async loadPluginResources(plugin) { + this.attachScreenshots(plugin); + // attach other resources + const handleImportError = (error, resourceType) => + console.error(`Failed to load ${resourceType} for plugin ${plugin.name}`); + const iconPromise = import(`../plugins/${plugin.name}/icon.svg`) + .then((icon) => (plugin.getIcon = icon.ReactComponent)) + .catch((error) => handleImportError(error, "icon")); + const readmePromise = import(`../plugins/${plugin.name}/readme.md`) + .then((content) => (plugin.readme = content.default)) + .catch((error) => handleImportError(error, "readme")); + const metaPromise = import(`../plugins/${plugin.name}/meta.json`) + .then((json) => Object.assign(plugin, json)) + .catch((error) => handleImportError(error, "meta")); + return Promise.allSettled([iconPromise, readmePromise, metaPromise]); } getRendererPluginByName(name: string): RendererPluginType { @@ -105,17 +172,15 @@ class PluginModelClass { } return null; } + + allPlugins() { + return this.rendererPlugins; + } } let PluginModel: PluginModelClass = null; if ((window as any).PluginModel == null) { - PluginModel = new PluginModelClass(); - PluginModel.registerRendererPlugin(ImagePlugin); - PluginModel.registerRendererPlugin(MarkdownPlugin); - PluginModel.registerRendererPlugin(CodePlugin); - PluginModel.registerRendererPlugin(OpenAIPlugin); - PluginModel.registerRendererPlugin(MustachePlugin); - PluginModel.registerRendererPlugin(CSVPlugin); + PluginModel = new PluginModelClass(PluginConfigs); (window as any).PluginModel = PluginModel; } diff --git a/webpack/webpack.web.js b/webpack/webpack.web.js index 8313a0ab..c6478a67 100644 --- a/webpack/webpack.web.js +++ b/webpack/webpack.web.js @@ -65,7 +65,15 @@ var webCommon = { }, { test: /\.svg$/, - use: ["@svgr/webpack", "file-loader"], + use: [{ loader: "@svgr/webpack", options: { icon: true, svgo: false } }, "file-loader"], + }, + { + test: /\.md$/, + use: "raw-loader", + }, + { + test: /\.(png|jpe?g|gif)$/i, + use: "file-loader", }, ], }, @@ -128,4 +136,4 @@ if (BundleAnalyzerPlugin != null) { webProd.plugins.push(new BundleAnalyzerPlugin()); } -module.exports = {webDev: webDev, webProd: webProd}; +module.exports = { webDev: webDev, webProd: webProd }; diff --git a/yarn.lock b/yarn.lock index 81ddceb9..5c60c64c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6427,6 +6427,14 @@ raw-body@2.5.1: iconv-lite "0.4.24" unpipe "1.0.0" +raw-loader@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-4.0.2.tgz#1aac6b7d1ad1501e66efdac1522c73e59a584eb6" + integrity sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + rcedit@^3.0.1: version "3.1.0" resolved "https://registry.yarnpkg.com/rcedit/-/rcedit-3.1.0.tgz#1563ec7a5663de639f94c5dc85429db1da364b3e"