diff --git a/package-lock.json b/package-lock.json index ed7a356ca..72d37f2cd 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 ec3e0e52e..e9813da24 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 68608e3b5..f082b6614 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 003ad822e..f60e6da22 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 000000000..e8233ee55 --- /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 000000000..94e1ceff0 --- /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 13144253d..c5a171733 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 f658fb687..5484fc750 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 000000000..be5696e37 --- /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 000000000..baf229879 --- /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 000000000..8319f3381 --- /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 000000000..9743341dd 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 000000000..5377e8f8d --- /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 000000000..63d878731 --- /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 000000000..98c6c8a35 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 000000000..c1637e157 --- /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 000000000..7b54c93e8 --- /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 000000000..786a6bc36 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 000000000..2d4ebcd09 --- /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 000000000..6442c41b4 --- /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 000000000..4e11dd0bb --- /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 000000000..dc4ac3a7f --- /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 000000000..be5696e37 --- /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 000000000..eec8cb52c --- /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 0bc4c7a62..a9a8809cc 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 8313a0ab5..c6478a67a 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 81ddceb96..5c60c64cd 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"