New Sep 19, 2024

How to enable vim, vi, and nano to work in Xterm.js?

Libraries, Frameworks, etc. All from Newest questions tagged reactjs - Stack Overflow View How to enable vim, vi, and nano to work in Xterm.js? on stackoverflow.com

I am developing a web-based terminal in a Next.js application. The frontend communicates with an API (/api/socket) that opens a WebSocket session with a Go application. The Go application establishes a WebSocket connection to a Kubernetes Pod using kubectl exec. While basic commands like ping, ls, and cd work fine, commands such as vi, vim, and nano are not functioning as expected in Xterm.js. How can I make these commands work properly?

The Terminal component supports navigating through previously executed commands using the up/down arrow keys. It also handles Ctrl+C or Ctrl+Z to exit commands like ping. Additionally, the component uses the FitAddon to enable copy and paste functionality.

Here is my terminal component

import React, { useEffect, useRef, useState } from 'react';
import '../../../../node_modules/@xterm/xterm/css/xterm.css';
import io from 'socket.io-client';

const Terminal = ({ accountId, data, requestId }) => { const terminalRef = useRef(null); const xtermRef = useRef(null); const socketRef = useRef(null); const commandHistoryRef = useRef([]); const isConnectedRef = useRef(null); const [currentInput, setCurrentInput] = useState(''); let promptText = '';

const fetchInitialData = async () => { try { const response = await fetch('/api/socket'); if (!response.ok) { throw new Error('Network response was not ok'); } initializeWebSocket(); } catch (error) { console.error('Fetch error:', error); } };

const initializeWebSocket = () => { socketRef.current = io();

socketRef.current.on('connect', () => { console.log('Connected to Socket.IO server'); xtermRef.current.writeln('Initializing...'); socketRef.current.emit('checkConnection'); });

socketRef.current.on('connectionReady', () => { try { xtermRef.current.writeln('Connecting...'); const payload = JSON.stringify({ no_sinks: true, body: { account_id: accountId, action_name: 'interactive_session', action_params: { name: data.name, namespace: data.namespace, }, origin: 'Nudgebee UI', }, request_id: requestId, }); socketRef.current.emit('initialCommand', payload); if (xtermRef.current) { xtermRef.current.clear(); } } catch (error) { console.error('Error in emit:', error); xtermRef.current.writeln('Failed to send message'); } });

socketRef.current.on('connect_error', (error) => { console.error('Connection error:', error); xtermRef.current.writeln('Failed to connect to pod'); });

socketRef.current.on('commandOutput', (data) => { if (xtermRef.current) { let folder = ''; isConnectedRef.current = true; const parsedJson = JSON.parse(data); const infoStream = parsedJson?.data?.info_stream ?? ''; const errorStream = parsedJson?.data?.error_stream ?? ''; if (infoStream) { const lines = infoStream.split('\n'); folder = lines[lines.length - 1]; lines.slice(0, -1).forEach((line) => xtermRef.current.writeln(line)); } else if (errorStream) { errorStream.split('\n').forEach((line) => xtermRef.current.writeln(line)); } writeNewPrompt(folder); } }); };

const writeNewPrompt = (folder) => { promptText = folder || ''; xtermRef.current.write(folder || ``); };

useEffect(() => { fetchInitialData(); }, []);

useEffect(() => { if (terminalRef.current) { const { Terminal } = require('@xterm/xterm'); const { FitAddon } = require('@xterm/addon-fit'); xtermRef.current = new Terminal({ cursorBlink: true, allowTransparency: true, cursorInactiveStyle: 'bar', });

const fitAddon = new FitAddon(); xtermRef.current.loadAddon(fitAddon); xtermRef.current.open(terminalRef.current); setTimeout(() => { fitAddon.fit(); }, 0); // Small delay

let currentLine = ''; let localHistoryIndex = -1;

xtermRef.current.attachCustomKeyEventHandler((event) => { if (event.ctrlKey && event.key === 'c' && xtermRef.current.hasSelection()) { const selection = xtermRef.current.getSelection(); navigator.clipboard.writeText(selection); return false; } return true; });

xtermRef.current.attachCustomKeyEventHandler((event) => { if (event.ctrlKey && event.key === 'v') { navigator.clipboard.readText().then((text) => { xtermRef.current.write(text); currentLine += text; }); return false; } return true; });

xtermRef.current.onKey(({ key, domEvent }) => { const printable = !domEvent.altKey && !domEvent.ctrlKey && !domEvent.metaKey;

if (!isConnectedRef.current) { return; } const forbiddenCommands = ['vim', 'vi', 'nano']; if (domEvent.keyCode === 13) { xtermRef.current.writeln(''); if (currentLine.trim().length > 0) { const command = currentLine.trim(); if (forbiddenCommands.some((forbidden) => command.startsWith(forbidden))) { xtermRef.current.writeln(Currently Command &quot;${command}&quot; is not supported.); xtermRef.current.writeln(''); currentLine = ''; return; } commandHistoryRef.current = [...commandHistoryRef.current, command]; setCurrentInput(''); localHistoryIndex = -1; currentLine = ''; if (socketRef.current) { socketRef.current.emit( 'sendCommand', JSON.stringify({ no_sinks: true, body: { account_id: accountId, action_name: 'interactive_session', action_params: { name: data.name, namespace: data.namespace, command: command, }, origin: 'Nudgebee UI', }, request_id: requestId, }) ); } } else { xtermRef.current.write(\x1b[2K\r${promptText}); } } else if (domEvent.keyCode === 8) { if (currentLine.length > 0) { currentLine = currentLine.slice(0, -1); xtermRef.current.write('\b \b'); } } else if (domEvent.keyCode === 38) { if (localHistoryIndex < commandHistoryRef.current.length - 1) { if (localHistoryIndex === -1) { setCurrentInput(currentLine); } localHistoryIndex++; const newCommand = commandHistoryRef.current[commandHistoryRef.current.length - 1 - localHistoryIndex]; xtermRef.current.write(\x1b[2K\r${promptText}${newCommand}); currentLine = newCommand; } } else if (domEvent.keyCode === 40) { if (localHistoryIndex > -1) { localHistoryIndex--; let newCommand; if (localHistoryIndex === -1) { newCommand = currentInput; } else { newCommand = commandHistoryRef.current[commandHistoryRef.current.length - 1 - localHistoryIndex]; } xtermRef.current.write(\x1b[2K\r${promptText}${newCommand}); currentLine = newCommand; } } else if (printable) { currentLine += key; xtermRef.current.write(key); } else if (domEvent.ctrlKey && (domEvent.key === 'c' || domEvent.key === 'z')) { xtermRef.current.writeln('^C'); currentLine = ''; if (socketRef.current) { socketRef.current.emit( 'sendCommand', JSON.stringify({ no_sinks: true, body: { account_id: accountId, action_name: 'interactive_session', action_params: { name: data.name, namespace: data.namespace, command: '\x03', }, origin: 'Nudgebee UI', }, request_id: requestId, }) ); } } });

const handleResize = () => { fitAddon.fit(); };

window.addEventListener('resize', handleResize);

return () => { window.removeEventListener('resize', handleResize); if (socketRef.current) { console.log('Disconnecting socket'); const payload = JSON.stringify({ no_sinks: true, body: { account_id: accountId, action_name: 'interactive_session', action_params: { name: data.name, namespace: data.namespace, command: 'exit', }, origin: 'Nudgebee UI', }, request_id: requestId, }); socketRef.current.emit('sendCommand', payload); socketRef.current.disconnect(); }

if (xtermRef.current) { xtermRef.current.dispose(); } }; } }, []);

return <div className='terminal' ref={terminalRef} style={{ width: '100%', height: '400px', marginLeft: '30px', marginTop: '20px' }} />; };

export default Terminal;

Here is my socket.js API

import { Server } from 'socket.io';
import WebSocket from 'ws';

const SocketHandler = (req, res) => { const goWSEndpoint = 'ws://localhost:52832/ws'; const secretKey = '';

if (res.socket.server.io) { console.log('Socket is already running'); res.end(); return; }

console.log('Socket is initializing'); const io = new Server(res.socket.server, { pingTimeout: 60000, pingInterval: 25000, }); res.socket.server.io = io;

io.on('connection', (socket) => { console.log(New client connected: ${socket.id}); const ws = new WebSocket(goWSEndpoint, { headers: { 'X-SECRET-KEY': secretKey, }, });

ws.on('open', () => { socket.emit('connectionReady', JSON.stringify({ connection: 'ready' })); socket.on('initialCommand', (jsonMessage) => { try { console.log('Received JSON message from client for initial connection:', jsonMessage); ws.send(jsonMessage); } catch (error) { console.error('Error in initialCommand:', error); } }); socket.on('sendCommand', (jsonMessage) => { try { console.log('Received JSON message from client:', jsonMessage); ws.send(jsonMessage); } catch (error) { console.error('Error in sendCommand:', error); } }); });

ws.on('message', (data) => { console.log(Received message from Go WebSocket server: ${data}); socket.emit('commandOutput', data.toString('utf-8')); });

ws.on('error', (error) => { console.error('Go WebSocket error:', error); });

ws.on('close', () => { console.log('Go Connection closed'); socket.emit( 'commandOutput', JSON.stringify({ data: { info_stream: 'Connection closed', }, }) ); });

socket.on('disconnect', (reason) => { console.log(Client disconnected: ${socket.id}, reason: ${reason}); console.log(Clearing interval for client ${socket.id}); });

socket.on('error', (error) => { console.error(Error for client ${socket.id}:, error); }); });

io.on('connect_error', (err) => { console.error('Connection error:', err); });

console.log('Socket.IO server initialized'); res.end(); };

export const config = { api: { bodyParser: false, }, };

export default SocketHandler;

The Go WebSocket successfully returns the response for the ls -al command, providing the detailed directory listing.

{"action": "response", "request_id": "6712fc75-b252-45e9-9e3d-7fc57f56e4fa", "status_code": 200, "data": {"info_stream": "ls -al\r\ntotal 16\r\ndrwxr-xr-x    1 root     root            19 Sep 19 03:03 \u001b[1;34m.\u001b[m\r\ndrwxr-xr-x    1 root     root            40 Sep 19 03:20 \u001b[1;34m..\u001b[m", "error_stream": null, "session_id": "6712fc75-b252-45e9-9e3d-7fc57f56e4fa", "exit": false}, "output_type": "interactive_session"}

{"action": "response", "request_id": "6712fc75-b252-45e9-9e3d-7fc57f56e4fa", "status_code": 200, "data": {"info_stream": "\r\ndrwxr-xr-x 1 nextjs nodejs 20 Sep 19 03:03 \u001b[1;34m.next\u001b[m\r\ndrwxr-xr-x 83 nextjs nodejs 4096 Sep 19 03:03 \u001b[1;34mnode_modules\u001b[m\r\n-rw-r--r-- 1 nextjs nodejs 3572 Sep 19 03:03 \u001b[0;0mpackage.json\u001b[m\r\ndrwxr-xr-x 4 root root 198 Sep 19 03:03 \u001b[1;34mpublic\u001b[m\r\n-rw-r--r-- 1 nextjs nodejs 4543 Sep 19 03:03 \u001b[0;0mserver.js\u001b[m\r\ndrwxr-xr-x 3 nextjs nodejs 19 Sep 19 03:03 \u001b[1;34msrc\u001b[m\r\n", "error_stream": null, "session_id": "6712fc75-b252-45e9-9e3d-7fc57f56e4fa", "exit": false}, "output_type": "interactive_session"}

Scroll to top