From cc6354ff51ebde25f97858d0be810ef298e5c3f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8jberg?= Date: Thu, 30 Jan 2025 15:52:55 -0500 Subject: [PATCH] wip --- package-lock.json | 87 ++++++++++++++++++++++++-------- package.json | 5 +- src/Ucm/WorkspaceScreen.elm | 22 ++++++-- src/css/ucm/workspace-screen.css | 9 ++++ src/main.js | 33 ++++++++++-- src/preload.js | 11 ++++ src/renderer.js | 26 ++++++++++ webpack.main.config.js | 1 + 8 files changed, 165 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3293aa7..c3cd5e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,10 @@ "version": "1.0.0", "hasInstallScript": true, "dependencies": { - "electron-squirrel-startup": "^1.0.1" + "@xterm/xterm": "^5.5.0", + "electron-squirrel-startup": "^1.0.1", + "node-pty": "^1.1.0-beta30", + "xterm-addon-fit": "^0.8.0" }, "devDependencies": { "@electron-forge/cli": "^7.6.1", @@ -522,6 +525,36 @@ "node": ">= 16.4.0" } }, + "node_modules/@electron-forge/web-multi-logger/node_modules/xterm": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.19.0.tgz", + "integrity": "sha512-c3Cp4eOVsYY5Q839dR5IejghRPpxciGmLWWaP9g+ppfMeBChMeLa1DCA+pmX/jyDZ+zxFOmlJL/82qVdayVoGQ==", + "deprecated": "This package is now deprecated. Move to @xterm/xterm instead.", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron-forge/web-multi-logger/node_modules/xterm-addon-fit": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz", + "integrity": "sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==", + "deprecated": "This package is now deprecated. Move to @xterm/addon-fit instead.", + "dev": true, + "license": "MIT", + "peerDependencies": { + "xterm": "^4.0.0" + } + }, + "node_modules/@electron-forge/web-multi-logger/node_modules/xterm-addon-search": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/xterm-addon-search/-/xterm-addon-search-0.8.2.tgz", + "integrity": "sha512-I1863mjn8P6uVrqm/X+btalVsqjAKLhnhpbP7SavAOpEkI1jJhbHU2UTp7NjeRtcKTks6UWk/ycgds5snDSejg==", + "deprecated": "This package is now deprecated. Move to @xterm/addon-search instead.", + "dev": true, + "license": "MIT", + "peerDependencies": { + "xterm": "^4.0.0" + } + }, "node_modules/@electron/asar": { "version": "3.2.18", "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.18.tgz", @@ -1880,6 +1913,12 @@ "node": ">=10.0.0" } }, + "node_modules/@xterm/xterm": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", + "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", + "license": "MIT" + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -7695,6 +7734,12 @@ "node": ">=10" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/node-api-version": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.0.tgz", @@ -7845,6 +7890,16 @@ "webpack": "^5.0.0" } }, + "node_modules/node-pty": { + "version": "1.1.0-beta30", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta30.tgz", + "integrity": "sha512-cmNYVWfbf961aOqnxIFXssvw6Fp6/78BQBNlwYRWUHBenJjUhCJ1wMZpJy+SegoLC07P9D6HTtq39Kd89rpv/w==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^7.1.0" + } + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -11612,33 +11667,21 @@ } }, "node_modules/xterm": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.19.0.tgz", - "integrity": "sha512-c3Cp4eOVsYY5Q839dR5IejghRPpxciGmLWWaP9g+ppfMeBChMeLa1DCA+pmX/jyDZ+zxFOmlJL/82qVdayVoGQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz", + "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==", "deprecated": "This package is now deprecated. Move to @xterm/xterm instead.", - "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/xterm-addon-fit": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz", - "integrity": "sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.8.0.tgz", + "integrity": "sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==", "deprecated": "This package is now deprecated. Move to @xterm/addon-fit instead.", - "dev": true, "license": "MIT", "peerDependencies": { - "xterm": "^4.0.0" - } - }, - "node_modules/xterm-addon-search": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/xterm-addon-search/-/xterm-addon-search-0.8.2.tgz", - "integrity": "sha512-I1863mjn8P6uVrqm/X+btalVsqjAKLhnhpbP7SavAOpEkI1jJhbHU2UTp7NjeRtcKTks6UWk/ycgds5snDSejg==", - "deprecated": "This package is now deprecated. Move to @xterm/addon-search instead.", - "dev": true, - "license": "MIT", - "peerDependencies": { - "xterm": "^4.0.0" + "xterm": "^5.0.0" } }, "node_modules/y18n": { diff --git a/package.json b/package.json index 930c1a2..5e83b27 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,9 @@ }, "keywords": [], "dependencies": { - "electron-squirrel-startup": "^1.0.1" + "@xterm/xterm": "^5.5.0", + "electron-squirrel-startup": "^1.0.1", + "node-pty": "^1.1.0-beta30", + "xterm-addon-fit": "^0.8.0" } } diff --git a/src/Ucm/WorkspaceScreen.elm b/src/Ucm/WorkspaceScreen.elm index aaa559b..e2d7d33 100644 --- a/src/Ucm/WorkspaceScreen.elm +++ b/src/Ucm/WorkspaceScreen.elm @@ -1,11 +1,12 @@ -module Ucm.WorkspaceScreen exposing (..) +port module Ucm.WorkspaceScreen exposing (..) import Browser import Code.BranchRef as BranchRef import Code.CodebaseTree as CodebaseTree import Code.Config import Html exposing (Html, div, text) -import Html.Attributes exposing (class) +import Html.Attributes exposing (class, id) +import Lib.Util as Util import RemoteData exposing (RemoteData(..)) import UI.AnchoredOverlay as AnchoredOverlay import UI.Button as Button @@ -72,6 +73,7 @@ init appContext workspaceContext = , Cmd.batch [ Cmd.map CodebaseTreeMsg codebaseTreeCmd , Cmd.map WorkspacePanesMsg panesCmd + , Util.delayMsg 5000 OpenTerminal ] ) @@ -88,6 +90,7 @@ type Msg | ToggleSidebar | ToggleRightPane | RefreshCodebase + | OpenTerminal | Keydown KeyboardEvent.KeyboardEvent | CodebaseTreeMsg CodebaseTree.Msg | WorkspacePanesMsg WorkspacePanes.Msg @@ -104,6 +107,9 @@ type OutMsg update : AppContext -> Msg -> Model -> ( Model, Cmd Msg, OutMsg ) update appContext msg model = case msg of + OpenTerminal -> + ( model, openTerminal "terminal-pane", None ) + CodebaseTreeMsg codebaseTreeMsg -> let ( codebaseTree, codebaseTreeCmd, outMsg ) = @@ -388,6 +394,13 @@ update appContext msg model = +-- PORTS + + +port openTerminal : String -> Cmd msg + + + -- SUBSCRIPTIONS @@ -538,7 +551,10 @@ view appContext model = window_ content = - [ Html.map WorkspacePanesMsg (WorkspacePanes.view model.panes) ] + [ Html.map WorkspacePanesMsg + (WorkspacePanes.view model.panes) + , div [ id "terminal-pane" ] [] + ] in window__ |> Window.withTitlebarLeft (titlebarLeft model) diff --git a/src/css/ucm/workspace-screen.css b/src/css/ucm/workspace-screen.css index 1441b59..ca8ebfa 100644 --- a/src/css/ucm/workspace-screen.css +++ b/src/css/ucm/workspace-screen.css @@ -63,3 +63,12 @@ color: var(--u-color_icon); } } + +#terminal-pane { + border-top: 1px solid var(--u-color_border); + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 300px; +} diff --git a/src/main.js b/src/main.js index fde8487..b1a124f 100644 --- a/src/main.js +++ b/src/main.js @@ -1,6 +1,7 @@ -import { app, BrowserWindow } from 'electron'; -import path from 'node:path'; +import { app, BrowserWindow, ipcMain } from 'electron'; import started from 'electron-squirrel-startup'; +import * as os from 'os'; +import * as pty from 'node-pty'; // Handle creating/removing shortcuts on Windows when installing/uninstalling. if (started) { @@ -22,13 +23,15 @@ const createWindow = () => { // and load the index.html of the app. mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY); + + return mainWindow; }; // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. app.whenReady().then(() => { - createWindow(); + const mainWindow = createWindow(); // On OS X it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. @@ -37,6 +40,13 @@ app.whenReady().then(() => { createWindow(); } }); + + try { + initPty(mainWindow); + } + catch (ex) { + console.error(ex); + } }); // Quit when all windows are closed, except on macOS. There, it's common @@ -50,3 +60,20 @@ app.on('window-all-closed', () => { // In this file you can include the rest of your app's specific main process // code. You can also put them in separate files and import them here. +// + +function initPty(mainWindow) { + const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash'; + const ptyProcess = pty.spawn(shell, [], {}); + + ptyProcess.write('ucm\r'); + ptyProcess.write('clear\r'); + + ptyProcess.on("data", (data) => { + mainWindow.webContents.send("pty-output", data); + }); + + ipcMain.on("pty-input", (_evt, data) => { + ptyProcess.write(data); + }); +} diff --git a/src/preload.js b/src/preload.js index 5e9d369..d8d6e43 100644 --- a/src/preload.js +++ b/src/preload.js @@ -1,2 +1,13 @@ // See the Electron documentation for details on how to use preload scripts: // https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts +// +import { contextBridge, ipcRenderer } from 'electron'; + +contextBridge.exposeInMainWorld('electronAPI', { + ptyInput: (command) => ipcRenderer.send('pty-input', command), + onPtyOutput: (callback) => { + ipcRenderer.on('pty-output', (_event, value) => { + callback(value); + }); + }, +}) diff --git a/src/renderer.js b/src/renderer.js index c70961e..2b5ed5c 100644 --- a/src/renderer.js +++ b/src/renderer.js @@ -15,6 +15,9 @@ import "./main.css"; import * as AppError from "./Ucm/AppError"; import * as AppSettings from "./Ucm/AppSettings"; import * as Theme from "./Ucm/Theme"; +import "@xterm/xterm/css/xterm.css"; +import { Terminal } from "@xterm/xterm"; +import { FitAddon } from 'xterm-addon-fit'; // @ts-ignore import { Elm } from './Main.elm'; @@ -32,6 +35,21 @@ try { const appSettings = AppSettings.init(); const operatingSystem = detectOs(window.navigator); + // -- Terminal ----------------------------------------------------------- + const terminal = new Terminal({ + // fontFamily: "Fira Code var", + theme: { + background: "#18181c" //--color-gray-darken-30 + } + }); + terminal.onData(data => window.electronAPI.ptyInput(data)); + + window.electronAPI.onPtyOutput((output) => { + terminal.write(output); + }); + const fitAddon = new FitAddon(); + terminal.loadAddon(fitAddon); + // -- Elm ------------------------------------------------------------------- const flags = { operatingSystem: operatingSystem, @@ -60,6 +78,14 @@ try { AppSettings.clear(); window.location.reload(); }); + + app.ports.openTerminal?.subscribe((targetId) => { + const target = document.getElementById(targetId); + if (target) { + terminal.open(target); + fitAddon.fit(); + } + }); } // -- CSS env classes ------------------------------------------------------- diff --git a/webpack.main.config.js b/webpack.main.config.js index d23d0e3..c9ba57e 100644 --- a/webpack.main.config.js +++ b/webpack.main.config.js @@ -8,4 +8,5 @@ module.exports = { module: { rules: require('./webpack.rules'), }, + externals: ['node-pty'], };