English · 中文
Nintendo 3DS SSH client with on-screen pinyin IME
Top screen runs a citro2d ANSI terminal · bottom screen draws its own
soft keyboard · RSA public-key auth over libssh2 + mbedTLS
Run tmux + claude-code from a 3DS — code from
the couch without ever opening the laptop.

Real New 2DS XL · top screen ANSI terminal · bottom screen soft
keyboard + clock + crab
Full 1m42s demo video (10 MB MP4)

Real New 2DS XL · top: typing「你好啊!请问您是谁,你可以做什么」into
Claude Code · bottom: letter page + CHN mode + Shift held
- Full ANSI / VT100 terminal — tmux status bar, claude-code spinner, box-drawing borders, 256-color, TrueColor, Braille; everything renders.
- Chinese rendering — bundled Zpix 12px pixel font covers 21,000+ CJK unified ideographs, Terminus 6×12 for ASCII; mixed CJK/ASCII baselines align cleanly on the same line.
- Self-drawn soft keyboard — iOS-style 3px rounded keys with smooth press-down animation; letters / symbols pages.
- Pinyin input method — top 300k entries from rime-ice, plus
abbreviation matching (
nh→ 你好), prefix fallback (nihaozauto-falls-back tonihao), and a candidate cursor. - RSA-4096 public-key auth — libssh2 + mbedTLS, private key read from the SD card.
- Full physical-key mapping — D-pad arrow keys, hold-style modifiers (L = Shift, Y = Ctrl, X = Alt), Circle Pad scrollback / mouse-wheel.
- Anthropic-red crab mascot — scampers along the bottom row, dodges when you tap it 🦀.
- Hidden debug page — double-tap the ENG/CHN badge to see the live SSH byte stream, full key-binding cheat sheet, and a mascot toggle.
- Install
- Server-side setup
- Configure config.ini
- Key bindings
- Using the IME
- Debug page
- Build from source
- Project layout
- Credits
- License
DSSH runs on a modded 3DS / 2DS / New 3DS. You need either the Homebrew Launcher (HBL) or a CIA installer like FBI.
- Grab
DSSH.cia(~14 MB) from the latest release. - Copy it anywhere on the SD card (e.g.
/cias/DSSH.cia). - Open FBI → SD → select
DSSH.cia→Install CIA. - The orange DSSH icon shows up on the HOME menu.
- Grab
3dssh.3dsxfrom the latest release. - Copy to
/3ds/dssh/dssh.3dsxon the SD card. - Open HBL → pick DSSH.
# On the 3DS: launch HBL, press Y → "Waiting for 3dslink..."
3dslink -a <3DS-LAN-IP> 3dssh.3dsxThe 3DS libssh2 build uses mbedTLS as its crypto backend and hardcodes-disables ed25519. So you generate a fresh RSA-4096 keypair just for the 3DS — your existing ed25519 key on the PC keeps working untouched:
# 1. Generate a 3DS-only RSA key on your PC
ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa_3ds -C "3ds-ssh-client"
# 2. Copy the public half into the server's authorized_keys
ssh-copy-id -i ~/.ssh/id_rsa_3ds.pub [email protected]
# 3. Verify the new RSA key works from your PC
ssh -i ~/.ssh/id_rsa_3ds [email protected] 'echo OK'Recommended hardening: prepend the new line in the server's
~/.ssh/authorized_keys with from="<your-home-public-IP>" so a lost
SD card can only log in from your home network.
Copy the private key ~/.ssh/id_rsa_3ds onto the SD card at
/3ds/3dssh/id_rsa (the path is fixed even when DSSH is installed as a
.cia — config + key always read from sdmc:/3ds/3dssh/).
⚠️ The SD card stores the key in plain text. Anyone holding the SD can log in to your server. Addfrom="..."IP restriction orcommand="..."lockdown inauthorized_keys.
Copy sd_template/3ds/3dssh/config.ini.example to the SD card at
/3ds/3dssh/config.ini and edit the values:
host = your-server.example.com
port = 22
user = ubuntu
key_path = sdmc:/3ds/3dssh/id_rsa
passphrase =| Field | Meaning |
|---|---|
host |
Server IP or hostname |
port |
Port (default 22) |
user |
SSH login user |
key_path |
Private key path; sdmc:/... is the 3DS standard SD prefix |
passphrase |
Optional key passphrase; leave empty (typing one on the soft keyboard is awkward) |
Final SD layout:
sdmc:/3ds/3dssh/
├── config.ini
└── id_rsa
| Button | Function | Notes |
|---|---|---|
| A | Enter (EN) / emit pinyin buffer as English (IME) | Accidentally typed English in CN mode? Press A and the buffer flies to SSH as raw ASCII — no need to backspace and switch modes. |
| B | Backspace / consume one pinyin letter | Hold-style auto-repeat (peaks at 60 / sec) |
| X | Alt modifier | Hold-style — held when the next key fires |
| Y | Ctrl modifier | Hold-style — Y + tap c → Ctrl-C |
| L | Shift modifier / + Circle Pad → right pane | See tmux split scrolling below |
| R | Toggle CN/EN input mode | Top-right ENG/CHN reflects the current mode |
| SELECT | Esc | Tap fires immediately |
| START | Quit DSSH | |
| Space (soft keyboard) | Plain space (EN) / commit highlighted candidate (IME) | Matches sogou / fcitx convention |
| Shift + . | 。 (full-width Chinese period, U+3002) | Works in both EN and CN modes |
| D-pad ↑↓ | Arrow keys / IME page nav | When the IME buffer is active, ↑↓ paginates candidates |
| D-pad ←→ | Arrow keys / IME selection cursor | When active, ←→ moves the candidate cursor within the page |
| Circle Pad ↑↓ | Scrollback / tmux mouse-wheel | Default targets the left/top pane; hold L → right/bottom pane |
Long-press D-pad or B: 250 ms initial delay, ramps up to 12 / sec at 0.5 s, peaks at 60 / sec after 1.5 s.
The 3DS has no real cursor, so tmux's mouse-wheel events get routed by
the (col, row) we send. DSSH defaults to (1, 1) → hits the
left/top pane; holding L sends (60, 12) → hits the right/bottom
pane. In a vertical-split tmux:
| Action | Effect |
|---|---|
| Circle Pad ↑↓ | Scrolls the left pane |
| L held + Circle Pad ↑↓ | Scrolls the right pane |
The bottom screen is the soft keyboard — two pages:
- Letters page (default): QWERTY layout with
,.punctuation, Tab, and a wide Space. - Symbols page (toggle via the bottom-left
123key):1234567890,!@#$%^&*()cleanly aligned on two rows, plus other common punctuation including?and\.
Any key supports hold-style modifier combos. Example:
hold Y + tap b = Ctrl-B (the tmux prefix).
┌──────────────────────────────────────────────────────┐
│ [SFT] candidate strip / pinyin buffer / cands [CHN]│
└──────────────────────────────────────────────────────┘
- Left slot [STA]: 3-letter modifier indicator (SFT/CTL/ALT stays lit
while held; ENT/BSP/ESC/
R→Cflashes for 200 ms on transient events). - Middle: pinyin buffer + candidates in CN mode; empty in EN mode.
- Right slot [ENG/CHN]: current IME mode. Double-tap to enter the debug page.
Tapping letters in CN mode brings up the candidate strip:
ni → 年 你 牛奶 娘 念 (page 1/52, total 256)
nihao → 你好 你好吗 你好啊 拟好 你好呀
shijie → 世界 世界上 世界杯 世界各地 世界里
- A or Space commits the currently highlighted candidate.
- D-pad ←→ moves the highlight within the current page.
- D-pad ↑↓ flips between pages.
- Tap a candidate to commit it directly.
- B consumes one letter from the pinyin buffer.
Every multi-syllable word gets an extra entry keyed by its initials — typing the initials still surfaces it (with weight × 0.3, so the full-pinyin form still ranks first when typed in full):
nh → 你好 (around the 8th candidate)
wm → 我们 (top candidate)
sj → 世界
zw → 中文
xx → 谢谢
Page or cursor over to your target, then commit with A.
Typed an extra letter past a valid prefix? The engine automatically matches the longest valid prefix and shows the surplus letters in red:
buffer: niha[oz] ← niha in green + oz in red
candidates: still showing what nihao would produce
Press B to chew the red tail back to a clean prefix.
In CN mode, hold Y + tap c still sends Ctrl-C; hold L + tap a
still sends A. Modifiers take priority over IME routing, so
vim / tmux / claude-code shortcuts keep working.
CN mode + non-empty buffer + press A = the buffer flies to SSH as
raw ASCII letters and clears. Example: you accidentally typed
cd /etc while in CN mode and the candidate strip is showing strange
Chinese. One press of A delivers cd /etc to the shell — no
backspacing, no mode-toggle, no retyping.
Difference: Space commits the highlighted candidate (Chinese chars on screen). A sends the typed letters as-is.
Double-tap the ENG/CHN badge in the top-right corner (two taps within 500 ms) to enter the debug overlay. Single-tap the badge again to leave.
What it shows:
- Title + exit hint.
- recv hex: the last 32 bytes received from SSH — for diagnosing ANSI / SCS / mouse-protocol issues at the byte level.
- Physical key cheat sheet: a condensed version of the bindings table above.
- MASCOT: ON/OFF toggle button. Default is ON.
- Linux x86_64 (tested on Ubuntu 22.04; other distros need the obvious package-name adjustments).
- devkitPro / devkitARM release 65+, GCC 14.2.0.
- Python 3.10+ with Pillow (for font + dictionary generators).
# 1. Install devkitPro
wget https://apt.devkitpro.org/install-devkitpro-pacman
bash install-devkitpro-pacman
sudo dkp-pacman -S 3ds-dev 3ds-mbedtls 3ds-libpng 3ds-zlib
# 2. Clone + cd
git clone https://github.com/Fishason/DSSH.git
cd DSSH
# 3. Cross-compile libssh2 (one-time, drops into $DEVKITPRO/portlibs/3ds/lib/)
bash build-libssh2.sh
# 4. Install system fonts (Terminus provides ASCII / box-drawing)
sudo apt install fonts-terminus
# 5. Fetch font sources (Zpix)
bash tools/fetch_fonts.sh
# 6. Generate the font atlas (→ source/font_data.c, ~3 MB)
python3 tools/gen_font.py
# 7. Fetch + build the pinyin dictionary (→ romfs/pinyin_dict.bin, ~13 MB)
bash tools/fetch_pinyin_dict.sh
python3 tools/gen_pinyin_dict.py
# 8. Build the .3dsx
make
# 9. (Optional) build the .cia
bash tools/install_cia_tools.sh # installs bannertool + makerom into ~/bin
make cia # → DSSH.ciamake test-imeCompiles tools/test_ime.c linked against source/ime_pinyin.c and
runs nine smoke-test queries (ni → 你, nihao → 你好, nh → 你好,
etc.).
DSSH/
├── 69633.PNG # Source icon (162×102)
├── icon.png # 48×48 icon for .3dsx / SMDH (derived)
├── app.rsf # makerom CIA spec
├── Makefile # Top-level build (make / make cia / make test-ime)
├── build-libssh2.sh # libssh2 + mbedTLS ARM cross-compile
├── source/
│ ├── main.c # Main loop, SSH receive, UTF-8 reassembly
│ ├── ssh_client.{c,h} # libssh2 wrapper
│ ├── config.{c,h} # SD-card config.ini parser
│ ├── terminal.{c,h} # ANSI/VT100 parser (forked from skmtrd)
│ ├── renderer.{c,h} # citro2d rendering (terminal, text, CJK)
│ ├── keyboard.{c,h} # Physical buttons + IME routing
│ ├── softkb.{c,h} # Soft keyboard + candidate strip + debug page
│ ├── ime_pinyin.{c,h} # Pinyin engine
│ ├── mascot.{c,h} # Crab mascot
│ ├── font_atlas.{c,h} # Codepoint → glyph index
│ └── font_data.c # Font bitmaps (gen_font.py output)
├── tools/
│ ├── fetch_fonts.sh # Download Zpix
│ ├── gen_font.py # Font atlas generator
│ ├── fetch_pinyin_dict.sh # Download rime-ice
│ ├── gen_pinyin_dict.py # Dictionary → binary
│ ├── test_ime.{c,sh} # Host-side IME smoke test
│ ├── gen_cia_assets.py # Icon / banner derivation
│ └── install_cia_tools.sh # bannertool + makerom installer
├── romfs/ # gitignored — packs pinyin_dict.bin
├── data/ # gitignored — font + dict sources
└── sd_template/ # SD-card deployment template
├── README.md
└── 3ds/3dssh/config.ini.example
SSH server (somewhere on the internet)
▲ libssh2 over mbedTLS-RSA-4096
│
┌────┴──────────────────────────────────────────────────┐
│ main.c poll loop @ 60 fps │
│ ├─ ssh_read → softkb_record_recv → utf8 reassemble │
│ │ ↓ │
│ │ terminal_write_n → ANSI parser → cell grid │
│ ├─ hidScanInput → keyboard_handle_input │
│ │ └─ IME mode? → ime_input_letter / page / select │
│ ├─ hidTouchRead → softkb_touch │
│ │ ├─ candidate strip hit → ime_select │
│ │ ├─ key hit → keyboard_emit_for / ime_input │
│ │ └─ badge double-tap → debug_mode toggle │
│ └─ render: top = renderer_draw_terminal │
│ bot = softkb_draw + clock + mascot │
└───────────────────────────────────────────────────────┘
│
citro2d (3DS 2D rendering)
│
GPU (top 400×240 + bottom 320×240, 24-bit color)
The build went through milestones M0 → M9; see the commit history for the full progression.
- skmtrd/3dssh — the original Japanese-localized 3DS SSH client; DSSH reuses its ANSI/VT100 parser, UTF-8 reassembly, and citro2d framing.
- rime-ice — pinyin dictionary
source (pinned at commit
3f57a6f6). - Zpix Pixel Font — 12 px CJK pixel font (OFL 1.1).
- Terminus TTF — ASCII and box-drawing pixel font.
- libssh2 + mbedTLS — SSH / TLS protocol stack.
- devkitPro libctru / citro2d / citro3d — 3DS user-mode runtime and rendering.
- carstene1ns/3ds-bannertool
- 3DSGuy/Project_CTR makerom — CIA packaging tools.
MIT — see LICENSE.
The bundled fonts, dictionary, and upstream SSH/TLS libraries each have their own licenses (OFL / GPL / BSD / MIT / Apache). Respect those when redistributing the binary.