diff --git a/package.json b/package.json index e4cbee185..1ad34db66 100644 --- a/package.json +++ b/package.json @@ -17,5 +17,13 @@ "prettier": "^3.4.2", "tailwindcss": "^3.4.13", "vite": "^6.0" + }, + "dependencies": { + "@xterm/addon-fit": "^0.10.0", + "@xterm/addon-search": "^0.15.0", + "@xterm/addon-web-links": "^0.11.0", + "xterm": "^5.3.0", + "xterm-addon-search": "^0.13.0", + "xterm-addon-search-bar": "^0.2.0" } } diff --git a/resources/css/app.css b/resources/css/app.css index 197ad6727..05301c1ff 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -1,8 +1,12 @@ +@import 'xterm/css/xterm.css'; +@import 'xterm-addon-search-bar/src/index.css'; + @tailwind base; @tailwind components; @tailwind utilities; @tailwind variants; + .fi-section-header-icon { @apply !self-center; } diff --git a/resources/js/app.js b/resources/js/app.js index e59d6a0ad..5aa7cf8bd 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1 +1,61 @@ import './bootstrap'; +import { initializeTerminal } from './terminal'; // Import the terminal initialization function + +document.addEventListener('DOMContentLoaded', () => { + // Access the PHP variables from window.phpData + const { userFont, userFontSize, userRows, socketUrl } = window.phpData; + + const terminalOptions = { + fontSize: userFontSize, + fontFamily: `${userFont}, monospace`, + lineHeight: 1.2, + disableStdin: true, + cursorStyle: 'underline', + cursorInactiveStyle: 'underline', + allowTransparency: true, + rows: userRows, + theme: { + background: 'rgba(19,26,32,0.7)', + cursor: 'transparent', + black: '#000000', + red: '#E54B4B', + green: '#9ECE58', + yellow: '#FAED70', + blue: '#396FE2', + magenta: '#BB80B3', + cyan: '#2DDAFD', + white: '#d0d0d0', + brightBlack: 'rgba(255, 255, 255, 0.2)', + brightRed: '#FF5370', + brightGreen: '#C3E88D', + brightYellow: '#FFCB6B', + brightBlue: '#82AAFF', + brightMagenta: '#C792EA', + brightCyan: '#89DDFF', + brightWhite: '#ffffff', + selection: '#FAF089' + } + }; + + // Initialize the terminal with the options and WebSocket URL + const { terminal, fitAddon, searchAddon, searchBarAddon, socket } = initializeTerminal({ + elId: 'terminal', + options: terminalOptions, + socketUrl: socketUrl, + onEvent: (event, args) => { + switch (event) { + case 'status': + console.log('Status event received:', args); + break; + case 'transfer status': + terminal.writeln(TERMINAL_PRELUDE + 'Transfer status: ' + args); + break; + case 'daemon error': + terminal.writeln(TERMINAL_PRELUDE + 'Daemon error: ' + args); + break; + default: + break; + } + } + }); +}); diff --git a/resources/js/terminal.js b/resources/js/terminal.js new file mode 100644 index 000000000..3b0854b00 --- /dev/null +++ b/resources/js/terminal.js @@ -0,0 +1,107 @@ +import { Terminal } from 'xterm'; +import { FitAddon } from '@xterm/addon-fit'; +import { WebLinksAddon } from '@xterm/addon-web-links'; +import { SearchAddon } from '@xterm/addon-search'; +import { SearchBarAddon } from 'xterm-addon-search-bar'; + +export function initializeTerminal({ elId = 'terminal', options = {}, socketUrl, onEvent = () => {} }) { + const terminal = new Terminal(options); + + const fitAddon = new FitAddon(); + const webLinksAddon = new WebLinksAddon(); + const searchAddon = new SearchAddon(); + const searchBarAddon = new SearchBarAddon({ searchAddon }); + + terminal.loadAddon(fitAddon); + terminal.loadAddon(webLinksAddon); + terminal.loadAddon(searchAddon); + terminal.loadAddon(searchBarAddon); + + terminal.open(document.getElementById(elId)); + fitAddon.fit(); + + window.addEventListener('resize', () => fitAddon.fit()); + + terminal.attachCustomKeyEventHandler((event) => { + if ((event.ctrlKey || event.metaKey) && event.key === 'c') { + document.execCommand('copy'); + return false; + } else if ((event.ctrlKey || event.metaKey) && event.key === 'f') { + event.preventDefault(); + searchBarAddon.show(); + return false; + } else if (event.key === 'Escape') { + searchBarAddon.hidden(); + } + return true; + }); + + // WebSocket connection handling + const socket = new WebSocket(socketUrl); + + socket.onerror = (event) => { + $wire.dispatchSelf('websocket-error'); + }; + + socket.onmessage = function(websocketMessageEvent) { + let { event, args } = JSON.parse(websocketMessageEvent.data); + + switch (event) { + case 'console output': + case 'install output': + terminal.writeln(args[0]); + break; + case 'feature match': + Livewire.dispatch('mount-feature', { data: args[0] }); + break; + case 'status': + handlePowerChangeEvent(args[0]); + $wire.dispatch('console-status', { state: args[0] }); + break; + case 'transfer status': + terminal.writeln(TERMINAL_PRELUDE + 'Transfer has failed.\u001b[0m'); + break; + case 'daemon error': + handleDaemonErrorOutput(args[0]); + break; + case 'stats': + $wire.dispatchSelf('store-stats', { data: args[0] }); + break; + case 'auth success': + socket.send(JSON.stringify({ + 'event': 'send logs', + 'args': [null] + })); + break; + case 'token expiring': + case 'token expired': + $wire.dispatchSelf('token-request'); + break; + } + }; + + socket.onopen = () => { + $wire.dispatchSelf('token-request'); + }; + + // Handle events + const TERMINAL_PRELUDE = '\u001b[1m\u001b[33mpelican@' + '{{ \Filament\Facades\Filament::getTenant()->name }}' + ' ~ \u001b[0m'; + + const handleConsoleOutput = (line, prelude = false) => + terminal.writeln((prelude ? TERMINAL_PRELUDE : '') + line.replace(/(?:\r\n|\r|\n)$/im, '') + '\u001b[0m'); + + const handleDaemonErrorOutput = (line) => + terminal.writeln(TERMINAL_PRELUDE + '\u001b[1m\u001b[41m' + line.replace(/(?:\r\n|\r|\n)$/im, '') + '\u001b[0m'); + + const handlePowerChangeEvent = (state) => + terminal.writeln(TERMINAL_PRELUDE + 'Server marked as ' + state + '...\u001b[0m'); + + // Return terminal, fitAddon, searchAddon, searchBarAddon for further usage + return { + terminal, + fitAddon, + searchAddon, + searchBarAddon, + socket + }; +} diff --git a/resources/views/filament/components/server-console.blade.php b/resources/views/filament/components/server-console.blade.php index 366f814c7..0edc58985 100644 --- a/resources/views/filament/components/server-console.blade.php +++ b/resources/views/filament/components/server-console.blade.php @@ -4,23 +4,18 @@ $userFont = auth()->user()->getCustomization()['console_font'] ?? 'monospace'; $userFontSize = auth()->user()->getCustomization()['console_font_size'] ?? 14; $userRows = auth()->user()->getCustomization()['console_rows'] ?? 30; + $socketUrl = $this->getSocket(); // Assuming this is the WebSocket URL @endphp - @if($userFont) - - - @endif - - - - - - - + + @vite(['resources/js/app.js', 'resources/css/app.css']) + @endassets
@@ -45,163 +40,4 @@ > @endif - - @script - - @endscript diff --git a/yarn.lock b/yarn.lock index 46c463bac..b0bb7bcdd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -329,6 +329,21 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== +"@xterm/addon-fit@^0.10.0": + version "0.10.0" + resolved "https://registry.yarnpkg.com/@xterm/addon-fit/-/addon-fit-0.10.0.tgz#bebf87fadd74e3af30fdcdeef47030e2592c6f55" + integrity sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ== + +"@xterm/addon-search@^0.15.0": + version "0.15.0" + resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.15.0.tgz#5c772d5f14c26546c4bfbeb0c3d4b3333057411f" + integrity sha512-ZBZKLQ+EuKE83CqCmSSz5y1tx+aNOCUaA7dm6emgOX+8J9H1FWXZyrKfzjwzV+V14TV3xToz1goIeRhXBS5qjg== + +"@xterm/addon-web-links@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@xterm/addon-web-links/-/addon-web-links-0.11.0.tgz#f283513b8c713757bad8e3bf04b6becc3b4e585f" + integrity sha512-nIHQ38pQI+a5kXnRaTgwqSHnX7KE6+4SVoceompgHL26unAxdfP6IPqUTSYPQgSwM56hsElfoNrrW5V7BUED/Q== + ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" @@ -395,6 +410,14 @@ axios@^1.7.4: form-data "^4.0.0" proxy-from-env "^1.1.0" +babel-runtime@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" + integrity sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g== + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.11.0" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -508,6 +531,11 @@ concurrently@^9.0.1: tree-kill "^1.2.2" yargs "^17.7.2" +core-js@^2.4.0: + version "2.6.12" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" + integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== + cross-spawn@^7.0.0: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" @@ -1027,6 +1055,11 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +regenerator-runtime@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" + integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -1081,6 +1114,11 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +rxjs-compat@^6.5.4: + version "6.6.7" + resolved "https://registry.yarnpkg.com/rxjs-compat/-/rxjs-compat-6.6.7.tgz#6eb4ef75c0a58ea672854a701ccc8d49f41e69cb" + integrity sha512-szN4fK+TqBPOFBcBcsR0g2cmTTUF/vaFEOZNuSdfU8/pGFnNmmn2u8SystYXG1QMrjOPBc6XTKHMVfENDf6hHw== + rxjs@^7.8.1: version "7.8.1" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" @@ -1325,6 +1363,24 @@ wrap-ansi@^8.1.0: string-width "^5.0.1" strip-ansi "^7.0.1" +xterm-addon-search-bar@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/xterm-addon-search-bar/-/xterm-addon-search-bar-0.2.0.tgz#e03c020a5ed22f1e8d503946b26a14ade508bc91" + integrity sha512-xvXmBA/ShbnzGe5CCy0kqPNNGqjkpuaRgH3Z1iW0V71vCAPRrtJ/v/hMnysZBH7WGUYhlCQr1cJZagW2fBVvSg== + dependencies: + babel-runtime "^6.26.0" + rxjs-compat "^6.5.4" + +xterm-addon-search@^0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.13.0.tgz#21286f4db48aa949fbefce34bb8bc0c9d3cec627" + integrity sha512-sDUwG4CnqxUjSEFh676DlS3gsh3XYCzAvBPSvJ5OPgF3MRL3iHLPfsb06doRicLC2xXNpeG2cWk8x1qpESWJMA== + +xterm@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.3.0.tgz#867daf9cc826f3d45b5377320aabd996cb0fce46" + integrity sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg== + y18n@^5.0.5: version "5.0.8" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"