226 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			226 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <div x-data="data()">
 | |
|     {{-- <div x-show="!terminalActive" class="flex items-center justify-center w-full py-4 mx-auto h-[510px]">
 | |
|         <div class="p-1 w-full h-full rounded border dark:bg-coolgray-100 dark:border-coolgray-300">
 | |
|             <span class="font-mono text-sm text-gray-500" x-text="message"></span>
 | |
|         </div>
 | |
|     </div> --}}
 | |
|     <div x-ref="terminalWrapper"
 | |
|         :class="fullscreen ? 'fullscreen' : 'relative w-full h-full py-4 mx-auto max-h-[510px]'">
 | |
|         <div id="terminal" wire:ignore></div>
 | |
|         <button title="Minimize" x-show="fullscreen" class="fixed top-4 right-4 text-white" x-on:click="makeFullscreen"><svg
 | |
|                 class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
 | |
|                 <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
 | |
|                     stroke-width="2" d="M6 14h4m0 0v4m0-4l-6 6m14-10h-4m0 0V6m0 4l6-6" />
 | |
|             </svg></button>
 | |
|         <button title="Fullscreen" x-show="!fullscreen && terminalActive" class="absolute right-4 top-6 text-white"
 | |
|             x-on:click="makeFullscreen"><svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
 | |
|                 <g fill="none">
 | |
|                     <path
 | |
|                         d="M24 0v24H0V0h24ZM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427c-.002-.01-.009-.017-.017-.018Zm.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093c.012.004.023 0 .029-.008l.004-.014l-.034-.614c-.003-.012-.01-.02-.02-.022Zm-.715.002a.023.023 0 0 0-.027.006l-.006.014l-.034.614c0 .012.007.02.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01l-.184-.092Z" />
 | |
|                     <path fill="currentColor"
 | |
|                         d="M9.793 12.793a1 1 0 0 1 1.497 1.32l-.083.094L6.414 19H9a1 1 0 0 1 .117 1.993L9 21H4a1 1 0 0 1-.993-.883L3 20v-5a1 1 0 0 1 1.993-.117L5 15v2.586l4.793-4.793ZM20 3a1 1 0 0 1 .993.883L21 4v5a1 1 0 0 1-1.993.117L19 9V6.414l-4.793 4.793a1 1 0 0 1-1.497-1.32l.083-.094L17.586 5H15a1 1 0 0 1-.117-1.993L15 3h5Z" />
 | |
|                 </g>
 | |
|             </svg></button>
 | |
|     </div>
 | |
| 
 | |
|     @script
 | |
|         <script>
 | |
|             const MAX_PENDING_WRITES = 5;
 | |
|             let pendingWrites = 0;
 | |
|             let paused = false;
 | |
| 
 | |
|             let socket;
 | |
|             let commandBuffer = '';
 | |
| 
 | |
| 
 | |
|             function keepAlive() {
 | |
|                 if (socket && socket.readyState === WebSocket.OPEN) {
 | |
|                     socket.send(JSON.stringify({
 | |
|                         ping: true
 | |
|                     }));
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             const keepAliveInterval = setInterval(keepAlive, 30000);
 | |
| 
 | |
|             // Clear the interval when the component is destroyed
 | |
|             document.addEventListener('livewire:navigating', () => {
 | |
|                 clearInterval(keepAliveInterval);
 | |
|             });
 | |
| 
 | |
|             function initializeWebSocket() {
 | |
|                 if (!socket || socket.readyState === WebSocket.CLOSED) {
 | |
|                     // Only use port if Coolify is used with ip (so it has a port in the url)
 | |
|                     let postPath = ':6002/terminal/ws';
 | |
|                     const port = window.location.port;
 | |
|                     if (!port) {
 | |
|                         postPath = '/terminal/ws';
 | |
|                     }
 | |
|                     let url = window.location.hostname;
 | |
|                     // make sure the port is not included
 | |
|                     url = url.split(':')[0];
 | |
|                     socket = new WebSocket((window.location.protocol === 'https:' ? 'wss://' : 'ws://') +
 | |
|                         url +
 | |
|                         postPath);
 | |
| 
 | |
|                     socket.onmessage = handleSocketMessage;
 | |
|                     socket.onerror = (e) => {
 | |
|                         console.error('WebSocket error:', e);
 | |
|                     };
 | |
|                     socket.onclose = () => {
 | |
|                         console.log('WebSocket connection closed');
 | |
| 
 | |
|                     };
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             function handleSocketMessage(event) {
 | |
|                 $data.message = '(connection closed)';
 | |
|                 // Initialize Terminal
 | |
|                 if (event.data === 'pty-ready') {
 | |
|                     term.open(document.getElementById('terminal'));
 | |
|                     $data.terminalActive = true;
 | |
|                     term.reset();
 | |
|                     term.focus();
 | |
|                     document.querySelector('.xterm-viewport').classList.add('scrollbar', 'rounded')
 | |
|                     $data.resizeTerminal()
 | |
|                 } else if (event.data === 'unprocessable') {
 | |
|                     term.reset();
 | |
|                     $data.terminalActive = false;
 | |
|                     $data.message = '(sorry, something went wrong, please try again)';
 | |
|                 } else {
 | |
|                     pendingWrites++;
 | |
|                     term.write(event.data, flowControlCallback);
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             function flowControlCallback() {
 | |
|                 pendingWrites--;
 | |
|                 if (pendingWrites > MAX_PENDING_WRITES && !paused) {
 | |
|                     paused = true;
 | |
|                     socket.send(JSON.stringify({
 | |
|                         pause: true
 | |
|                     }));
 | |
|                     return;
 | |
|                 }
 | |
|                 if (pendingWrites <= MAX_PENDING_WRITES && paused) {
 | |
|                     paused = false;
 | |
|                     socket.send(JSON.stringify({
 | |
|                         resume: true
 | |
|                     }));
 | |
|                     return;
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             term.onData((data) => {
 | |
|                 socket.send(JSON.stringify({
 | |
|                     message: data
 | |
|                 }));
 | |
|                 // Type CTRL + D or exit in the terminal
 | |
|                 if (data === '\x04' || (data === '\r' && stripAnsiCommands(commandBuffer).trim().includes('exit'))) {
 | |
|                     checkIfProcessIsRunningAndKillIt();
 | |
|                     setTimeout(() => {
 | |
|                         $data.terminalActive = false;
 | |
|                         term.reset();
 | |
|                     }, 500);
 | |
|                     commandBuffer = '';
 | |
|                 } else if (data === '\r') {
 | |
|                     commandBuffer = '';
 | |
|                 } else {
 | |
|                     commandBuffer += data;
 | |
|                 }
 | |
|             });
 | |
| 
 | |
|             function stripAnsiCommands(input) {
 | |
|                 return input.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
 | |
|             }
 | |
| 
 | |
|             // Copy and paste
 | |
|             // Enables ctrl + c and ctrl + v
 | |
|             // defaults otherwise to ctrl + insert, shift + insert
 | |
|             term.attachCustomKeyEventHandler((arg) => {
 | |
|                 if (arg.ctrlKey && arg.code === "KeyV" && arg.type === "keydown") {
 | |
|                     navigator.clipboard.readText()
 | |
|                         .then(text => {
 | |
|                             socket.send(JSON.stringify({
 | |
|                                 message: text
 | |
|                             }));
 | |
|                         })
 | |
|                 };
 | |
| 
 | |
|                 if (arg.ctrlKey && arg.code === "KeyC" && arg.type === "keydown") {
 | |
|                     const selection = term.getSelection();
 | |
|                     if (selection) {
 | |
|                         navigator.clipboard.writeText(selection);
 | |
|                         return false;
 | |
|                     }
 | |
|                 }
 | |
|                 return true;
 | |
|             });
 | |
| 
 | |
|             $wire.on('send-back-command', function(command) {
 | |
|                 socket.send(JSON.stringify({
 | |
|                     command: command
 | |
|                 }));
 | |
|             });
 | |
| 
 | |
|             window.addEventListener('beforeunload', function(e) {
 | |
|                 checkIfProcessIsRunningAndKillIt();
 | |
|             });
 | |
| 
 | |
|             function checkIfProcessIsRunningAndKillIt() {
 | |
|                 socket.send(JSON.stringify({
 | |
|                     checkActive: 'force'
 | |
|                 }));
 | |
|             }
 | |
| 
 | |
|             window.onresize = function() {
 | |
|                 $data.resizeTerminal()
 | |
|             };
 | |
| 
 | |
|             Alpine.data('data', () => ({
 | |
|                 fullscreen: false,
 | |
|                 terminalActive: false,
 | |
|                 message: '(connection closed)',
 | |
|                 init() {
 | |
|                     this.$watch('terminalActive', (value) => {
 | |
|                         this.$nextTick(() => {
 | |
|                             if (value) {
 | |
|                                 $refs.terminalWrapper.style.display = 'block';
 | |
|                                 this.resizeTerminal();
 | |
|                             } else {
 | |
|                                 $refs.terminalWrapper.style.display = 'none';
 | |
|                             }
 | |
|                         });
 | |
|                     });
 | |
|                 },
 | |
|                 makeFullscreen() {
 | |
|                     this.fullscreen = !this.fullscreen;
 | |
|                     $nextTick(() => {
 | |
|                         this.resizeTerminal()
 | |
|                     })
 | |
|                 },
 | |
| 
 | |
|                 resizeTerminal() {
 | |
|                     if (!this.terminalActive) return;
 | |
| 
 | |
|                     fitAddon.fit();
 | |
|                     const height = $refs.terminalWrapper.clientHeight;
 | |
|                     const rows = height / term._core._renderService._charSizeService.height - 1;
 | |
|                     var termWidth = term.cols;
 | |
|                     var termHeight = parseInt(rows.toString(), 10);
 | |
|                     term.resize(termWidth, termHeight);
 | |
|                     socket.send(JSON.stringify({
 | |
|                         resize: {
 | |
|                             cols: termWidth,
 | |
|                             rows: termHeight
 | |
|                         }
 | |
|                     }));
 | |
|                 }
 | |
|             }));
 | |
| 
 | |
|             initializeWebSocket();
 | |
|         </script>
 | |
|     @endscript
 | |
| </div>
 | 
