[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-81902":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":9,"language":10,"languages":9,"totalLinesOfCode":9,"stars":11,"forks":12,"watchers":13,"openIssues":13,"contributorsCount":14,"subscribersCount":14,"size":14,"stars1d":15,"stars7d":16,"stars30d":17,"stars90d":14,"forks30d":14,"starsTrendScore":17,"compositeScore":18,"rankGlobal":9,"rankLanguage":9,"license":19,"archived":20,"fork":20,"defaultBranch":21,"hasWiki":22,"hasPages":20,"topics":23,"createdAt":9,"pushedAt":9,"updatedAt":43,"readmeContent":44,"aiSummary":45,"trendingCount":14,"starSnapshotCount":14,"syncStatus":15,"lastSyncTime":46,"discoverSource":47},81902,"DSSH","Fishason\u002FDSSH","Fishason","Nintendo 3DS SSH client with on-screen pinyin IME, RSA auth, citro2d ANSI terminal, and a crab",null,"C",41,4,1,0,2,3,6,48.2,"Other",false,"main",true,[24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42],"3ds","ansi-terminal","chinese","citro2d","claude-code","devkitarm","devkitpro","homebrew","ime","libctru","libssh2","mbedtls","nintendo-3ds","pinyin","pinyin-input-method","rime-ice","ssh","ssh-client","terminal-emulator","2026-06-12 04:01:36","\u003Cp align=\"center\">\n  \u003Cb>English\u003C\u002Fb> · \u003Ca href=\"README.zh.md\">中文\u003C\u002Fa>\n\u003C\u002Fp>\n\n\u003Ch1 align=\"center\">DSSH\u003C\u002Fh1>\n\n\u003Cp align=\"center\">\n  \u003Cimg src=\"icon.png\" alt=\"DSSH icon\" width=\"96\" height=\"96\">\n\u003C\u002Fp>\n\n\u003Cp align=\"center\">\n  \u003Cb>Nintendo 3DS SSH client — pinyin IME · voice input · ANSI terminal\u003C\u002Fb>\u003Cbr>\n  Top screen runs a citro2d ANSI terminal · bottom screen draws its own\n  soft keyboard · RSA public-key auth over libssh2 + mbedTLS\u003Cbr>\n  Press \u003Cb>START\u003C\u002Fb> to dictate Chinese into Claude Code; type pinyin on\n  the soft keyboard for the rest.\u003Cbr>\n  Run \u003Ccode>tmux\u003C\u002Fcode> + \u003Ccode>claude-code\u003C\u002Fcode> from a 3DS — code from\n  the couch without ever opening the laptop.\n\u003C\u002Fp>\n\n\u003Cp align=\"center\">\n  \u003Cimg alt=\"platform\" src=\"https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Fplatform-Nintendo%203DS-FE0016?logo=nintendo3ds&logoColor=white\">\n  \u003Cimg alt=\"license\" src=\"https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Flicense-MIT-blue\">\n  \u003Cimg alt=\"build\" src=\"https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Fbuild-devkitARM-orange\">\n\u003C\u002Fp>\n\n\u003Cp align=\"center\">\n  \u003Cimg src=\"docs\u002Fmedia\u002Fpreview.gif\" alt=\"DSSH live demo\" width=\"540\">\u003Cbr>\n  \u003Csub>Real New 2DS XL · top screen ANSI terminal · bottom screen soft\n  keyboard + clock + crab\u003C\u002Fsub>\n\u003C\u002Fp>\n\n\u003Cp align=\"center\">\n  \u003Ca href=\"https:\u002F\u002Fgithub.com\u002FFishason\u002FDSSH\u002Freleases\u002Flatest\u002Fdownload\u002Fdemo.mp4\">\n    Full 1m42s demo video (10 MB MP4)\n  \u003C\u002Fa>\n\u003C\u002Fp>\n\n\u003Cp align=\"center\">\n  \u003Cimg src=\"docs\u002Fmedia\u002Fposter.jpg\" alt=\"Real device: typing Chinese into Claude Code via the pinyin IME\" width=\"720\">\u003Cbr>\n  \u003Csub>Real New 2DS XL · top: typing「你好啊！请问您是谁，你可以做什么」into\n  Claude Code · bottom: letter page + CHN mode + Shift held\u003C\u002Fsub>\n\u003C\u002Fp>\n\n---\n\n## Features\n\n- **Full ANSI \u002F VT100 terminal** — tmux status bar, claude-code spinner,\n  box-drawing borders, 256-color, TrueColor, Braille; everything renders.\n- **Chinese rendering** — bundled Zpix 12px pixel font covers 21,000+ CJK\n  unified ideographs, Terminus 6×12 for ASCII; mixed CJK\u002FASCII baselines\n  align cleanly on the same line.\n- **Self-drawn soft keyboard** — iOS-style 3px rounded keys with smooth\n  press-down animation; letters \u002F symbols pages.\n- **Pinyin input method** — top 300k entries from rime-ice, plus\n  abbreviation matching (`nh` → 你好), prefix fallback (`nihaoz`\n  auto-falls-back to `nihao`), and a candidate cursor.\n- **Voice input (v1.0)** — press **START**, speak a Chinese sentence,\n  press **START** again; ~1-2 s later the transcribed text drops\n  straight into the SSH terminal.  Default backend is OpenRouter\n  Whisper Large V3 Turbo over the cloud (`$0.04` per audio-hour); a\n  self-hosted whisper.cpp track is available if you'd rather not depend\n  on an external API.\n- **Voice AI ask (NEW in v1.1)** — hold **L** and press **START** to\n  ask DeepSeek-Chat a question by voice; the answer pops up in a\n  bottom-screen modal with markdown-styled rendering (headers in\n  yellow, code in cyan, bullets, etc.) without disturbing the SSH\n  session above.  Press **A** in the modal to keep history for follow-\n  up questions, **B** to clear and start a new conversation.\n- **RSA-4096 public-key auth** — libssh2 + mbedTLS, private key read\n  from the SD card.\n- **Full physical-key mapping** — D-pad arrow keys, hold-style modifiers\n  (L = Shift, Y = Ctrl, X = Alt), Circle Pad scrollback \u002F mouse-wheel.\n- **Anthropic-red crab mascot** — scampers along the bottom row, dodges\n  when you tap it 🦀.\n- **Hidden debug page** — double-tap the ENG\u002FCHN badge to see the live\n  SSH byte stream, full key-binding cheat sheet, and a mascot toggle.\n\n## Table of contents\n\n- [Install](#install)\n- [Server-side setup](#server-side-setup-one-time)\n- [Configure config.ini](#configure-configini)\n- [Voice input](#voice-input)\n- [Key bindings](#key-bindings)\n- [Using the IME](#using-the-ime)\n- [Debug page](#debug-page)\n- [Build from source](#build-from-source)\n- [Project layout](#project-layout)\n- [Credits](#credits)\n- [License](#license)\n\n---\n\n## Install\n\nDSSH runs on a **modded 3DS \u002F 2DS \u002F New 3DS**.  You need either the\nHomebrew Launcher (HBL) or a CIA installer like FBI.\n\n### Option A — `.cia` install (recommended)\n\n1. Grab `DSSH.cia` (~14 MB) from the [latest release](..\u002F..\u002Freleases\u002Flatest).\n2. Copy it anywhere on the SD card (e.g. `\u002Fcias\u002FDSSH.cia`).\n3. Open FBI → SD → select `DSSH.cia` → `Install CIA`.\n4. The orange DSSH icon shows up on the HOME menu.\n\n### Option B — `.3dsx` direct launch\n\n1. Grab `3dssh.3dsx` from the latest release.\n2. Copy to `\u002F3ds\u002Fdssh\u002Fdssh.3dsx` on the SD card.\n3. Open HBL → pick DSSH.\n\n### Option C — `3dslink` over Wi-Fi (developer flow)\n\n```bash\n# On the 3DS: launch HBL, press Y → \"Waiting for 3dslink...\"\n3dslink -a \u003C3DS-LAN-IP> 3dssh.3dsx\n```\n\n---\n\n## Server-side setup (one-time)\n\nThe 3DS libssh2 build uses mbedTLS as its crypto backend and\n**hardcodes-disables ed25519**.  So you generate a fresh RSA-4096\nkeypair just for the 3DS — your existing ed25519 key on the PC keeps\nworking untouched.\n\n> ⚠️ **The `-m PEM` flag matters.**  devkitPro's `3ds-mbedtls`\n> package is pinned at 2.28.x, which can only parse the **traditional\n> PEM** private-key format (`-----BEGIN RSA PRIVATE KEY-----`).  Recent\n> `ssh-keygen` (Ubuntu 18.04+, macOS) defaults to the **new OpenSSH\n> format** (`-----BEGIN OPENSSH PRIVATE KEY-----`) which mbedtls 2.28\n> *cannot* read — DSSH will fail at handshake with \"auth failed\" if\n> you skip `-m PEM`.\n\n```bash\n# 1. Generate a 3DS-only RSA key on your PC.  -m PEM forces the\n#    traditional PEM format so the on-3DS mbedtls can parse it.\nssh-keygen -t rsa -b 4096 -m PEM -f ~\u002F.ssh\u002Fid_rsa_3ds -C \"3ds-ssh-client\"\n\n# 2. Copy the public half into the server's authorized_keys\nssh-copy-id -i ~\u002F.ssh\u002Fid_rsa_3ds.pub user@your-server.example.com\n\n# 3. Verify the new RSA key works from your PC\nssh -i ~\u002F.ssh\u002Fid_rsa_3ds user@your-server.example.com 'echo OK'\n```\n\n**Already have an OpenSSH-format key?**  Convert it in place — no\nneed to re-add it to the server (the public half is unchanged):\n\n```bash\nssh-keygen -p -m PEM -f ~\u002F.ssh\u002Fid_rsa_3ds\n# enter old passphrase (empty if none) and accept any new passphrase.\n# The file's first line should now read: -----BEGIN RSA PRIVATE KEY-----\n```\n\nYou can sanity-check the format with `head -1 ~\u002F.ssh\u002Fid_rsa_3ds`.\n\n**Recommended hardening**: prepend the new line in the server's\n`~\u002F.ssh\u002Fauthorized_keys` with `from=\"\u003Cyour-home-public-IP>\"` so a lost\nSD card can only log in from your home network.\n\nCopy the **private key** `~\u002F.ssh\u002Fid_rsa_3ds` onto the SD card at\n`\u002F3ds\u002F3dssh\u002Fid_rsa` (the path is fixed even when DSSH is installed as a\n.cia — config + key always read from `sdmc:\u002F3ds\u002F3dssh\u002F`).\n\n> ⚠️ The SD card stores the key in plain text.  Anyone holding the SD\n> can log in to your server.  Add `from=\"...\"` IP restriction or\n> `command=\"...\"` lockdown in `authorized_keys`.\n\n---\n\n## Configure config.ini\n\nCopy `sd_template\u002F3ds\u002F3dssh\u002Fconfig.ini.example` to the SD card at\n`\u002F3ds\u002F3dssh\u002Fconfig.ini` and edit the values:\n\n```ini\nhost       = your-server.example.com\nport       = 22\nuser       = ubuntu\nkey_path   = sdmc:\u002F3ds\u002F3dssh\u002Fid_rsa\npassphrase =\n```\n\n| Field | Meaning |\n|---|---|\n| `host` | Server IP or hostname |\n| `port` | Port (default 22) |\n| `user` | SSH login user |\n| `key_path` | Private key path; `sdmc:\u002F...` is the 3DS standard SD prefix |\n| `passphrase` | Optional key passphrase; leave empty (typing one on the soft keyboard is awkward) |\n\nFinal SD layout:\n\n```\nsdmc:\u002F3ds\u002F3dssh\u002F\n├── config.ini\n└── id_rsa\n```\n\n---\n\n## Voice input\n\n> **TL;DR**: install the **API track** (one curl + one API key, ~30 KB\n> on disk, ~1-2 s end-to-end).  The self-hosted track exists for\n> completeness but is *not recommended* for typical servers — see the\n> warning at the bottom of this section.\n\nPress **START** on the 3DS, speak a Chinese sentence, press **START**\nagain — the transcribed UTF-8 text drops into the SSH terminal as if\nyou had typed it.  Full sentences flow into Claude Code without ever\nopening the soft keyboard.\n\nThe 3DS records 16 kHz PCM mono via its built-in microphone, ships up\nto 8 seconds of audio over a **second libssh2 channel** on the same\nSSH session (no new ports, no new auth, no firewall changes), and a\nsmall server-side shim transcribes via Whisper.\n\n**Status indicator** (top-left of the soft keyboard top row):\n- 🔴 **REC** (red, pulsing) — recording in progress\n- ⠋⠙⠹⠸ (cyan, spinning) — uploading + transcribing\n- **ERR** (red, 2 s) — request failed; press START again to retry\n\n### Recommended install — API track\n\nThe voice features need **two API keys** on the server.  Both\ntogether cost a few cents per month for personal use:\n\n| Key | Where to get it | What it powers | Required? |\n|---|---|---|---|\n| **OpenRouter** | [openrouter.ai\u002Fsettings\u002Fkeys](https:\u002F\u002Fopenrouter.ai\u002Fsettings\u002Fkeys) | Whisper Large V3 Turbo (speech → text) | **Required** for voice IME (START) and AI ask (L+START) |\n| **DeepSeek** | [platform.deepseek.com\u002Fapi_keys](https:\u002F\u002Fplatform.deepseek.com\u002Fapi_keys) | DeepSeek-Chat (AI question answering) | Optional — only needed for L+START AI ask; voice IME works without it |\n\nPricing: OpenRouter Whisper Turbo is `$0.04 \u002F audio-hour` (~a few\ncents per month for an individual); DeepSeek-Chat is roughly\n`$0.0001 per question`.  Both providers give a small free credit on\nsign-up, more than enough to test.\n\n#### One-command install\n\nSSH into your server, then:\n\n```bash\ngit clone https:\u002F\u002Fgithub.com\u002FFishason\u002FDSSH.git ~\u002Fdssh-repo\nbash ~\u002Fdssh-repo\u002Ftools\u002Finstall_whisper_api.sh\n```\n\nThe installer will interactively prompt for both keys:\n\n```\n▶ checking prerequisites...\n✓ python3 3.10\n▶ installing daemon + shim + dssh-whisper CLI ...\n▶ writing track config...\n\nNeed your OpenRouter API key (https:\u002F\u002Fopenrouter.ai\u002Fsettings\u002Fkeys).\nUsed for Whisper transcription.  Looks like:  sk-or-v1-...\nPaste your OpenRouter key (or empty to skip): █\n\n✓ api-key saved at \u002Fhome\u002Fyou\u002F.config\u002Fdssh-whisper\u002Fapi-key (chmod 0600)\n\nOptional: DeepSeek API key (https:\u002F\u002Fplatform.deepseek.com\u002Fapi_keys).\nUsed only by the L+START voice AI-ask modal — skip if you only\nwant plain voice IME.  Looks like:  sk-...\nPaste your DeepSeek key (or empty to skip): █\n\n✓ deepseek-key saved at \u002Fhome\u002Fyou\u002F.config\u002Fdssh-whisper\u002Fdeepseek-key (chmod 0600)\n✓ dssh-whisper-api installed (lightweight, OpenRouter Whisper Turbo).\n```\n\n**Non-interactive** install (handy for CI \u002F Ansible \u002F clean rebuilds):\n\n```bash\nOPENROUTER_API_KEY=\"sk-or-v1-...\" \\\nDEEPSEEK_API_KEY=\"sk-...\" \\\nbash ~\u002Fdssh-repo\u002Ftools\u002Finstall_whisper_api.sh\n```\n\n#### Verify the install worked\n\n```bash\n$ dssh-whisper status\nactive track: api\n(API-only install — no local daemon)\napi-key:  ✓ \u002Fhome\u002Fyou\u002F.config\u002Fdssh-whisper\u002Fapi-key\n```\n\nThe 3DS calls `~\u002F.local\u002Fbin\u002Fdssh-whisper-shim` over SSH-exec on every\n**START** press; no further server-side action is needed.\n\n#### What the installer dropped on disk\n\n```\n~\u002F.config\u002Fdssh-whisper\u002F\n├── track            # \"api\" or \"local\"\n├── api-key          # chmod 0600 — OpenRouter\n└── deepseek-key     # chmod 0600 — DeepSeek (optional)\n~\u002F.local\u002Fbin\u002F\n├── dssh-whisper          # CLI wrapper (start\u002Fstop\u002Fstatus\u002Fswitch\u002Funinstall)\n└── dssh-whisper-shim     # symlink the 3DS reaches over SSH-exec\n~\u002F.local\u002Fshare\u002Fdssh-whisper\u002F\n└── whisper_shim.py       # the actual Python that talks to both APIs\n```\n\nFootprint: ~30 KB.  No daemon, no model, nothing to monitor.\n\n#### Rotating a key later\n\nCompromised key, expired credit, swapping providers — overwrite the\nfile:\n\n```bash\necho 'sk-or-v1-NEW_KEY' > ~\u002F.config\u002Fdssh-whisper\u002Fapi-key\nchmod 0600 ~\u002F.config\u002Fdssh-whisper\u002Fapi-key\n# (no service to restart — the shim re-reads the key on every call)\n```\n\n| Field | Value |\n|---|---|\n| Inference | OpenRouter Whisper Large V3 Turbo (cloud) + DeepSeek-Chat |\n| Latency (4 s clip) | ~1-2 s STT, +1-2 s LLM for AI ask |\n| Cost | $0.04 \u002F audio-hour STT + ~$0.0001 \u002F AI question |\n| Server install size | ~30 KB |\n| Server CPU load | negligible |\n| Internet | required (HTTPS to openrouter.ai + api.deepseek.com) |\n\nThat's enough for 99% of users — install it, press START, done.\n\n### `dssh-whisper` CLI\n\n```\ndssh-whisper status                # active track + daemon status + key presence\ndssh-whisper switch                # toggle api ↔ local\ndssh-whisper switch [api|local]    # set explicit track\ndssh-whisper start                 # start local daemon (dual install only)\ndssh-whisper stop | close          # stop local daemon\ndssh-whisper restart\ndssh-whisper logs [-f]             # tail systemd-user logs (dual install)\ndssh-whisper uninstall             # remove all dssh-whisper files\n```\n\nDaemon-related commands degrade gracefully on the API-only install\n(they print \"no daemon to start\", non-fatal).\n\n### Quick AI questions (L + START) — v1.1\n\nStuck in `yazi` and forgot the keybind for hidden files?  Need a\none-liner regex for `vim`?  Hold **L** and press **START** — you're\nnow asking the AI instead of typing into the SSH session.  A modal\npops up on the bottom screen with the answer; the SSH terminal on top\nis left untouched.\n\n| In modal | Effect |\n|---|---|\n| **A** | close, **keep** the Q&A in history; next L+START continues the conversation |\n| **B** or any **touch** on the bottom screen | close, **clear** history; next L+START is fresh |\n\nConversation history caps at 5 turns; if you keep pressing A past\nthat, the oldest turn drops off (FIFO).\n\nThe model is `deepseek-chat` (DeepSeek direct API, not via\nOpenRouter) — picked for its sub-second latency and very low pricing\n(~$0.0001 per question for personal use).  Answers are 6-15 sentences\ntypically; long answers wrap inside the modal and overflow truncates\nwith a trailing `...`.\n\n#### Markdown rendering in the modal\n\nDeepSeek tends to use markdown.  The modal renders common forms in\ncolour rather than showing raw syntax characters:\n\n| Markdown source | Modal rendering |\n|---|---|\n| `# Heading` \u002F `## Heading` \u002F `### Heading` | yellow accent, `#` markers stripped |\n| `` `inline code` `` | cyan, backticks stripped |\n| ` ``` …code block… ``` ` | cyan multi-line, fences stripped |\n| `- item` \u002F `* item` | `• item` (dim grey bullet + normal text) |\n| `**bold**` \u002F `*italic*` | normal text — syntax stripped, no emphasis (no bold font at this size) |\n| `[link text](url)` | `link text` only — URL dropped |\n| `~~strikethrough~~` | normal text — syntax stripped |\n\n#### DeepSeek key\n\nYou'll need a **DeepSeek API key** from\n[platform.deepseek.com\u002Fapi_keys](https:\u002F\u002Fplatform.deepseek.com\u002Fapi_keys).\nThe installer prompts for it alongside the OpenRouter key (skippable\n— plain voice IME works without it).  Stored at\n`~\u002F.config\u002Fdssh-whisper\u002Fdeepseek-key` chmod 0600 — see [Rotating a\nkey later](#rotating-a-key-later) above to change it.\n\n### Notes\n\n- 3DS firmware caps recording at ~32 s per press (the 1 MB mic buffer\n  fills at 16 kHz × 16-bit).  Tap **START** earlier to commit any time.\n- Use **HOME** (not START) to exit DSSH — START is dedicated to voice.\n- `~\u002F.config\u002Fdssh-whisper\u002F` is on `.gitignore` already; rotating the\n  API key is one `echo > api-key` away.\n- The 3DS code calls `~\u002F.local\u002Fbin\u002Fdssh-whisper-shim` over a libssh2\n  exec channel.  The shim reads the active track and dispatches —\n  switching tracks doesn't require restarting the 3DS or the SSH\n  session.\n\n### Advanced — Dual track (self-hosted, ⚠️ not recommended)\n\n> ⚠️ **Heads-up**: the self-hosted track loads the `whisper-small`\n> model (~1 GB resident) and runs CPU inference for every recording.\n> On a 2-vCPU AWS t3.medium with VS Code Remote, claude-code, tmux, and\n> chrome-devtools-mcp running, transcribing 4 seconds of audio took\n> **~40 seconds** — vs. **~1.5 seconds** through OpenRouter at the same\n> moment.  The cost difference is so small ($0.04 per *audio* hour ≈\n> pennies\u002Fmonth for personal use) that we strongly recommend the cloud\n> path unless you have a hard reason to keep audio on-prem.\n>\n> If you do go local, plan on:\n>\n> - **4+ idle vCPU cores** at 3+ GHz — anything less and the 3DS UX\n>   spinner becomes painful.\n> - **2+ GB free RAM** for the small.zh model + buffers.\n> - **No competing CPU consumers** during transcription windows.\n> - **~600 MB disk** for the model + venv.\n\nIf you genuinely want the offline path, the same `dssh-whisper` CLI\nmanages both tracks side-by-side — install the dual variant and flip\non demand:\n\n```bash\ngit clone https:\u002F\u002Fgithub.com\u002FFishason\u002FDSSH.git ~\u002Fdssh-repo\nbash ~\u002Fdssh-repo\u002Ftools\u002Finstall_whisper_dual.sh\n```\n\nThe dual install still defaults to `track=api`; flip to local only when\nneeded:\n\n```bash\ndssh-whisper switch local   # next START press → self-hosted whisper.cpp\ndssh-whisper switch api     # next START press → OpenRouter (default)\ndssh-whisper switch         # no arg = toggle\n```\n\n---\n\n## Key bindings\n\n### Physical buttons\n\n| Button | Function | Notes |\n|---|---|---|\n| **A** | Enter (EN) \u002F **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. |\n| **B** | Backspace \u002F consume one pinyin letter | Hold-style auto-repeat (peaks at 60 \u002F sec) |\n| **X** | Alt modifier | Hold-style — held when the next key fires |\n| **Y** | Ctrl modifier | Hold-style — Y + tap `c` → Ctrl-C |\n| **L** | Shift modifier \u002F **+ Circle Pad → right pane** | See [tmux split scrolling](#tmux-split-scrolling) below |\n| **R** | Toggle CN\u002FEN input mode | Top-right ENG\u002FCHN reflects the current mode |\n| **SELECT** | Esc | Tap fires immediately |\n| **START** | **Voice input toggle** | Press once to start recording; press again to stop and transcribe.  See [Voice input](#voice-input). |\n| **Space** (soft keyboard) | Plain space (EN) \u002F **commit highlighted candidate** (IME) | Matches sogou \u002F fcitx convention |\n| **Shift + .** | **。** (full-width Chinese period, U+3002) | Works in both EN and CN modes |\n| **D-pad ↑↓** | Arrow keys \u002F IME page nav | When the IME buffer is active, ↑↓ paginates candidates |\n| **D-pad ←→** | Arrow keys \u002F IME selection cursor | When active, ←→ moves the candidate cursor within the page |\n| **Circle Pad ↑↓** | Scrollback \u002F tmux mouse-wheel | Default targets the left\u002Ftop pane; **hold L → right\u002Fbottom pane** |\n\n> Long-press D-pad or B: 250 ms initial delay, ramps up to 12 \u002F sec at\n> 0.5 s, peaks at 60 \u002F sec after 1.5 s.\n\n### tmux split scrolling\n\nThe 3DS has no real cursor, so tmux's mouse-wheel events get routed by\nthe `(col, row)` we send.  DSSH defaults to `(1, 1)` → hits the\nleft\u002Ftop pane; holding **L** sends `(60, 12)` → hits the right\u002Fbottom\npane.  In a vertical-split tmux:\n\n| Action | Effect |\n|---|---|\n| Circle Pad ↑↓ | Scrolls the **left** pane |\n| L held + Circle Pad ↑↓ | Scrolls the **right** pane |\n\n### Soft keyboard\n\nThe bottom screen is the soft keyboard — two pages:\n\n- **Letters page** (default): QWERTY layout with `,` `.` punctuation,\n  Tab, and a wide Space.\n- **Symbols page** (toggle via the bottom-left `123` key):\n  `1234567890`, `!@#$%^&*()` cleanly aligned on two rows, plus other\n  common punctuation including `?` and `\\`.\n\nAny key supports hold-style modifier combos.  Example:\n**hold Y + tap b** = `Ctrl-B` (the tmux prefix).\n\n### Status bar (top 30 px)\n\n```\n┌──────────────────────────────────────────────────────┐\n│ [SFT]   candidate strip \u002F pinyin buffer \u002F cands [CHN]│\n└──────────────────────────────────────────────────────┘\n```\n\n- **Left slot [STA]**: 3-letter modifier indicator (SFT\u002FCTL\u002FALT stays lit\n  while held; ENT\u002FBSP\u002FESC\u002F`R→C` flashes for 200 ms on transient events).\n- **Middle**: pinyin buffer + candidates in CN mode; empty in EN mode.\n- **Right slot [ENG\u002FCHN]**: current IME mode.  **Double-tap to enter\n  the debug page**.\n\n---\n\n## Using the IME\n\n### Full pinyin\n\nTapping letters in CN mode brings up the candidate strip:\n\n```\nni       → 年 你 牛奶 娘 念   (page 1\u002F52, total 256)\nnihao    → 你好 你好吗 你好啊 拟好 你好呀\nshijie   → 世界 世界上 世界杯 世界各地 世界里\n```\n\n- **A** or **Space** commits the currently highlighted candidate.\n- **D-pad ←→** moves the highlight within the current page.\n- **D-pad ↑↓** flips between pages.\n- **Tap** a candidate to commit it directly.\n- **B** consumes one letter from the pinyin buffer.\n\n### Abbreviation (initials)\n\nEvery multi-syllable word gets an extra entry keyed by its initials —\ntyping the initials still surfaces it (with weight × 0.3, so the\nfull-pinyin form still ranks first when typed in full):\n\n```\nnh → 你好  (around the 8th candidate)\nwm → 我们  (top candidate)\nsj → 世界\nzw → 中文\nxx → 谢谢\n```\n\nPage or cursor over to your target, then commit with A.\n\n### Prefix fallback\n\nTyped an extra letter past a valid prefix?  The engine automatically\nmatches the longest valid prefix and shows the surplus letters in red:\n\n```\nbuffer:  niha[oz]    ← niha in green + oz in red\ncandidates:           still showing what nihao would produce\n```\n\nPress B to chew the red tail back to a clean prefix.\n\n### Modifiers always bypass the IME\n\nIn CN mode, **hold Y + tap c** still sends `Ctrl-C`; **hold L + tap a**\nstill sends `A`.  Modifiers take priority over IME routing, so\nvim \u002F tmux \u002F claude-code shortcuts keep working.\n\n### Bail out: emit pinyin as English\n\nCN mode + non-empty buffer + press **A** = the buffer flies to SSH as\nraw ASCII letters and clears.  Example: you accidentally typed\n`cd \u002Fetc` while in CN mode and the candidate strip is showing strange\nChinese.  One press of **A** delivers `cd \u002Fetc` to the shell — no\nbackspacing, no mode-toggle, no retyping.\n\n> Difference: **Space** commits the highlighted candidate (Chinese\n> chars on screen).  **A** sends the typed letters as-is.\n\n---\n\n## Debug page\n\n**Double-tap the ENG\u002FCHN badge** in the top-right corner (two taps within\n500 ms) to enter the debug overlay.  Single-tap the badge again to leave.\n\nWhat it shows:\n\n- Title + exit hint.\n- **recv hex**: the last 32 bytes received from SSH — for diagnosing\n  ANSI \u002F SCS \u002F mouse-protocol issues at the byte level.\n- **Physical key cheat sheet**: a condensed version of the bindings\n  table above.\n- **MASCOT: ON\u002FOFF** toggle button.  Default is ON.\n\n---\n\n## Build from source\n\n### Prerequisites\n\n- Linux x86\\_64 (tested on Ubuntu 22.04; other distros need the obvious\n  package-name adjustments).\n- [devkitPro \u002F devkitARM](https:\u002F\u002Fdevkitpro.org\u002Fwiki\u002FGetting_Started)\n  release 65+, GCC 14.2.0.\n- Python 3.10+ with Pillow (for font + dictionary generators).\n\n### Steps\n\n```bash\n# 1. Install devkitPro\nwget https:\u002F\u002Fapt.devkitpro.org\u002Finstall-devkitpro-pacman\nbash install-devkitpro-pacman\nsudo dkp-pacman -S 3ds-dev 3ds-mbedtls 3ds-libpng 3ds-zlib\n\n# 2. Clone + cd\ngit clone https:\u002F\u002Fgithub.com\u002FFishason\u002FDSSH.git\ncd DSSH\n\n# 3. Cross-compile libssh2 (one-time, drops into $DEVKITPRO\u002Fportlibs\u002F3ds\u002Flib\u002F)\nbash build-libssh2.sh\n\n# 4. Install system fonts (Terminus provides ASCII \u002F box-drawing)\nsudo apt install fonts-terminus\n\n# 5. Fetch font sources (Zpix)\nbash tools\u002Ffetch_fonts.sh\n\n# 6. Generate the font atlas (→ source\u002Ffont_data.c, ~3 MB)\npython3 tools\u002Fgen_font.py\n\n# 7. Fetch + build the pinyin dictionary (→ romfs\u002Fpinyin_dict.bin, ~13 MB)\nbash tools\u002Ffetch_pinyin_dict.sh\npython3 tools\u002Fgen_pinyin_dict.py\n\n# 8. Build the .3dsx\nmake\n\n# 9. (Optional) build the .cia\nbash tools\u002Finstall_cia_tools.sh   # installs bannertool + makerom into ~\u002Fbin\nmake cia                          # → DSSH.cia\n```\n\n### Test the IME engine on the host (no 3DS needed)\n\n```bash\nmake test-ime\n```\n\nCompiles `tools\u002Ftest_ime.c` linked against `source\u002Fime_pinyin.c` and\nruns nine smoke-test queries (`ni → 你`, `nihao → 你好`, `nh → 你好`,\netc.).\n\n---\n\n## Project layout\n\n```\nDSSH\u002F\n├── 69633.PNG                  # Source icon (162×102)\n├── icon.png                   # 48×48 icon for .3dsx \u002F SMDH (derived)\n├── app.rsf                    # makerom CIA spec\n├── Makefile                   # Top-level build (make \u002F make cia \u002F make test-ime)\n├── build-libssh2.sh           # libssh2 + mbedTLS ARM cross-compile\n├── source\u002F\n│   ├── main.c                 # Main loop, SSH receive, UTF-8 reassembly\n│   ├── ssh_client.{c,h}       # libssh2 wrapper\n│   ├── config.{c,h}           # SD-card config.ini parser\n│   ├── terminal.{c,h}         # ANSI\u002FVT100 parser (forked from skmtrd)\n│   ├── renderer.{c,h}         # citro2d rendering (terminal, text, CJK)\n│   ├── keyboard.{c,h}         # Physical buttons + IME routing\n│   ├── softkb.{c,h}           # Soft keyboard + candidate strip + debug page\n│   ├── ime_pinyin.{c,h}       # Pinyin engine\n│   ├── mascot.{c,h}           # Crab mascot\n│   ├── font_atlas.{c,h}       # Codepoint → glyph index\n│   └── font_data.c            # Font bitmaps (gen_font.py output)\n├── tools\u002F\n│   ├── fetch_fonts.sh         # Download Zpix\n│   ├── gen_font.py            # Font atlas generator\n│   ├── fetch_pinyin_dict.sh   # Download rime-ice\n│   ├── gen_pinyin_dict.py     # Dictionary → binary\n│   ├── test_ime.{c,sh}        # Host-side IME smoke test\n│   ├── gen_cia_assets.py      # Icon \u002F banner derivation\n│   └── install_cia_tools.sh   # bannertool + makerom installer\n├── romfs\u002F                     # gitignored — packs pinyin_dict.bin\n├── data\u002F                      # gitignored — font + dict sources\n└── sd_template\u002F               # SD-card deployment template\n    ├── README.md\n    └── 3ds\u002F3dssh\u002Fconfig.ini.example\n```\n\n## Architecture\n\n```\nSSH server (somewhere on the internet)\n     ▲ libssh2 over mbedTLS-RSA-4096\n     │\n┌────┴──────────────────────────────────────────────────┐\n│  main.c poll loop @ 60 fps                            │\n│   ├─ ssh_read → softkb_record_recv → utf8 reassemble  │\n│   │                ↓                                  │\n│   │   terminal_write_n → ANSI parser → cell grid      │\n│   ├─ hidScanInput → keyboard_handle_input             │\n│   │   └─ IME mode? → ime_input_letter \u002F page \u002F select │\n│   ├─ hidTouchRead → softkb_touch                      │\n│   │   ├─ candidate strip hit → ime_select             │\n│   │   ├─ key hit → keyboard_emit_for \u002F ime_input      │\n│   │   └─ badge double-tap → debug_mode toggle         │\n│   └─ render: top = renderer_draw_terminal             │\n│              bot = softkb_draw + clock + mascot       │\n└───────────────────────────────────────────────────────┘\n     │\n   citro2d (3DS 2D rendering)\n     │\n   GPU (top 400×240 + bottom 320×240, 24-bit color)\n```\n\nThe build went through milestones M0 → M9; see the commit history for\nthe full progression.\n\n---\n\n## Credits\n\n- **[skmtrd\u002F3dssh](https:\u002F\u002Fgithub.com\u002Fskmtrd\u002F3dssh)** — the original\n  Japanese-localized 3DS SSH client; DSSH reuses its ANSI\u002FVT100 parser,\n  UTF-8 reassembly, and citro2d framing.\n- **[rime-ice](https:\u002F\u002Fgithub.com\u002FiDvel\u002Frime-ice)** — pinyin dictionary\n  source (pinned at commit `3f57a6f6`).\n- **[Zpix Pixel Font](https:\u002F\u002Fgithub.com\u002FSolidZORO\u002Fzpix-pixel-font)** —\n  12 px CJK pixel font (OFL 1.1).\n- **[Terminus TTF](https:\u002F\u002Fterminus-font.sourceforge.net\u002F)** — ASCII\n  and box-drawing pixel font.\n- **[libssh2](https:\u002F\u002Fwww.libssh2.org\u002F)** + **[mbedTLS](https:\u002F\u002Fwww.trustedfirmware.org\u002Fprojects\u002Fmbed-tls\u002F)** —\n  SSH \u002F TLS protocol stack.\n- **[devkitPro](https:\u002F\u002Fdevkitpro.org\u002F) libctru \u002F citro2d \u002F citro3d** —\n  3DS user-mode runtime and rendering.\n- **[carstene1ns\u002F3ds-bannertool](https:\u002F\u002Fgithub.com\u002Fcarstene1ns\u002F3ds-bannertool)**\n  + **[3DSGuy\u002FProject_CTR makerom](https:\u002F\u002Fgithub.com\u002F3DSGuy\u002FProject_CTR)** —\n  CIA packaging tools.\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n\nThe bundled fonts, dictionary, and upstream SSH\u002FTLS libraries each\nhave their own licenses (OFL \u002F GPL \u002F BSD \u002F MIT \u002F Apache).  Respect\nthose when redistributing the binary.\n","DSSH 是一个为 Nintendo 3DS 设计的 SSH 客户端，支持拼音输入法、语音输入和 ANSI 终端。其核心功能包括全功能的 ANSI\u002FVT100 终端模拟器，能够渲染 tmux 状态栏、256 色及 TrueColor 图形；内置了支持超过 21,000 个 CJK 统一汉字的 Zpix 字体，确保中文与英文字符在同一行对齐显示；自绘软键盘提供了流畅的按键动画；以及基于 rime-ice 的拼音输入法，支持缩写匹配和前缀回退。此外，DSSH 还集成了语音输入功能，用户可以通过按下 START 键来将口语转换成文本，并直接发送到 SSH 会话中。该项目特别适合需要在 Nintendo 3DS 上远程操作服务器或编写代码的开发者使用，尤其对于那些偏好移动设备工作环境的人士来说更为便利。","2026-06-11 04:07:08","CREATED_QUERY"]