SSH Terminal in Flutter
Part 3 of 4 — building Linxr, a single APK that runs Alpine Linux on non-rooted Android.
← Part 2: Shipping QEMU in an APK
The Goal
Once Alpine boots and sshd starts, the app needs a usable terminal — without requiring an external SSH client.
Two Flutter packages make this possible:
| Package | Role |
|---|---|
dartssh2 |
Pure-Dart SSH2 client — connects to the VM's sshd |
xterm |
Terminal emulator widget — renders VT100/xterm sequences |
Both are pure Dart. No platform channels, no native code, no extra Android permissions.
Connecting to the VM
final socket = await SSHSocket.connect('127.0.0.1', 2222)
.timeout(const Duration(seconds: 10));
_client = SSHClient(
socket,
username: 'root',
onPasswordRequest: () => 'alpine',
);
_session = await _client!.shell(
pty: SSHPtyConfig(
type: 'xterm-256color',
width: _terminal.viewWidth,
height: _terminal.viewHeight,
),
);
SSHPtyConfig allocates a pseudo-terminal with xterm-256color — enabling colour output, cursor movement, and correct TERM variable.
Wiring SSH to the Terminal
// VM output → terminal display
_session!.stdout.listen(
(data) => _terminal.write(String.fromCharCodes(data)),
);
// Keyboard input → SSH session
_terminal.onOutput = (data) {
_session?.stdin.add(Uint8List.fromList(data.codeUnits));
};
// Terminal resize → SSH PTY resize
_terminal.onResize = (w, h, pw, ph) {
_session?.resizeTerminal(w, h);
};
Resize events keep tools like vim, htop, and nano correctly sized.
Auto-Reconnect
Alpine takes ~15 seconds to boot. The terminal handles the window where sshd isn't ready yet:
static const _maxRetries = 24; // 24 × 5s = 2 minutes
void _retryOrError(String msg) {
_retryCount++;
if (_retryCount < _maxRetries) {
_terminal.write('\r\n[$msg — retrying in 5s...]\r\n');
_scheduleConnect(delaySeconds: 5);
} else {
_setError('Gave up after $_maxRetries attempts.');
}
}
Progress messages appear inline in the terminal buffer — no separate loading spinner.
Terminal Theme
theme: const TerminalTheme(
cursor: Color(0xFF20C997), // teal
foreground: Color(0xFFE0E0E0), // light grey
background: Color(0xFF0E1117), // near-black
green: Color(0xFF20C997),
blue: Color(0xFF0D6EFD),
yellow: Color(0xFFFFC107),
red: Color(0xFFDC3545),
)
The cursor teal (#20C997) matches the "Connected" status chip.
VM-Not-Running Banner
if (vmStatus != 'running')
_Banner(
icon: Icons.warning_amber,
color: Color(0xFFFFC107),
message: 'VM is not running. Start it from the Home tab.',
)
Prevents confusing "Connection refused" errors when the user opens the terminal before starting the VM.
Next: Part 4 — Test Results
GitHub: github.com/AI2TH/Linxr
Linxr Series — Alpine Linux on Android
Linxr = Linux + r. A single Android APK that runs a full Alpine Linux shell on any Android phone — no root, no Termux, no PC required.
| # | Post | Topic |
|---|---|---|
| 📖 | Intro | What is Linxr? Start here |
| 1 | Part 1 | The Idea and Architecture |
| 2 | Part 2 | Shipping QEMU in an APK |
| 3 | Part 3 | SSH Terminal in Flutter |
| 4 | Part 4 | Test Results |
GitHub: github.com/AI2TH/Linxr
Website: ai2th.github.io
Top comments (0)