diff --git a/.github/workflows/build-helper.yml b/.github/workflows/build-helper.yml index 4439b963d..94d6a9efa 100644 --- a/.github/workflows/build-helper.yml +++ b/.github/workflows/build-helper.yml @@ -41,6 +41,7 @@ jobs: cd scripthaus; go get ./...; CGO_ENABLED=1 go build -o scripthaus cmd/main.go + echo $PWD >> $GITHUB_PATH - uses: actions/setup-node@v4 with: node-version: ${{env.NODE_VERSION}} @@ -53,7 +54,7 @@ jobs: - name: Install Yarn Dependencies run: yarn --frozen-lockfile - name: Build ${{ matrix.platform }}/${{ matrix.arch }} - run: ./scripthaus/scripthaus run ${{ matrix.scripthaus }} + run: scripthaus run ${{ matrix.scripthaus }} env: GOARCH: ${{ matrix.arch }} CSC_LINK: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_CERTIFICATE}} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 456593c51..07ae2985b 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -47,6 +47,32 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Checkout Scripthaus (Go only) + if: matrix.language == 'go' + uses: actions/checkout@v4 + with: + repository: scripthaus-dev/scripthaus + path: scripthaus + + - name: Setup Go (Go only) + uses: actions/setup-go@v5 + if: matrix.language == 'go' + with: + go-version: stable + cache-dependency-path: | + wavesrv/go.sum + waveshell/go.sum + scripthaus/go.sum + + - name: Install Scripthaus (Go only) + if: matrix.language == 'go' + run: | + go work use ./scripthaus; + cd scripthaus; + go get ./...; + CGO_ENABLED=1 go build -o scripthaus cmd/main.go + echo $PWD >> $GITHUB_PATH + # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v3 @@ -61,9 +87,14 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild + - name: Autobuild (not Go) + if: matrix.language != 'go' uses: github/codeql-action/autobuild@v3 + - name: Build (Go only) + if: matrix.language == 'go' + run: scripthaus run build-backend + # ℹī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun diff --git a/assets/default-keybindings.json b/assets/default-keybindings.json index c942ec4c4..fff2891d5 100644 --- a/assets/default-keybindings.json +++ b/assets/default-keybindings.json @@ -3,6 +3,10 @@ "command": "system:toggleDeveloperTools", "keys": ["Cmd:Option:i"] }, + { + "command": "system:hideWindow", + "keys": ["Cmd:m"] + }, { "command": "generic:cancel", "keys": ["Escape"] @@ -32,7 +36,7 @@ "keys": ["PageDown"] }, { - "command": "app:openHistory", + "command": "app:openHistoryView", "keys": ["Cmd:h"] }, { @@ -41,11 +45,13 @@ }, { "command": "app:openConnectionsView", - "keys": [] + "keys": [], + "commandStr": "/mainview connections" }, { "command": "app:openSettingsView", - "keys": [] + "keys": [], + "commandStr": "/mainview clientsettings" }, { "command": "app:newTab", @@ -125,39 +131,48 @@ }, { "command": "app:selectWorkspace-1", - "keys": ["Cmd:Ctrl:1"] + "keys": ["Cmd:Ctrl:1"], + "commandStr": "/session 1" }, { "command": "app:selectWorkspace-2", - "keys": ["Cmd:Ctrl:2"] + "keys": ["Cmd:Ctrl:2"], + "commandStr": "/session 2" }, { "command": "app:selectWorkspace-3", - "keys": ["Cmd:Ctrl:3"] + "keys": ["Cmd:Ctrl:3"], + "commandStr": "/session 3" }, { "command": "app:selectWorkspace-4", - "keys": ["Cmd:Ctrl:4"] + "keys": ["Cmd:Ctrl:4"], + "commandStr": "/session 4" }, { "command": "app:selectWorkspace-5", - "keys": ["Cmd:Ctrl:5"] + "keys": ["Cmd:Ctrl:5"], + "commandStr": "/session 5" }, { "command": "app:selectWorkspace-6", - "keys": ["Cmd:Ctrl:6"] + "keys": ["Cmd:Ctrl:6"], + "commandStr": "/session 6" }, { "command": "app:selectWorkspace-7", - "keys": ["Cmd:Ctrl:7"] + "keys": ["Cmd:Ctrl:7"], + "commandStr": "/session 7" }, { "command": "app:selectWorkspace-8", - "keys": ["Cmd:Ctrl:8"] + "keys": ["Cmd:Ctrl:8"], + "commandStr": "/session 8" }, { "command": "app:selectWorkspace-9", - "keys": ["Cmd:Ctrl:9"] + "keys": ["Cmd:Ctrl:9"], + "commandStr": "/session 9" }, { "command": "app:toggleSidebar", @@ -168,8 +183,9 @@ "keys": ["Cmd:d"] }, { - "command": "app:bookmarkActiveLine", - "keys": ["Cmd:b"] + "command": "app:openBookmarksView", + "keys": ["Cmd:b"], + "commandStr": "/bookmarks:show" }, { "command": "bookmarks:edit", @@ -213,7 +229,8 @@ }, { "command": "cmdinput:openHistory", - "keys": ["Ctrl:r"] + "keys": ["Ctrl:r"], + "commandStr": "/history" }, { "command": "cmdinput:openAIChat", diff --git a/public/fonts/firacode-bold.woff2 b/public/fonts/firacode-bold.woff2 new file mode 100644 index 000000000..349dc36a5 Binary files /dev/null and b/public/fonts/firacode-bold.woff2 differ diff --git a/public/fonts/firacode-regular.woff2 b/public/fonts/firacode-regular.woff2 new file mode 100644 index 000000000..f8b63fb01 Binary files /dev/null and b/public/fonts/firacode-regular.woff2 differ diff --git a/public/themes/term-default.css b/public/themes/term-default.css index 2a868253c..9e3fbc31d 100644 --- a/public/themes/term-default.css +++ b/public/themes/term-default.css @@ -3,7 +3,7 @@ :root { /* - * term colors (16 + 5) form the base terminal theme + * term colors (16 + 6) form the base terminal theme * for consistency these colors should be used by plugins/applications */ --term-black: #000000; @@ -27,5 +27,6 @@ --term-cmdtext: #ffffff; --term-foreground: #d3d7cf; --term-background: #000000; - --term-selection-background: #ffffff40; + --term-selection-background: #ffffff90; + --term-cursor-accent: #000000; } diff --git a/public/themes/term-light.css b/public/themes/term-light.css index 479a6a346..d17bc0eef 100644 --- a/public/themes/term-light.css +++ b/public/themes/term-light.css @@ -6,5 +6,6 @@ --term-foreground: #000000; --term-background: #fefefe; --term-cmdtext: #000000; - --term-selection-background: #00000018; + --term-selection-background: #00000040; + --term-cursor-accent: #000000; } diff --git a/src/app/bookmarks/bookmarks.tsx b/src/app/bookmarks/bookmarks.tsx index e3c21d2ca..9e675ca61 100644 --- a/src/app/bookmarks/bookmarks.tsx +++ b/src/app/bookmarks/bookmarks.tsx @@ -193,7 +193,7 @@ class BookmarksView extends React.Component<{}, {}> { let bookmarks = GlobalModel.bookmarksModel.bookmarks; let bookmark: BookmarkType = null; return ( - +
diff --git a/src/app/clientsettings/clientsettings.tsx b/src/app/clientsettings/clientsettings.tsx index 17f4f811e..764bc993b 100644 --- a/src/app/clientsettings/clientsettings.tsx +++ b/src/app/clientsettings/clientsettings.tsx @@ -87,6 +87,7 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove const availableFontFamilies: DropdownItem[] = []; availableFontFamilies.push({ label: "JetBrains Mono", value: "JetBrains Mono" }); availableFontFamilies.push({ label: "Hack", value: "Hack" }); + availableFontFamilies.push({ label: "Fira Code", value: "Fira Code" }); return availableFontFamilies; } diff --git a/src/app/common/modals/alert.tsx b/src/app/common/modals/alert.tsx index f128f26d8..714869aec 100644 --- a/src/app/common/modals/alert.tsx +++ b/src/app/common/modals/alert.tsx @@ -44,7 +44,7 @@ class AlertModal extends React.Component<{}, {}> { {message?.message} - + ; - api: RendererModelContainerApi; - savedHeight: number; - loading: OV; - loadError: OV = mobx.observable.box(null, { - name: "renderer-loadError", - }); - lineState: LineStateType; - ptyData: PtyDataType; - ptyDataSource: (termContext: TermContextUnion) => Promise; - dataBlob: Blob; - readOnly: boolean; - notFound: boolean; - - initialize(params: RendererModelInitializeParams): void { - this.loading = mobx.observable.box(true, { name: "renderer-loading" }); - this.isDone = mobx.observable.box(params.isDone, { - name: "renderer-isDone", - }); - this.context = params.context; - this.opts = params.opts; - this.api = params.api; - this.lineState = params.lineState; - this.savedHeight = params.savedHeight; - this.ptyDataSource = params.ptyDataSource; - if (this.isDone.get()) { - setTimeout(() => this.reload(0), 10); - } - } - - dispose(): void { - return; - } - - giveFocus(): void { - return; - } - - updateOpts(update: RendererOptsUpdate): void { - Object.assign(this.opts, update); - } - - updateHeight(newHeight: number): void { - if (this.savedHeight != newHeight) { - this.savedHeight = newHeight; - this.api.saveHeight(newHeight); - } - } - - setIsDone(): void { - if (this.isDone.get()) { - return; - } - mobx.action(() => { - this.isDone.set(true); - })(); - this.reload(0); - } - - reload(delayMs: number): void { - mobx.action(() => { - this.loading.set(true); - })(); - if (delayMs == 0) { - this.reload_noDelay(); - } else { - setTimeout(() => { - this.reload_noDelay(); - }, delayMs); - } - } - - reload_noDelay(): void { - let source = this.lineState["prompt:source"] || "pty"; - if (source == "pty") { - this.reloadPtyData(); - } else if (source == "file") { - this.reloadFileData(); - } else { - mobx.action(() => { - this.loadError.set("error: invalid load source: " + source); - })(); - } - } - - reloadFileData(): void { - // todo add file methods to API, so we don't have a GlobalModel dependency here! - let path = this.lineState["prompt:file"]; - if (util.isBlank(path)) { - mobx.action(() => { - this.loadError.set("renderer has file source, but no prompt:file specified"); - })(); - return; - } - let rtnp = GlobalModel.readRemoteFile(this.context.screenId, this.context.lineId, path); - rtnp.then((file) => { - this.notFound = (file as any).notFound; - this.readOnly = (file as any).readOnly; - this.dataBlob = file; - mobx.action(() => { - this.loading.set(false); - this.loadError.set(null); - })(); - }).catch((e) => { - mobx.action(() => { - this.loadError.set("error loading file data: " + e); - })(); - }); - } - - reloadPtyData(): void { - this.readOnly = true; - let rtnp = this.ptyDataSource(this.context); - if (rtnp == null) { - console.log("no promise returned from ptyDataSource (simplerenderer)", this.context); - return; - } - rtnp.then((ptydata) => { - this.ptyData = ptydata; - this.dataBlob = new Blob([this.ptyData.data]); - mobx.action(() => { - this.loading.set(false); - this.loadError.set(null); - })(); - }).catch((e) => { - mobx.action(() => { - this.loadError.set("error loading data: " + e); - })(); - }); - } - - receiveData(pos: number, data: Uint8Array, reason?: string): void { - // this.dataBuf.receiveData(pos, data, reason); - } -} - -@mobxReact.observer -class SimpleBlobRenderer extends React.Component< - { - rendererContainer: RendererContainerType; - lineId: string; - plugin: RendererPluginType; - onHeightChange: () => void; - initParams: RendererModelInitializeParams; - scrollToBringIntoViewport: () => void; - isSelected: boolean; - shouldFocus: boolean; - }, - {} -> { - model: SimpleBlobRendererModel; - wrapperDivRef: React.RefObject = React.createRef(); - rszObs: ResizeObserver; - updateHeight_debounced: (newHeight: number) => void; - - constructor(props: any) { - super(props); - let { rendererContainer, lineId, plugin, initParams } = this.props; - this.model = new SimpleBlobRendererModel(); - this.model.initialize(initParams); - rendererContainer.registerRenderer(lineId, this.model); - this.updateHeight_debounced = debounce(1000, this.updateHeight.bind(this)); - } - - updateHeight(newHeight: number): void { - this.model.updateHeight(newHeight); - } - - handleResize(entries: ResizeObserverEntry[]): void { - if (this.model.loading.get()) { - return; - } - if (this.props.onHeightChange) { - this.props.onHeightChange(); - } - if (!this.model.loading.get() && this.wrapperDivRef.current != null) { - let height = this.wrapperDivRef.current.offsetHeight; - this.updateHeight_debounced(height); - } - } - - checkRszObs() { - if (this.rszObs != null) { - return; - } - if (this.wrapperDivRef.current == null) { - return; - } - this.rszObs = new ResizeObserver(this.handleResize.bind(this)); - this.rszObs.observe(this.wrapperDivRef.current); - } - - componentDidMount() { - this.checkRszObs(); - } - - componentWillUnmount() { - let { rendererContainer, lineId } = this.props; - rendererContainer.unloadRenderer(lineId); - if (this.rszObs != null) { - this.rszObs.disconnect(); - this.rszObs = null; - } - } - - componentDidUpdate() { - this.checkRszObs(); - } - - render() { - let { plugin } = this.props; - let model = this.model; - if (model.loadError.get() != null) { - let errorText = model.loadError.get(); - let height = this.model.savedHeight; - return ( -
-
ERROR: {errorText}
-
- ); - } - if (model.loading.get()) { - let height = this.model.savedHeight; - return ( -
- loading content -
- ); - } - let Comp = plugin.simpleComponent; - if (Comp == null) { -
(no component found in plugin)
; - } - let { festate, cmdstr, exitcode } = this.props.initParams.rawCmd; - return ( -
- -
- ); - } -} - -export { SimpleBlobRendererModel, SimpleBlobRenderer }; diff --git a/src/electron/emain.ts b/src/electron/emain.ts index 5aff80c44..2b6114775 100644 --- a/src/electron/emain.ts +++ b/src/electron/emain.ts @@ -263,7 +263,12 @@ const menuTemplate: Electron.MenuItemConstructorOptions[] = [ { type: "separator" }, { role: "services" }, { type: "separator" }, - { role: "hide" }, + { + label: "Hide", + click: () => { + app.hide(); + }, + }, { role: "hideOthers" }, { type: "separator" }, { role: "quit" }, @@ -303,16 +308,21 @@ function shFrameNavHandler(event: Electron.Event { event.returnValue = true; }); +electron.ipcMain.on("hide-window", (event) => { + if (MainWindow != null) { + MainWindow.hide(); + } + event.returnValue = true; +}); + electron.ipcMain.on("get-id", (event) => { event.returnValue = instanceId + ":" + event.processId; }); diff --git a/src/electron/preload.js b/src/electron/preload.js index 14be4eb10..1b5f1a637 100644 --- a/src/electron/preload.js +++ b/src/electron/preload.js @@ -1,6 +1,7 @@ let { contextBridge, ipcRenderer } = require("electron"); contextBridge.exposeInMainWorld("api", { + hideWindow: () => ipc.Renderer.send("hide-window"), toggleDeveloperTools: () => ipcRenderer.send("toggle-developer-tools"), getId: () => ipcRenderer.sendSync("get-id"), getPlatform: () => ipcRenderer.sendSync("get-platform"), diff --git a/src/models/bookmarks.ts b/src/models/bookmarks.ts index ea69c6a45..a5e58f55b 100644 --- a/src/models/bookmarks.ts +++ b/src/models/bookmarks.ts @@ -206,7 +206,7 @@ class BookmarksModel { } handleDocKeyDown(e: any): void { - let waveEvent = adaptFromReactOrNativeKeyEvent(e); + const waveEvent = adaptFromReactOrNativeKeyEvent(e); if (checkKeyPressed(waveEvent, "Escape")) { e.preventDefault(); if (this.editingBookmark.get() != null) { diff --git a/src/models/clientsettingsview.ts b/src/models/clientsettingsview.ts index ad177e7ab..e74a02a53 100644 --- a/src/models/clientsettingsview.ts +++ b/src/models/clientsettingsview.ts @@ -3,6 +3,7 @@ import * as mobx from "mobx"; import { Model } from "./model"; +import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "@/util/keyutil"; class ClientSettingsViewModel { globalModel: Model; @@ -21,6 +22,15 @@ class ClientSettingsViewModel { this.globalModel.activeMainView.set("clientsettings"); })(); } + + handleDocKeyDown(e: any): void { + const waveEvent = adaptFromReactOrNativeKeyEvent(e); + if (checkKeyPressed(waveEvent, "Escape")) { + e.preventDefault(); + this.closeView(); + return; + } + } } export { ClientSettingsViewModel }; diff --git a/src/models/commandrunner.ts b/src/models/commandrunner.ts index 98150ccee..5b57a0c4d 100644 --- a/src/models/commandrunner.ts +++ b/src/models/commandrunner.ts @@ -304,6 +304,10 @@ class CommandRunner { GlobalModel.clientSettingsViewModel.showClientSettingsView(); } + syncShellState() { + GlobalModel.submitCommand("sync", null, null, { nohist: "1" }, false); + } + historyView(params: HistorySearchParams) { let kwargs = { nohist: "1" }; kwargs["offset"] = String(params.offset); diff --git a/src/models/connectionsview.ts b/src/models/connectionsview.ts index 6d819b88d..297beda1c 100644 --- a/src/models/connectionsview.ts +++ b/src/models/connectionsview.ts @@ -3,6 +3,7 @@ import * as mobx from "mobx"; import { Model } from "./model"; +import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "@/util/keyutil"; class ConnectionsViewModel { globalModel: Model; @@ -21,6 +22,15 @@ class ConnectionsViewModel { this.globalModel.activeMainView.set("connections"); })(); } + + handleDocKeyDown(e: any): void { + const waveEvent = adaptFromReactOrNativeKeyEvent(e); + if (checkKeyPressed(waveEvent, "Escape")) { + e.preventDefault(); + this.closeView(); + return; + } + } } export { ConnectionsViewModel }; diff --git a/src/models/historyview.ts b/src/models/historyview.ts index 6892b854a..63b934d20 100644 --- a/src/models/historyview.ts +++ b/src/models/historyview.ts @@ -291,7 +291,7 @@ class HistoryViewModel { } handleDocKeyDown(e: any): void { - let waveEvent = adaptFromReactOrNativeKeyEvent(e); + const waveEvent = adaptFromReactOrNativeKeyEvent(e); if (checkKeyPressed(waveEvent, "Escape")) { e.preventDefault(); this.closeView(); diff --git a/src/models/modals.ts b/src/models/modals.ts index 528c4992a..4c4dcfd99 100644 --- a/src/models/modals.ts +++ b/src/models/modals.ts @@ -18,10 +18,11 @@ class ModalsModel { } } - popModal() { + popModal(callback?: () => void) { mobx.action(() => { this.store.pop(); })(); + callback && callback(); } } diff --git a/src/models/model.ts b/src/models/model.ts index 15af1ae58..8d35ef550 100644 --- a/src/models/model.ts +++ b/src/models/model.ts @@ -143,7 +143,7 @@ class Model { this.runUpdate(message, interactive); }); this.ws.reconnect(); - this.keybindManager = new KeybindManager(); + this.keybindManager = new KeybindManager(this); this.readConfigKeybindings(); this.initSystemKeybindings(); this.initAppKeybindings(); @@ -222,46 +222,31 @@ class Model { getApi().toggleDeveloperTools(); return true; }); + this.keybindManager.registerKeybinding("system", "electron", "system:minimizeWindow", (waveEvent) => { + getApi().hideWindow(); + return true; + }); } initAppKeybindings() { for (let index = 1; index <= 9; index++) { - this.keybindManager.registerKeybinding("app", "model", "app:selectWorkspace-" + index, (waveEvent) => { - this.onSwitchSessionCmd(index); - return true; - }); + this.keybindManager.registerKeybinding("app", "model", "app:selectWorkspace-" + index, null); } - this.keybindManager.registerKeybinding("app", "model", "app:focusCmdInput", (waveEvent) => { - console.log("focus cmd input callback"); this.onFocusCmdInputPressed(); return true; }); - - this.keybindManager.registerKeybinding("app", "model", "app:bookmarkActiveLine", (waveEvent) => { - this.onBookmarkViewPressed(); - return true; - }); - - this.keybindManager.registerKeybinding("app", "model", "app:openHistory", (waveEvent) => { + this.keybindManager.registerKeybinding("app", "model", "app:openBookmarksView", null); + this.keybindManager.registerKeybinding("app", "model", "app:openHistoryView", (waveEvent) => { this.onOpenHistoryPressed(); return true; }); - this.keybindManager.registerKeybinding("app", "model", "app:openTabSearchModal", (waveEvent) => { this.onOpenTabSearchModalPressed(); return true; }); - - this.keybindManager.registerKeybinding("app", "model", "app:openConnectionsView", (waveEvent) => { - this.onOpenConnectionsViewPressed(); - return true; - }); - - this.keybindManager.registerKeybinding("app", "model", "app:openSettingsView", (waveEvent) => { - this.onOpenSettingsViewPressed(); - return true; - }); + this.keybindManager.registerKeybinding("app", "model", "app:openConnectionsView", null); + this.keybindManager.registerKeybinding("app", "model", "app:openSettingsView", null); } static getInstance(): Model { @@ -372,7 +357,6 @@ class Model { cancelAlert(): void { mobx.action(() => { this.alertMessage.set(null); - this.modalsModel.popModal(); })(); if (this.alertPromiseResolver != null) { this.alertPromiseResolver(false); @@ -493,7 +477,7 @@ class Model { if (this.alertMessage.get() != null) { if (checkKeyPressed(waveEvent, "Escape")) { e.preventDefault(); - this.cancelAlert(); + this.modalsModel.popModal(() => this.cancelAlert()); return; } if (checkKeyPressed(waveEvent, "Enter")) { @@ -503,6 +487,10 @@ class Model { } return; } + if (checkKeyPressed(waveEvent, "Escape") && this.modalsModel.store.length > 0) { + this.modalsModel.popModal(); + return; + } if (this.activeMainView.get() == "bookmarks") { this.bookmarksModel.handleDocKeyDown(e); } @@ -510,10 +498,10 @@ class Model { this.historyViewModel.handleDocKeyDown(e); } if (this.activeMainView.get() == "connections") { - this.historyViewModel.handleDocKeyDown(e); + this.connectionViewModel.handleDocKeyDown(e); } if (this.activeMainView.get() == "clientsettings") { - this.historyViewModel.handleDocKeyDown(e); + this.clientSettingsViewModel.handleDocKeyDown(e); } else { if (checkKeyPressed(waveEvent, "Escape")) { e.preventDefault(); @@ -521,9 +509,6 @@ class Model { this.showSessionView(); return; } - if (this.clearModals()) { - return; - } const inputModel = this.inputModel; inputModel.toggleInfoMsg(); if (inputModel.inputMode.get() != null) { @@ -643,33 +628,6 @@ class Model { return screen.getTermWrap(line.lineid); } - clearModals(): boolean { - let didSomething = false; - mobx.action(() => { - if (this.screenSettingsModal.get()) { - this.screenSettingsModal.set(null); - didSomething = true; - } - if (this.sessionSettingsModal.get()) { - this.sessionSettingsModal.set(null); - didSomething = true; - } - if (this.screenSettingsModal.get()) { - this.screenSettingsModal.set(null); - didSomething = true; - } - if (this.clientSettingsModal.get()) { - this.clientSettingsModal.set(false); - didSomething = true; - } - if (this.lineSettingsModal.get()) { - this.lineSettingsModal.set(null); - didSomething = true; - } - })(); - return didSomething; - } - restartWaveSrv(): void { getApi().restartWaveSrv(); } @@ -1023,6 +981,12 @@ class Model { console.warn("invalid bookmarksview in update:", update.mainview); } break; + case "clientsettings": + this.activeMainView.set("clientsettings"); + break; + case "connections": + this.activeMainView.set("connections"); + break; case "plugins": this.pluginsModel.showPluginsView(); break; @@ -1086,6 +1050,9 @@ class Model { this.activeMainView.set("session"); this.deactivateScreenLines(); this.ws.watchScreen(newActiveSessionId, newActiveScreenId); + setTimeout(() => { + GlobalCommandRunner.syncShellState(); + }, 100); } } else { console.warn("unknown update", genUpdate); @@ -1583,12 +1550,15 @@ class Model { return remote.remotecanonicalname; } - readRemoteFile(screenId: string, lineId: string, path: string): Promise { - const urlParams = { + readRemoteFile(screenId: string, lineId: string, path: string, mimetype?: string): Promise { + const urlParams: Record = { screenid: screenId, lineid: lineId, path: path, }; + if (mimetype != null) { + urlParams["mimetype"] = mimetype; + } const usp = new URLSearchParams(urlParams); const url = new URL(this.getBaseHostPort() + "/api/read-file?" + usp.toString()); const fetchHeaders = this.getFetchHeaders(); diff --git a/src/plugins/image/image.less b/src/plugins/image/image.less index 8ffe18850..240829084 100644 --- a/src/plugins/image/image.less +++ b/src/plugins/image/image.less @@ -1,4 +1,10 @@ .image-renderer { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + padding-top: var(--termpad); + img { display: block; } diff --git a/src/plugins/image/image.tsx b/src/plugins/image/image.tsx index 256a26bec..c46bad019 100644 --- a/src/plugins/image/image.tsx +++ b/src/plugins/image/image.tsx @@ -48,12 +48,15 @@ class SimpleImageRenderer extends React.Component< return (
- ERROR: file {dataBlob && dataBlob.name ? JSON.stringify(dataBlob.name) : ""} not found + ERROR: file {dataBlob?.name ? JSON.stringify(dataBlob.name) : ""} not found
); } if (this.objUrl == null) { + if (dataBlob.name?.endsWith(".svg")) { + dataBlob = new Blob([dataBlob], { type: "image/svg+xml" }) as ExtBlob; + } this.objUrl = URL.createObjectURL(dataBlob); } let opts = this.props.opts; diff --git a/src/plugins/media/media.less b/src/plugins/media/media.less new file mode 100644 index 000000000..a77d18b2b --- /dev/null +++ b/src/plugins/media/media.less @@ -0,0 +1,14 @@ +.media-renderer { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + padding-top: var(--termpad); + + video { + object-fit: contain; + object-position: center; + height: 100%; + width: auto; + } +} diff --git a/src/plugins/media/media.tsx b/src/plugins/media/media.tsx new file mode 100644 index 000000000..aad7b9858 --- /dev/null +++ b/src/plugins/media/media.tsx @@ -0,0 +1,60 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import * as React from "react"; +import * as mobx from "mobx"; +import * as mobxReact from "mobx-react"; +import * as util from "@/util/util"; +import { GlobalModel } from "@/models"; + +import "./media.less"; + +@mobxReact.observer +class SimpleMediaRenderer extends React.Component< + { data: ExtBlob; context: RendererContext; opts: RendererOpts; savedHeight: number; lineState: LineStateType }, + {} +> { + objUrl: string = null; + + componentWillUnmount() { + if (this.objUrl != null) { + URL.revokeObjectURL(this.objUrl); + } + } + + render() { + let dataBlob = this.props.data; + if (dataBlob == null || dataBlob.notFound) { + return ( +
+
+ ERROR: file {dataBlob && dataBlob.name ? JSON.stringify(dataBlob.name) : ""} not found +
+
+ ); + } + let fileUrl = this.props.lineState["wave:fileurl"]; + if (util.isBlank(fileUrl)) { + return ( +
+
+ ERROR: no fileurl found (please use `mediaview` to view media files) +
+
+ ); + } + let fullVideoUrl = GlobalModel.getBaseHostPort() + fileUrl; + const opts = this.props.opts; + const height = opts.idealSize.height - 10; + const width = opts.maxSize.width - 10; + return ( +
+ +
+ ); + } +} + +export { SimpleMediaRenderer }; diff --git a/src/plugins/pdf/pdf.less b/src/plugins/pdf/pdf.less new file mode 100644 index 000000000..4f58b90f7 --- /dev/null +++ b/src/plugins/pdf/pdf.less @@ -0,0 +1,7 @@ +.pdf-renderer { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + padding-top: var(--termpad); +} diff --git a/src/plugins/pdf/pdf.tsx b/src/plugins/pdf/pdf.tsx new file mode 100644 index 000000000..5e21d7152 --- /dev/null +++ b/src/plugins/pdf/pdf.tsx @@ -0,0 +1,49 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import * as React from "react"; +import * as mobx from "mobx"; +import * as mobxReact from "mobx-react"; + +import "./pdf.less"; + +@mobxReact.observer +class SimplePdfRenderer extends React.Component< + { data: ExtBlob; context: RendererContext; opts: RendererOpts; savedHeight: number }, + {} +> { + objUrl: string = null; + + componentWillUnmount() { + if (this.objUrl != null) { + URL.revokeObjectURL(this.objUrl); + } + } + + render() { + let dataBlob = this.props.data; + if (dataBlob == null || dataBlob.notFound) { + return ( +
+
+ ERROR: file {dataBlob && dataBlob.name ? JSON.stringify(dataBlob.name) : ""} not found +
+
+ ); + } + if (this.objUrl == null) { + const pdfBlob = new File([dataBlob], dataBlob.name ?? "file.pdf", { type: "application/pdf" }); + this.objUrl = URL.createObjectURL(pdfBlob); + } + const opts = this.props.opts; + const maxHeight = opts.maxSize.height - 10; + const maxWidth = opts.maxSize.width - 10; + return ( +
+