Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save possibilities/12df5a26f4d26e23ae13c53a829b3363 to your computer and use it in GitHub Desktop.

Select an option

Save possibilities/12df5a26f4d26e23ae13c53a829b3363 to your computer and use it in GitHub Desktop.
Embedding a Terminal in an Expo App

Embedding a Terminal in an Expo App

Date: 2026-02-28

Summary

You can embed a terminal UI in an Expo app, but it's emulated — iOS/Android sandboxing prevents spawning local shell processes. For a real shell, you need a server-side backend (tmux over WebSocket is a great fit). Three approaches exist, from simplest to most capable.

Approaches

1. Expo DOM Component + xterm.js (recommended)

Use "use dom" to embed xterm.js directly — runs as a webview on native, as-is on web. Zero extra native dependencies.

// components/Terminal.tsx
"use dom";

import { Terminal } from "@xterm/xterm";
import "@xterm/xterm/css/xterm.css";
import { useEffect, useRef } from "react";

export default function TerminalView({ onData }: { onData?: (data: string) => void }) {
  const ref = useRef<HTMLDivElement>(null);
  useEffect(() => {
    const term = new Terminal();
    term.open(ref.current!);
    term.onData((data) => onData?.(data));
    return () => term.dispose();
  }, []);
  return <div ref={ref} style={{ width: "100%", height: "100%" }} />;
}

2. xterm.js in a WebView (dedicated library)

@fressh/react-native-xtermjs-webview wraps xterm.js inside react-native-webview. Works in Expo managed workflow.

import XTermWebView from '@fressh/react-native-xtermjs-webview';

<XTermWebView
  onInput={(data) => { /* send to backend via websocket */ }}
  style={{ flex: 1 }}
/>

3. Pure JS terminal emulator (no real shell)

react-native-terminal-component — fully JS, in-memory filesystem, simulated unix commands. Good for toy terminals, game UI, or in-app command palettes. Last updated 2019.

Backend: tmux over WebSocket

For a real shell experience, connect the xterm.js frontend to a tmux session on a server.

[xterm.js in app] <--WebSocket--> [server] <--PTY--> [tmux attach]

Easiest: ttyd

ttyd is a single binary that exposes a terminal over WebSocket+HTTP:

ttyd -W tmux new -A -s mobile

Point xterm.js WebSocket at wss://yourserver:7681/ws. -W enables write, -A attaches or creates.

DIY: node-pty + ws

import { WebSocketServer } from "ws";
import * as pty from "node-pty";

const wss = new WebSocketServer({ port: 8080 });

wss.on("connection", (ws) => {
  const proc = pty.spawn("tmux", ["new", "-A", "-s", "mobile"], {
    cols: 80, rows: 24,
  });
  proc.onData((data) => ws.send(data));
  ws.on("message", (msg) => proc.write(msg.toString()));
  ws.on("close", () => proc.kill());
});

Why tmux is a good fit

  • Session persistence — disconnect, reconnect later, state preserved
  • Resize handling — send tmux resize-window when the terminal view resizes
  • Multiple clients — attach from app and desktop simultaneously
  • Scriptingtmux send-keys lets the server inject commands programmatically

Key Constraint

No local shell on device. iOS and Android sandbox prevents exec/spawn. Options for "real" terminal:

  • Remote shell: WebSocket to server running node-pty/ttyd
  • Local WASM shell: WebContainers (web only) or wasm-based shell
  • Command emulation: Custom JS command handlers
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment