React
React hooks for connection management and an xterm.js terminal component. The fastest way to build a terminal UI.
react/src/useWalkieTalkie.tsInstall
terminal
npm install @walkie-talkie/react @xterm/xterm @xterm/addon-fitPeer dependencies: This package requires
react >= 18. The @xterm/xterm and @xterm/addon-fit packages are optional peer dependencies — only needed if you use the TerminalView component.Quick Start
App.tsx
import { useWalkieTalkie } from '@walkie-talkie/react';
import { TerminalView } from '@walkie-talkie/react';
import '@xterm/xterm/css/xterm.css';
export default function App() {
const wt = useWalkieTalkie();
if (wt.connectionState !== 'connected') {
return (
<button onClick={() => wt.connect('http://localhost:3456', 'your-token')}>
Connect
</button>
);
}
return (
<div style={{ height: '100vh' }}>
<button onClick={() => wt.createTerminal(80, 24)}>
New Terminal
</button>
{wt.terminals.map((term) => (
<TerminalView
key={term.id}
terminalId={term.id}
isActive={true}
onInput={(data) => wt.sendInput(term.id, data)}
onResize={(cols, rows) => wt.resizeTerminal(term.id, cols, rows)}
registerOutput={(handler) => wt.registerOutputHandler(term.id, handler)}
/>
))}
</div>
);
}useWalkieTalkie
react/src/useWalkieTalkie.tsThe main hook. Manages the WebSocket connection, terminal list, and output buffering. Call it once at the top of your app.
const {
// State
connectionState, // ConnectionState
terminals, // TerminalInfo[]
isResuming, // boolean
// Connection
connect, // (serverUrl: string, token: string) => void
resumeSession, // (serverUrl: string, sessionId: string) => void
disconnect, // () => void
// Terminal management
createTerminal, // (cols: number, rows: number) => void
sendInput, // (terminalId: string, data: string) => void
resizeTerminal, // (terminalId: string, cols: number, rows: number) => void
killTerminal, // (terminalId: string) => void
listTerminals, // () => void
// Output
registerOutputHandler, // (terminalId: string, handler: (data: string) => void) => unsubscribe
} = useWalkieTalkie();Connection
connecting.ts
// Fresh connection with a token
wt.connect('http://192.168.1.50:3456', 'abcd-ef01-2345-6789');
// Resume a previous session (no token needed)
wt.resumeSession('http://192.168.1.50:3456', 'session-uuid');
// Check state
if (wt.connectionState === 'connected') {
// Ready to create terminals
}
// Disconnect (clears saved session)
wt.disconnect();Terminal Lifecycle
terminals.ts
// Create a terminal (80 cols x 24 rows)
wt.createTerminal(80, 24);
// The terminals array updates reactively
wt.terminals.forEach((term) => {
console.log(term.id, term.shell, term.pid);
});
// Send input (keystrokes, commands)
wt.sendInput(terminalId, 'echo hello\n');
// Resize when the container changes
wt.resizeTerminal(terminalId, 120, 40);
// Kill a terminal
wt.killTerminal(terminalId);Output Handling
output.ts
// Register a handler — receives live output AND replays the buffer
const unsubscribe = wt.registerOutputHandler(terminalId, (data) => {
// data is a string of terminal output
// Write it to an xterm.js instance, a textarea, etc.
terminal.write(data);
});
// When done, unsubscribe
unsubscribe();Output buffering: The hook buffers up to 100KB of output per terminal. When you call
registerOutputHandler, the handler immediately receives the full buffer — so terminals restore their scrollback on re-mount.TerminalView
react/src/TerminalView.tsxA React component that renders an xterm.js terminal. Handles fit-to-container, resize events, and input/output wiring.
usage.tsx
import { TerminalView } from '@walkie-talkie/react';
<TerminalView
terminalId={term.id}
isActive={activeTab === term.id}
onInput={(data) => wt.sendInput(term.id, data)}
onResize={(cols, rows) => wt.resizeTerminal(term.id, cols, rows)}
registerOutput={(handler) => wt.registerOutputHandler(term.id, handler)}
fontSize={14}
cursorBlink={true}
theme={{
background: '#0d1117',
foreground: '#e6edf3',
cursor: '#00d4aa',
}}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
terminalId | string | - | Unique terminal ID from the server |
isActive | boolean | - | Whether this terminal is visible. Hidden terminals use display: none. |
onInput | (data: string) => void | - | Called when the user types |
onResize | (cols, rows) => void | - | Called when the terminal resizes |
registerOutput | (handler) => unsubscribe | - | Register an output handler. Must return unsubscribe function. |
fontSize | number | 14 | Font size in pixels |
fontFamily | string | SF Mono, Fira Code... | CSS font-family string |
lineHeight | number | 1.2 | Line height multiplier |
cursorBlink | boolean | true | Whether the cursor blinks |
theme | TerminalTheme | Dark theme | Full xterm.js theme object |
Default Theme
theme.ts
import { defaultTheme } from '@walkie-talkie/react';
// {
// background: '#0d1117',
// foreground: '#e6edf3',
// cursor: '#00d4aa',
// cursorAccent: '#0d1117',
// selectionBackground: '#264f78',
// black: '#484f58',
// red: '#ff7b72',
// green: '#3fb950',
// yellow: '#d29922',
// blue: '#58a6ff',
// magenta: '#bc8cff',
// cyan: '#39c5cf',
// white: '#b1bac4',
// brightBlack: '#6e7681',
// brightRed: '#ffa198',
// brightGreen: '#56d364',
// brightYellow: '#e3b341',
// brightBlue: '#79c0ff',
// brightMagenta: '#d2a8ff',
// brightCyan: '#56d4dd',
// brightWhite: '#f0f6fc',
// }Full Example: Tabbed Terminal App
TabbedTerminals.tsx
import { useState } from 'react';
import { useWalkieTalkie, TerminalView } from '@walkie-talkie/react';
import '@xterm/xterm/css/xterm.css';
export function TabbedTerminals({ serverUrl, token }: {
serverUrl: string;
token: string;
}) {
const wt = useWalkieTalkie();
const [activeId, setActiveId] = useState<string | null>(null);
// Auto-connect on mount
if (wt.connectionState === 'disconnected') {
wt.connect(serverUrl, token);
}
if (wt.connectionState !== 'connected') {
return <div>Connecting...</div>;
}
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
{/* Tab bar */}
<div style={{ display: 'flex', gap: 4, padding: 8 }}>
{wt.terminals.map((t) => (
<button
key={t.id}
onClick={() => setActiveId(t.id)}
style={{ fontWeight: activeId === t.id ? 'bold' : 'normal' }}
>
{t.shell} #{t.pid}
</button>
))}
<button onClick={() => wt.createTerminal(80, 24)}>+ New</button>
</div>
{/* Terminal area */}
<div style={{ flex: 1 }}>
{wt.terminals.map((t) => (
<TerminalView
key={t.id}
terminalId={t.id}
isActive={activeId === t.id}
onInput={(data) => wt.sendInput(t.id, data)}
onResize={(cols, rows) => wt.resizeTerminal(t.id, cols, rows)}
registerOutput={(handler) =>
wt.registerOutputHandler(t.id, handler)
}
/>
))}
</div>
</div>
);
}