A real xterm-compatible terminal emulator for the R36S (ArkOS / AeUX) built with Python + SDL2, designed for handheld use with an on-screen keyboard and full gamepad controls.
A single shell runs on a pseudo-terminal for the whole session, exactly like xterm. Output is interpreted by a VT100/xterm emulator into a character grid, and input is encoded into proper xterm escape sequences. In practice that means:
- Shell state persists.
cd,export,source, venv and conda activation all behave normally, because there is one long-lived shell rather than a new subprocess per command. - Full-screen programs work.
vim,nano,htop,less,top,sshand anything else curses-based runs natively, including the alternate screen. - Real geometry.
stty sizeandtput colsreport the true grid size, and resizing deliversSIGWINCH. - 256-color and truecolor ANSI output, plus bold, underline, and reverse video.
- Job control. Ctrl+C interrupts the foreground job as it should.
| Module | Responsibility |
|---|---|
Terminal/r36term_app.py |
SDL2 app: event loop, layout, settings overlay |
Terminal/r36term/pty_session.py |
Raw pty transport (TIOCSWINSZ, TIOCSCTTY, SIGWINCH) |
Terminal/r36term/emulator.py |
pyte screen + scrollback + cursor-key mode tracking |
Terminal/r36term/renderer.py |
Glyph-atlas grid renderer with color modulation |
Terminal/r36term/keys.py |
Key → xterm byte sequences (single input chokepoint) |
Terminal/r36term/osk.py |
On-screen gamepad keyboard layouts |
Terminal/r36term/hints.py |
Controller hints — drives the bar and the help page |
Terminal/r36term/palette.py |
ANSI 16/256/truecolor resolution |
Terminal/r36term/config.py |
Config and theme loading |
Important: The first time you install dependencies, the installer uses the internet to download required packages (APT + pip), and creates a Python virtual environment in the real user’s home directory.
-
Download
R36Term.zipfrom the latest release. -
Extract it into
/roms/tools/on the console SD card, so you end up with:/roms/tools/R36Term/(containingTerminal-Installer.sh,Run-Terminal.sh, and theTerminal/app)
-
On the R36S, open Tools and run:
Terminal-Installer.sh
-
In the menu, choose:
Install dependencies
What the installer does (automatically):
- Runs
apt-get update+ installs required system packages (SDL2, python venv/pip, DejaVu mono fonts). - Creates a venv at:
~/terminal_venv - Installs
pysdl2andpyteinto that venv (fromrequirements.txt).
Both Python dependencies are pure Python or prebuilt bindings, so no compiler toolchain is needed on the device.
Building from source? Run
./debug-scripts/build-zip.shto producedist/R36Term.zipyourself instead of downloading a release.
Once installed, you can update in place from Settings → About → Update: it checks the latest GitHub release and, if a newer one is available, downloads and applies the release zip (restart R36Term afterward to run the new version). You can also just download a newer R36Term.zip and extract it over /roms/tools/R36Term/ again.
Two ways, both from the Tools menu:
Run-Terminal.sh— launches the terminal directly (use this once dependencies are installed).Terminal-Installer.sh→Run app— the installer can also launch it, and returns to its menu on exit.
Either way the terminal runs using the venv Python and launches:
Terminal/r36term_app.py
To find where frames are spent on the actual hardware, run the app with
--profile (uses the stdlib cProfile, so nothing extra to install):
# On the device, from a real shell (SSH or a text console), as the same user
# the launcher uses:
~/terminal_venv/bin/python3 /roms/tools/R36Term/Terminal/r36term_app.py --profileUse the app for a bit (open the keyboard, run something text-heavy), then quit
(Start + Select). On exit it writes ~/r36term.prof and prints the top 25
functions by cumulative time to stderr. Analyse the dump later on any machine:
python3 -m pstats ~/r36term.prof # then: sort cumtime / stats 30Pass a path to change the output file: --profile /tmp/run.prof. The About →
Developer page also reports the live SDL2 backend (e.g. opengles2 (accelerated)), which tells you whether rendering is GPU-accelerated or falling
back to software.
- A real terminal. One persistent shell on a pty; every program that works in xterm works here.
- Full ANSI color — 16 named, 256-indexed, and 24-bit truecolor, plus bold/underline/reverse.
- Scrollback with history you can page through (2000 lines by default).
- On-screen keyboard with proper Ctrl, Alt, arrows, Home/End, PgUp/PgDn, Insert/Delete and F1–F12.
- Settings overlay for theme, font size, cursor style, and chrome toggles.
- Hardware keyboard support too, if you plug one in via USB/OTG.
Themes are loaded from Terminal/Themes/*.json. Each theme's semantic colors are mapped onto the 16 ANSI slots automatically, so existing themes gain color support for free. A theme can also specify an explicit "ansi" block to control all 16 slots directly:
{
"name": "My Theme",
"background": "#0f172a",
"text": "#e2e8f0",
"ansi": {
"red": "#ff5555",
"green": "#50fa7b",
"blue": "#6272a4"
}
}A hint bar along the bottom shows what each button does right now, Big Picture style, and updates as the context changes. There's also a full Controls & help page in the settings overlay (Start).
Select toggles the on-screen keyboard, which switches between the two modes. The header shows which one you're in (KBD or NAV).
| Input | Keyboard shown (KBD) |
Keyboard hidden (NAV) |
|---|---|---|
| Select | Hide the keyboard | Show the keyboard |
| Start | Open settings overlay | Open settings overlay |
| Start + Select | Exit R36Term | Exit R36Term |
| D-Pad / stick | Move between keys | Arrow keys |
| A | Press the selected key | Enter |
| X | Backspace | Backspace |
| Y | Space | Space |
| B | Esc | Esc |
| L1 | Shift | — |
| R1 | Enter | — |
| L2 | Ctrl (latches for one key) | — |
| R2 | Alt / Meta (latches for one key) | — |
| Select + A / B | — | y / n |
| Select + X | — | Ctrl+C |
| Select + L1 / R1 | Home / End | Home / End |
| Select + L2 / R2 | Meta + ← / → | Meta + ← / → |
| FN (Guide) | Fn layer (see below) | Fn layer (see below) |
Select layer (nav mode) — with the keyboard hidden, hold Select to reveal a small cheat-sheet; Select + A / B answer y / n prompts, Select + X sends Ctrl+C, Select + L1 / R1 send Home / End, and Select + L2 / R2 send Meta+← / Meta+→ (word-jump in most shells). A quick Select tap still shows the keyboard. (The right stick still scrolls the scrollback.)
Guide (Fn) layer — the Guide button is a held modifier. While you hold it, the face buttons emit terminal combos (the d-pad is left alone since it collides with the hardware volume control). Hold Guide on its own to pop a small cheat-sheet; a quick tap just reminds you to hold it.
| Combo | Sends |
|---|---|
| Guide + A | Tab |
| Guide + B | Shift+Tab |
| Guide + X | Esc |
| Guide (hold) | Show the Fn cheat-sheet |
| Guide (double-tap) | Cycle the on-screen keyboard's Fn layout |
The keys themselves are labelled with these buttons — Backspace shows an X badge, Enter shows Y, Tab shows L1, Ctrl shows L2, Alt shows R2 — so you don't have to memorize the table. Double-tap Guide to switch between the main and Fn keyboard layouts.
Hiding the keyboard gives the terminal its rows back — at 640×480 that's 17 rows → 28, about 65% more visible output — and hands the D-pad and analog stick straight to the terminal as arrow keys. That's the mode for reading output, paging in less, or navigating in vim. Held directions auto-repeat with acceleration.
The keyboard is laid out like the Steam Big Picture one: a full staggered US keyboard on a single screen, with modifiers down the left edge and dual glyphs on the punctuation keys, so you can see that 4 gives $ without switching anything. Pressing Shift promotes the shifted glyph in place, the way the output will actually look.
R2 switches to the Fn layer: F1–F12, the arrow/Home/End/PgUp/PgDn/Ins/Del cluster, the shell control codes (^C ^D ^Z ^L ^A ^E ^K ^U ^W ^R ^Y ^P), and snippets for strings that are tedious one key at a time (~/ ../ ./ | > >> && $( "" '').
All 94 printable ASCII characters are reachable, most in one keypress. Ctrl, Alt and Shift latch for a single keypress; the header shows which are armed.
The bottom bar lists the buttons that matter in the current context and reorders itself between KBD, NAV, settings, and help. Face buttons get the usual gamepad colors. If a button is remapped by Swap A/B, the bar and help page both show the physical button, so they never lie about what to press. Turn the bar off under Button hints in settings to reclaim the row.
For the full list, open Controls & help in the settings overlay — it covers both modes, all three keyboard layouts, and the modifier behavior. Scroll with the D-pad or L1/R1, close with B. On a USB keyboard, F1 opens and closes it.
Three ways out: Start + Select quits immediately, Exit R36Term at the bottom of the settings overlay asks once before quitting, or just type exit in the shell. All of them shut the shell down cleanly.
If a USB keyboard is attached it works as a normal terminal keyboard, modifiers and all. Shift+PageUp / Shift+PageDown scroll the view.
Some R36S clones wire the face buttons in the Nintendo order, with A and B mirrored relative to the Xbox layout. Turn on Swap A/B in the settings overlay (Start), then Save settings.
The swap is applied at the label level, so everything that names a button follows it: the hint bar, the help page, and the badges printed on the keys all show the button as it's printed on your hardware, with the colour that belongs to that letter.
R36Term stores settings in Terminal/terminal_config.json. Use the in-app settings overlay (Start) to change theme, font size, cursor style, and chrome, then pick Save settings.
Notable keys:
| Key | Meaning |
|---|---|
screen_width / screen_height |
0 means autodetect from the display. Set both to override |
fullscreen |
Use fullscreen-desktop (default true) |
shell / shell_args |
Which shell to run, e.g. /bin/bash with ["-l"] |
input.swap_ab |
Swap the A and B buttons |
terminal.term |
TERM exported to the child (default xterm-256color) |
terminal.scrollback |
Lines of scrollback to retain |
terminal.font_path |
Must be a monospace font. Auto-detected if empty |
terminal.font_fallback_paths |
Ordered list of extra fonts; each glyph the primary font lacks (CJK, symbols, …) falls through to the first font here that provides it |
terminal.font_size |
Font size; the grid is refit to the screen automatically |
terminal.cursor_style |
block, bar, or underline |
theme_settings.show_keyboard |
Whether the OSK starts visible (Select toggles it live) |
theme_settings.show_hints |
Show the controller hint bar (default true) |
By default the panel resolution is discovered from SDL at startup, so the same build fits a 640×480, 720×720 or 1280×720 device without editing anything. The grid and all UI chrome are then derived from that and from the font metrics, so the terminal always fills the screen exactly.
The settings overlay shows the detected size and resulting grid. Measured results:
| Panel | Grid (keyboard shown) | Grid (keyboard hidden) |
|---|---|---|
| 480×320 | 60×11 | 60×17 |
| 640×480 | 80×19 | 80×27 |
| 720×720 | 90×33 | 90×42 |
| 1280×720 | 160×28 | 160×41 |
| 1920×1080 | 240×45 | 240×62 |
The keyboard takes 20–31% of the panel height depending on aspect ratio, and every resize — including toggling the keyboard — pushes the new size to the shell via TIOCSWINSZ and SIGWINCH, so full-screen programs reflow correctly.
To pin a specific size instead, set screen_width and screen_height to non-zero values.
Run the test suite on a Linux or macOS machine:
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
./tests/run_all.sh "$PWD/.venv/bin/python3"The suite covers key encoding, emulator behavior, offscreen rendering, pty transport, an end-to-end live shell, and real vi/less sessions. The pty tests spawn real shells, so they need to run outside any sandbox.
To test against the target OS exactly (Ubuntu 19.10 / Python 3.7.5 / SDL 2.0.10):
./tests/run_ubuntu1910.sh # requires DockerThe device runs Ubuntu 19.10 (eoan), which constrains things in ways worth knowing before changing dependencies:
- Python is 3.7.5. No walrus operator, no
dict |merge, nolist[str]builtin generics, nofunctools.cached_property. pyteis pinned to 0.8.1, not 0.8.2 — the latter declaresrequires_python >= 3.8and pip will not even list it on 3.7.wcwidthis pinned to 0.2.5 for the same reason.- 19.10 is EOL, so its apt archives live on
old-releases.ubuntu.com. The launcher rewritessources.listautomatically when it detects an EOL codename. - SDL is 2.0.10. Its headless dummy video driver cannot read textures back via
SDL_RenderReadPixels, so the two pixel-verification tests detect this and skip those specific assertions. They still run the draw calls, and they still verify pixels on newer SDL. This does not affect the device, which renders to a real framebuffer.
- Tested on R36S clone G80CA-MB V1.2-20250422
- dArkos Release (01012026) : https://github.com/southoz/dArkOS-G80CA

