DEV Community

Ai2th
Ai2th

Posted on

Linxr | Part 3 — SSH Terminal in Flutter

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,
    ),
);
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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.');
    }
}
Enter fullscreen mode Exit fullscreen mode

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),
)
Enter fullscreen mode Exit fullscreen mode

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.',
    )
Enter fullscreen mode Exit fullscreen mode

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)