English · 中文
DSSH
Nintendo 3DS 中文 SSH 客户端 — 拼音 IME · 语音输入 · ANSI 终端
上屏 ANSI 终端 · 下屏自绘软键盘 + 拼音 IME · RSA 公钥认证
按 START 直接说中文给 Claude Code,剩下的拼音键盘补打
从 3DS 直接连服务器跑 tmux + claude-code,离不开沙发也能 coding

实机 New 2DS XL · 上屏 ANSI 终端 · 下屏软键盘 + 时钟 + 螃蟹
完整 1m42s 实机演示(10 MB MP4)

实机 New 2DS XL · 上屏 Claude Code 输入「你好啊!请问您是谁,你可以做什么」 · 下屏字母页 + CHN 模式 + Shift
---
## 功能
- **完整 ANSI/VT100 终端**:tmux 状态栏、claude-code spinner、box-drawing
边框、256-color、TrueColor、Braille — 都正常显示
- **中文渲染**:内置 Zpix 12px 像素字体覆盖 21,000+ 个 CJK 统一汉字 +
Terminus 6×12 ASCII,中英混排 baseline 对齐
- **自绘软键盘**:iOS 风格 3px 圆角键 + 平滑下沉动画,字母 / 符号双页
- **拼音输入法**:rime-ice 顶部 30 万词典 + 缩写匹配(`nh` → 你好)+
前缀 fallback(`nihaoz` 自动回退到 `nihao`)+ 候选条游标导航
- **语音输入(v1.0)**:按 **START** 录中文 → 再按 **START** 停 →
~1-2 秒后转写文字直接进 SSH 终端。默认走 OpenRouter Whisper Large
V3 Turbo 云端推理($0.04/小时、约几美分一个月够个人用);也可装
自部署 whisper.cpp 副轨当离线 / 隐私后路。
- **语音问 AI(v1.1 新增)**:按住 **L + START** 录中文 → 再按一次
停 → 1-2 秒后底屏弹出**问答 modal**,DeepSeek-Chat 答案带
markdown 渲染(标题黄色、行内代码青色、列表 • 前缀等),顶屏
SSH 会话完全不受打扰。modal 里按 **A** 关窗保留 history(可追问),
按 **B** 关窗清空 history(新对话)。
- **RSA-4096 公钥认证**:libssh2 + mbedTLS,私钥放 SD 卡读
- **物理键全映射**:D-pad 方向键、修饰键 hold-style(L=Shift / Y=Ctrl /
X=Alt)、Circle Pad scrollback / mouse-wheel
- **Anthropic 红螃蟹吉祥物**:底行左右奔跑,点击会躲开 🦀
- **隐藏 debug 页面**:双击右上 ENG/CHN 进,看 SSH 字节流 + 物理键说明 +
螃蟹开关
## 目录
- [安装](#安装)
- [服务器侧准备](#服务器侧准备一次性)
- [配置 config.ini](#配置-configini)
- [语音输入](#语音输入)
- [按键说明](#按键说明)
- [输入法用法](#输入法用法)
- [Debug 页面](#debug-页面)
- [从源码构建](#从源码构建)
- [项目结构](#项目结构)
- [致谢](#致谢)
- [License](#license)
---
## 安装
DSSH 跑在 **破解过的 3DS / 2DS / New 3DS** 上。需要 Homebrew Launcher
(HBL)或 FBI 一类的 CIA 安装器。
### 方案 A — `.cia` 安装(推荐)
1. 下载本仓库 [Releases](../../releases) 里的 `DSSH.cia`(约 14 MB)
2. 拷到 SD 卡,路径任意(比如 `/cias/DSSH.cia`)
3. 启 FBI → SD → 选 `DSSH.cia` → `Install CIA`
4. HOME 菜单里出现橙色 DSSH 图标
### 方案 B — `.3dsx` 直跑
1. Releases 里下 `dssh.3dsx`
2. 拷到 SD 卡 `/3ds/dssh/dssh.3dsx`
3. 启 HBL → 选 DSSH
### 方案 C — `3dslink` WiFi 推送(开发用)
```bash
# 3DS 上 HBL 按 Y 进 "Waiting for 3dslink..."
3dslink -a <3DS-LAN-IP> 3dssh.3dsx
```
---
## 服务器侧准备(一次性)
3DS 上的 libssh2 用的是 mbedTLS 后端,**硬编码不支持 ed25519**。所以要
专门给 3DS 生成一把 RSA-4096 公私钥对,原来 PC 上的 ed25519 不动。
> ⚠️ **`-m PEM` 这个参数必加。** devkitPro 仓库里的 `3ds-mbedtls`
> 还是 2.28.x 版本,**只能解析传统 PEM 格式**的私钥(首行
> `-----BEGIN RSA PRIVATE KEY-----`)。Ubuntu 18.04+ / macOS 上的
> 新版 `ssh-keygen` 默认输出的是**新 OpenSSH 格式**(首行
> `-----BEGIN OPENSSH PRIVATE KEY-----`),mbedtls 2.28 **解析不了**
> ——不加 `-m PEM` 的话 DSSH 会在握手时报 "auth failed"。
```bash
# 1. 在你的 PC 上生成 3DS 专用 RSA 密钥。-m PEM 强制传统 PEM 格式,
# 这样 3DS 上的 mbedtls 才能解析。
ssh-keygen -t rsa -b 4096 -m PEM -f ~/.ssh/id_rsa_3ds -C "3ds-ssh-client"
# 2. 把公钥复制到服务器的 authorized_keys
ssh-copy-id -i ~/.ssh/id_rsa_3ds.pub user@your-server.example.com
# 3. 验证从 PC 用新 RSA 能连
ssh -i ~/.ssh/id_rsa_3ds user@your-server.example.com 'echo OK'
```
**已经有 OpenSSH 格式的私钥怎么办?** 原地转格式即可,**不用**再
往服务器加公钥(公钥不变):
```bash
ssh-keygen -p -m PEM -f ~/.ssh/id_rsa_3ds
# 输入旧 passphrase(如果原来没有就直接回车)
# 转换后文件首行应该是:-----BEGIN RSA PRIVATE KEY-----
```
可以用 `head -1 ~/.ssh/id_rsa_3ds` 检查首行确认格式。
**安全建议**:编辑服务器的 `~/.ssh/authorized_keys`,给这把 RSA 那一行
前面加 `from="<家里公网 IP>"`,这样即使 SD 卡丢了,钥匙也只能从你家网
络登入。
把 **私钥** `~/.ssh/id_rsa_3ds` 通过读卡器复制到 SD 卡的
`/3ds/3dssh/id_rsa`(注意:3DS 装 CIA 后 title-id 路径不同,但**配置 +
密钥**始终读 `sdmc:/3ds/3dssh/`)。
> ⚠️ SD 卡是明文。物理持有 SD 的人就能登服务器。务必加 `from="..."` IP
> 限制或 `command="..."` 命令限制。
---
## 配置 config.ini
把仓库里的 `sd_template/3ds/3dssh/config.ini.example` 拷到 SD 卡的
`/3ds/3dssh/config.ini`,按你的服务器改:
```ini
host = your-server.example.com
port = 22
user = ubuntu
key_path = sdmc:/3ds/3dssh/id_rsa
passphrase =
```
| 字段 | 说明 |
|------|------|
| `host` | 服务器 IP 或域名 |
| `port` | 端口(默认 22) |
| `user` | 服务器登录用户名 |
| `key_path` | 私钥路径,`sdmc:/...` 是 3DS 标准 SD 路径前缀 |
| `passphrase` | 私钥口令;建议留空(SD 卡上输 passphrase 体验差) |
最终 SD 卡布局:
```
sdmc:/3ds/3dssh/
├── config.ini
└── id_rsa
```
---
## 语音输入
> **太长不看**:装 **API 轨**(一行 curl + 一个 API key、~30 KB 落地、
> 端到端 1-2 秒)。自部署轨道仓库里也带,但**对服务器要求高、不建议**
> ——见本节末尾的警告。
按 **START** 录音 → 说一句中文 → 再按 **START** 停止;约 1-2 秒后
转写文字直接进 SSH 终端,效果如同您逐字打出来。整段中文只用嘴说就行,
软键盘可以省略。
3DS 用内置麦克风录 16 kHz PCM mono,最多 8 秒音频通过**复用现有 SSH
session 的第二个 channel** 上传(零新端口、零新认证、零防火墙改动),
服务器端的 shim 调 Whisper 转写。
**状态指示**(软键盘左上角):
- 🔴 **REC**(红色脉冲)— 录音中
- ⠋⠙⠹⠸(青色旋转)— 上传 + 转写中
- **ERR**(红色 2 秒)— 失败,再按 START 重试
### 推荐安装 — API 轨
语音功能在服务器端需要**两把 API key**。两把加起来日常使用月成本几美分:
| Key | 申请地址 | 用途 | 是否必需 |
|---|---|---|---|
| **OpenRouter** | [openrouter.ai/settings/keys](https://openrouter.ai/settings/keys) | Whisper Large V3 Turbo(语音→文字)| **必需**——语音 IME (START) 与 AI 问答 (L+START) 都用 |
| **DeepSeek** | [platform.deepseek.com/api_keys](https://platform.deepseek.com/api_keys) | DeepSeek-Chat(AI 回答)| 可选——只有 L+START AI 问答用;语音 IME 不需要 |
费用:OpenRouter Whisper Turbo `$0.04 / 音频小时`(个人用一个月几美分);
DeepSeek-Chat 单次问答 `~$0.0001`。两边注册都送少量免费额度,足够上手。
#### 一条命令安装
SSH 进服务器后:
```bash
git clone https://github.com/Fishason/DSSH.git ~/dssh-repo
bash ~/dssh-repo/tools/install_whisper_api.sh
```
安装脚本会**交互式**地依次提示输入两把 key:
```
▶ checking prerequisites...
✓ python3 3.10
▶ installing daemon + shim + dssh-whisper CLI ...
▶ writing track config...
Need your OpenRouter API key (https://openrouter.ai/settings/keys).
Used for Whisper transcription. Looks like: sk-or-v1-...
Paste your OpenRouter key (or empty to skip): █
✓ api-key saved at /home/you/.config/dssh-whisper/api-key (chmod 0600)
Optional: DeepSeek API key (https://platform.deepseek.com/api_keys).
Used only by the L+START voice AI-ask modal — skip if you only
want plain voice IME. Looks like: sk-...
Paste your DeepSeek key (or empty to skip): █
✓ deepseek-key saved at /home/you/.config/dssh-whisper/deepseek-key (chmod 0600)
✓ dssh-whisper-api installed (lightweight, OpenRouter Whisper Turbo).
```
**非交互式**安装(适合 CI / Ansible / 重装):
```bash
OPENROUTER_API_KEY="sk-or-v1-..." \
DEEPSEEK_API_KEY="sk-..." \
bash ~/dssh-repo/tools/install_whisper_api.sh
```
#### 验证安装
```bash
$ dssh-whisper status
active track: api
(API-only install — no local daemon)
api-key: ✓ /home/you/.config/dssh-whisper/api-key
```
3DS 每次按 START 都通过 SSH-exec 调用 `~/.local/bin/dssh-whisper-shim`;
服务器端**没有任何后续操作**——装一次终身使用。
#### 安装后服务器上的文件
```
~/.config/dssh-whisper/
├── track # "api" 或 "local"
├── api-key # chmod 0600 — OpenRouter
└── deepseek-key # chmod 0600 — DeepSeek(可选)
~/.local/bin/
├── dssh-whisper # CLI(start/stop/status/switch/uninstall)
└── dssh-whisper-shim # 3DS 通过 SSH-exec 调这个
~/.local/share/dssh-whisper/
└── whisper_shim.py # 实际跟两个 API 通信的 Python 脚本
```
总占用:~30 KB。无 daemon、无模型、无需运维。
#### 后续轮换 key
key 泄漏 / 额度用完 / 换 provider 时直接覆写文件:
```bash
echo 'sk-or-v1-NEW_KEY' > ~/.config/dssh-whisper/api-key
chmod 0600 ~/.config/dssh-whisper/api-key
# 不需重启任何服务——shim 每次调用都重新读 key
```
| 项 | 值 |
|---|---|
| 推理 | OpenRouter Whisper Turbo(云端)+ DeepSeek-Chat(云端)|
| 4 秒音频延迟 | ~1-2 秒(STT),AI 问答 +1-2 秒(LLM)|
| 费用 | $0.04 / 音频小时 + ~$0.0001 / AI 问答 |
| 服务器占用 | ~30 KB |
| 服务器 CPU 负载 | 几乎零 |
| 是否需联网 | 是(HTTPS 到 openrouter.ai + api.deepseek.com)|
99% 用户用 API 轨即可——装好、按 START、收工。
### `dssh-whisper` CLI
```
dssh-whisper status # 当前 track + daemon 状态 + key 是否在
dssh-whisper switch # toggle api ↔ local
dssh-whisper switch [api|local] # 显式指定
dssh-whisper start # 启动本地 daemon(仅双轨装态有效)
dssh-whisper stop | close # 停本地 daemon
dssh-whisper restart
dssh-whisper logs [-f] # tail systemd-user 日志
dssh-whisper uninstall # 全卸载
```
API-only 装态下 daemon 相关命令会优雅退化(打印 "no daemon to ...",非致命)。
### 快速 AI 问答(L + START)— v1.1
在 yazi 里忘了快捷键?想要一行 vim 正则?按住 **L** 同时按 **START**
—— 这下您是在问 AI、不是给 SSH 输入。底屏弹出一个问答 modal,顶屏的
SSH 会话完全不动。
| 在窗口里 | 效果 |
|---|---|
| **A** | 关闭,**保留**这条 Q&A 进 history;下次 L+START 接续上下文 |
| **B** 或下屏**任意触屏** | 关闭,**清空** history;下次 L+START 是全新对话 |
History 上限 5 轮;连续按 A 超过 5 轮会按 FIFO 丢掉最旧的一轮。
模型是 `deepseek-chat`(DeepSeek 官方 API,不经 OpenRouter)—— 选它
是因为响应快(~1-2 秒)+ 单价极低(个人用一次 ~$0.0001)。回答通常 6-15
句,长答案在窗口里自动换行,超长尾部用 `...` 截。
#### Modal 中的 markdown 渲染
DeepSeek 默认会用 markdown 排版。modal 把常见格式渲染成颜色,**不是**
显示原始符号:
| Markdown 源 | modal 渲染 |
|---|---|
| `# 标题` / `## 标题` / `### 标题` | 黄色强调,`#` 隐去 |
| `` `行内代码` `` | 青色,反引号隐去 |
| ` ``` …代码块… ``` ` | 青色多行,三反引号围栏隐去 |
| `- 项` / `* 项` | `• 项`(灰色项目符号 + 普通色内容)|
| `**加粗**` / `*斜体*` | 普通色 — syntax 隐去,无加粗 / 斜体效果(位图字体撑不起字形变化)|
| `[链接文字](url)` | `链接文字` — URL 丢弃 |
| `~~删除线~~` | 普通色 — syntax 隐去 |
#### DeepSeek key
需要单独配置一把 **DeepSeek API key**(在
[platform.deepseek.com/api_keys](https://platform.deepseek.com/api_keys)
申请);安装脚本在 OpenRouter key 之外会再提示这把(可跳过——纯语音
IME 不需要它)。存放在 `~/.config/dssh-whisper/deepseek-key` chmod 0600
——如需轮换见上面 [后续轮换 key](#后续轮换-key) 段。
### 注意事项
- 3DS 一次录音上限 ~32 秒(1 MB mic buffer 在 16 kHz × 16-bit 下满)。
随时按 **START** 提前结束。
- 退出 DSSH 用 **HOME** 键(不是 START)—— START 已绑定语音输入。
- `~/.config/dssh-whisper/` 已在 `.gitignore` 里;轮换 API key 一行即可:
`echo 'sk-or-v1-NEW' > ~/.config/dssh-whisper/api-key`
- 3DS 端通过 libssh2 exec 调 `~/.local/bin/dssh-whisper-shim`,shim 读
当前 track 配置后分发——**切换轨道无需重启 3DS 或 SSH 会话**。
### 进阶 — 双轨(自部署,⚠️ 不建议)
> ⚠️ **请注意**:自部署轨道会把 `whisper-small` 模型加载进内存(约 1 GB
> 常驻),每次录音都跑一次 CPU 推理。在 2-vCPU 的 AWS t3.medium 上、
> 同时跑 VS Code Remote / claude-code / tmux / chrome-devtools-mcp 时,
> 转写 4 秒音频耗时 **~40 秒**——同一时刻走 OpenRouter 仅 **~1.5 秒**。
> 由于 API 费用极低($0.04/*音频*小时 ≈ 个人用一个月几美分),除非
> 您有强需求把音频留在本地,否则**强烈建议走云端**。
>
> 如果您还是要装本地轨,请确保服务器:
>
> - **4+ 颗空闲 vCPU**(≥3 GHz)——再低 3DS 上的 spinner 会让人难受
> - **2+ GB 空闲内存**给 small.zh 模型 + buffer
> - **转写时刻没有别的吃 CPU 的进程**(vscode、ide、playwright 等)
> - **~600 MB 磁盘**给模型 + venv
如果您确实要离线部署,仓库里的双轨 installer 可以让 API 和本地共存,
随时切换:
```bash
git clone https://github.com/Fishason/DSSH.git ~/dssh-repo
bash ~/dssh-repo/tools/install_whisper_dual.sh
```
双轨装好后**默认仍用 API**,需要时再切:
```bash
dssh-whisper switch local # 下次按 START → 本地 whisper.cpp
dssh-whisper switch api # 下次按 START → OpenRouter(默认)
dssh-whisper switch # 不带参数即 toggle
```
---
## 按键说明
### 物理键
| 键 | 功能 | 备注 |
|---|---|---|
| **A** | Enter(EN)/ **拼音 buffer 当英文直出**(IME) | CN 模式下不小心打了英文?按 A 直接把 buffer 作为 ASCII 发出去并清空,不用切换模式重打 |
| **B** | Backspace / 消拼音 buffer | hold-style 自动重复(最快 60/秒) |
| **X** | Alt 修饰 | hold-style,按住时下次按键加 Alt |
| **Y** | Ctrl 修饰 | hold-style,按住 Y + 点 c → Ctrl-C |
| **L** | Shift 修饰 / **+ Circle Pad → 切右窗格** | 见下面 [tmux 窗格滚动](#tmux-分屏滚动) |
| **R** | 切换中/英输入模式 | 右上 ENG/CHN 显示当前模式 |
| **SELECT** | Esc | 单点立即发 |
| **START** | **语音输入开关** | 一按开始录音、再按结束并转写。详见 [语音输入](#语音输入)。 |
| **Space**(软键盘)| 普通空格(EN)/ **提交当前高亮候选**(IME) | 跟 sogou/fcitx 一致 |
| **Shift + .** | **。**(中文句号 U+3002) | 不论 EN/CN 模式都生效 |
| **D-pad ↑↓** | 方向键 / IME 翻页 | IME buffer 非空时翻候选页 |
| **D-pad ←→** | 方向键 / IME 选词 | IME buffer 非空时移动候选游标 |
| **Circle Pad ↑↓** | 滚动 scrollback / tmux mouse-wheel | 默认目标左/上窗格,**L 按住 → 右/下窗格** |
> 长按 D-pad 或 B 键:250ms 启动重复,0.5s 后 12/秒,1.5s 后冲到 60/秒。
### tmux 分屏滚动
3DS 没有真正的鼠标光标,tmux 的 mouse-wheel 事件按 (col, row) 路由到对应
窗格。DSSH 默认发 `(1,1)` 命中**左/上窗格**;按住 **L** 时改发 `(60,12)`
命中**右/下窗格**。所以 vertical-split tmux 里:
| 操作 | 效果 |
|---|---|
| Circle Pad ↑↓ | 滚动**左**窗格 |
| L 按住 + Circle Pad ↑↓ | 滚动**右**窗格 |
### 软键盘
下屏占满软键盘,两页:
- **字母页**(默认):QWERTY 布局,含 `,` `.` 标点 + Tab + 大空格
- **符号页**(左下 `123` 切换):1234567890、!@#\$%^&\*() 整齐对齐两行 +
其他常用符号 + 反斜杠
任何键支持 hold-style 修饰组合,例如:**按住 Y + 点 b** = `Ctrl-B`
(tmux prefix)。
### 状态条(顶部 30 px)
```
┌──────────────────────────────────────────────────────┐
│ [SFT] 候选条 / 拼音 buffer / IME 候选词 [CHN] │
└──────────────────────────────────────────────────────┘
```
- **左槽 [STA]**:3 字母指示当前修饰键(SFT/CTL/ALT 持续亮,
ENT/BSP/ESC/`R→C` 闪 200ms)
- **中段**:CN 模式拼音 buffer + 候选词;EN 模式空白
- **右槽 [ENG/CHN]**:当前 IME 模式,**双击此处** 进入 debug 页面
---
## 输入法用法
### 全拼
CN 模式下按字母键 → 顶部候选条出现拼音 + 候选:
```
ni → 年 你 牛奶 娘 念 (page 1/52, total 256)
nihao → 你好 你好吗 你好啊 拟好 你好呀
shijie → 世界 世界上 世界杯 世界各地 世界里
```
- **A 键** 或 **空格键** 提交当前高亮候选
- **D-pad ←→** 移动高亮(同一页内)
- **D-pad ↑↓** 翻页
- **触屏** 直接点候选词提交
- **B 键** 删拼音 buffer 一个字母
### 缩写(声母)
每个多音节词都额外有一条声母拼音条目,**输入声母也能召回**(频率打 0.3
折,所以全拼仍排在前):
```
nh → 你好(在第 8 个候选)
wm → 我们(首个)
sj → 世界
zw → 中文
xx → 谢谢
```
按 D-pad ↓ 翻页或 ←→ 移动游标找到目标后 A 键提交。
### Prefix 回退
输错字母多打了一个?引擎自动用最长有效前缀匹配,红色显示多出的部分:
```
buffer: niha[oz] ← 绿色 niha + 红色 oz
候选: 依旧显示 nihao 的结果
```
按 B 键消掉红色尾巴回到正常。
### 修饰键不被 IME 拦截
CN 模式下按住 **Y + c** 仍然发 `Ctrl-C`,按住 **L + a** 仍然发 `A`。修饰
键优先于 IME 路由,跑 vim/tmux/claude-code 不受影响。
### 误打英文逃生:A 键直出
CN 模式 + 拼音 buffer 非空时,按 **A** 把 buffer 当英文 ASCII 发到 SSH
并清空。例如不小心在 CN 模式打了 `cd /etc`:候选条会显示一堆奇怪中文,
此时按 **A** 一次,SSH 直接收到 `cd /etc`,不用退格再切模式重打。
> 区别:**Space** 提交高亮候选(中文上屏),**A** 直接把字母原样发出去。
---
## Debug 页面
**右上 ENG/CHN 双击**(500 ms 内点两次)→ 进入 debug 页面:
- 标题 + 退出提示(再单击 CHN/ENG 即退)
- **recv hex**:最近 32 字节 SSH 接收数据,用来排查 ANSI 协议问题
- **物理键绑定列表**:上述按键说明的速查
- **MASCOT: ON/OFF** 按钮:关掉螃蟹(默认 ON)
---
## 从源码构建
### 依赖
- Linux x86\_64(在 Ubuntu 22.04 测过;其他发行版需要相应调整)
- [devkitPro / devkitARM](https://devkitpro.org/wiki/Getting_Started)
release 65+,GCC 14.2.0
- Python 3.10+ + Pillow(生成字体 + 词典)
- ImageMagick(可选;图标处理用 Pillow 也可)
### 步骤
```bash
# 1. 装 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 + 进项目
git clone https://github.com/Fishason/DSSH.git
cd DSSH
# 3. 交叉编译 libssh2(一次性,输出到 $DEVKITPRO/portlibs/3ds/lib/)
bash build-libssh2.sh
# 4. 装系统字体(Terminus 提供 ASCII / box-drawing)
sudo apt install fonts-terminus
# 5. 下载字体源(Zpix)
bash tools/fetch_fonts.sh
# 6. 生成字体 atlas(→ source/font_data.c,~3 MB)
python3 tools/gen_font.py
# 7. 下载 + 生成拼音词典(→ romfs/pinyin_dict.bin,~13 MB)
bash tools/fetch_pinyin_dict.sh
python3 tools/gen_pinyin_dict.py
# 8. 编译 .3dsx
make
# 9. (可选)打 .cia
bash tools/install_cia_tools.sh # 装 bannertool + makerom 到 ~/bin
make cia # 输出 DSSH.cia
```
### 测试 IME 引擎(host 端,不需 3DS)
```bash
make test-ime
```
会编译 `tools/test_ime.c` 链 `source/ime_pinyin.c`,跑 9 个常用查询用例
(`ni→你`,`nihao→你好`,`nh→你好`,等等)。
---
## 项目结构
```
DSSH/
├── 69633.PNG # 源图标 (162×102)
├── icon.png # .3dsx / SMDH 图标 (48×48,从源图生成)
├── app.rsf # makerom CIA spec
├── Makefile # 主构建(make / make cia / make test-ime)
├── build-libssh2.sh # libssh2 + mbedTLS ARM 交叉编译
├── source/
│ ├── main.c # 主循环、SSH receive、UTF-8 边界
│ ├── ssh_client.{c,h} # libssh2 封装
│ ├── config.{c,h} # SD 卡 config.ini 解析
│ ├── terminal.{c,h} # ANSI/VT100 解析器(fork skmtrd)
│ ├── renderer.{c,h} # citro2d 渲染(终端、文本、CJK)
│ ├── keyboard.{c,h} # 物理按键 + IME 路由
│ ├── softkb.{c,h} # 软键盘 + 候选条 + debug 页面
│ ├── ime_pinyin.{c,h} # 拼音引擎
│ ├── mascot.{c,h} # 螃蟹吉祥物
│ ├── font_atlas.{c,h} # 字体索引
│ └── font_data.c # 字体位图(gen_font.py 生成)
├── tools/
│ ├── fetch_fonts.sh # 下载 Zpix
│ ├── gen_font.py # 字体 atlas 生成器
│ ├── fetch_pinyin_dict.sh # 下载 rime-ice
│ ├── gen_pinyin_dict.py # 词典 → 二进制
│ ├── test_ime.{c,sh} # host 端 IME 测试
│ ├── gen_cia_assets.py # 图标/banner 派生
│ └── install_cia_tools.sh # bannertool + makerom 安装
├── romfs/ # gitignored — 装载 pinyin_dict.bin
├── data/ # gitignored — 字体源 + 词典源
└── sd_template/ # SD 卡部署模板
├── README.md
└── 3ds/3dssh/config.ini.example
```
## 架构概览
```
SSH server (somewhere on the internet)
▲ libssh2 over mbedTLS-RSA-4096
│
┌────┴──────────────────────────────────────────────────┐
│ main.c poll loop @60fps │
│ ├─ 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)
```
详细 milestone 进度参见 commit history(M0 → M9 全部提交)。
---
## 致谢
- **[skmtrd/3dssh](https://github.com/skmtrd/3dssh)** — 原日文版,复用了
ANSI/VT100 解析器、UTF-8 边界处理、citro2d 框架基础
- **[rime-ice](https://github.com/iDvel/rime-ice)** — 拼音词典源
(commit `3f57a6f6` pin)
- **[Zpix Pixel Font](https://github.com/SolidZORO/zpix-pixel-font)** —
12px CJK 像素字体(OFL 1.1)
- **[Terminus TTF](https://terminus-font.sourceforge.net/)** — ASCII /
box-drawing 像素字体
- **[libssh2](https://www.libssh2.org/)** + **[mbedTLS](https://www.trustedfirmware.org/projects/mbed-tls/)** —
SSH/TLS 协议栈
- **[devkitPro](https://devkitpro.org/) libctru / citro2d / citro3d** —
3DS 用户态运行时 + 渲染
- **[carstene1ns/3ds-bannertool](https://github.com/carstene1ns/3ds-bannertool)**
+ **[3DSGuy/Project_CTR makerom](https://github.com/3DSGuy/Project_CTR)** —
CIA 打包工具
## License
MIT — 见 [LICENSE](LICENSE)。
字体、词典、上游 SSH/TLS 库各有各的许可证(OFL / GPL / BSD / MIT),
分发二进制时请遵守对应条款。