Build and deploy 小貓中文 (FreeDOS, self-hosted js-dos)
Browse files- README.md +20 -8
- dev/HANDOFF.md +29 -19
- dev/xiaomao-dev.zip +2 -2
- dos.b64.js +0 -0
- index.html +4 -0
README.md
CHANGED
|
@@ -10,20 +10,32 @@ sdk: static
|
|
| 10 |
|
| 11 |
DOS 常駐(TSR)的 Big5 中文顯示與注音輸入外掛,在瀏覽器(含手機)中以 FreeDOS + js-dos 6.22 執行。載入後回到一般 DOS 提示字元,DOS 指令照常使用,並可直接輸入、顯示中文。全部元件皆為開源,建置時由上游下載並驗證雜湊。
|
| 12 |
|
| 13 |
-
## 運作方式
|
| 14 |
|
| 15 |
-
-
|
| 16 |
-
-
|
| 17 |
-
-
|
| 18 |
-
-
|
| 19 |
-
- INT 2Fh `AX=D7A0h` 防
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
## 操作
|
| 22 |
|
| 23 |
- 點選 DOS 畫面後使用實體鍵盤,或用畫面下方的虛擬鍵盤(含 Shift、`\`、`:`、Tab)。
|
| 24 |
-
- `Ctrl+Space` 切換注音/英數(網頁有按鈕)。
|
| 25 |
- 注音採倚天鍵位;輸入音節後按聲調鍵(`6 3 4 7`)或空白鍵出候選字,按 `1`–`9` 或空白鍵選字。
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
## 一鍵建置與部署
|
| 29 |
|
|
|
|
| 10 |
|
| 11 |
DOS 常駐(TSR)的 Big5 中文顯示與注音輸入外掛,在瀏覽器(含手機)中以 FreeDOS + js-dos 6.22 執行。載入後回到一般 DOS 提示字元,DOS 指令照常使用,並可直接輸入、顯示中文。全部元件皆為開源,建置時由上游下載並驗證雜湊。
|
| 12 |
|
| 13 |
+
## 運作方式(傳統中文系統作法)
|
| 14 |
|
| 15 |
+
- 程式看到的仍是 80×25 文字模式:文字畫面保存在 `B800:0000`(影子文字緩衝區),所以 **PE2 這類直接寫顯示記憶體的程式**與使用 INT 10h 的程式都能照常運作。
|
| 16 |
+
- 實際顯示是 640×400 的 16 色繪圖模式;常駐程式把影子文字畫成像素:英數用 ROM 8×16 字型,Big5 雙位元組用放在 XMS 的 16×16 字型。**畫面上不同漢字數量沒有上限**(整頁約 900 個不同漢字已測過)。
|
| 17 |
+
- 攔截 INT 10h:完整模擬文字模式 BIOS(模式設定、游標、捲動、讀寫字元、TTY、字串);程式要求繪圖模式時(例如 13h)直接交給 BIOS,回到文字模式時自動恢復。
|
| 18 |
+
- 攔截 INT 16h:`Ctrl+Space` 切換注音/英數,選好的字以兩個按鍵位元組交給程式,同時是畫面重繪的觸發點;注音候選列畫在最底列,用完即還原。
|
| 19 |
+
- `Ctrl+\` 切換 Big5 顯示(遇到使用 CP437 畫框字元的程式時關閉)。INT 2Fh `AX=D7A0h` 防重複載入。
|
| 20 |
+
- 常駐約 15 KB 傳統記憶體;字型與詞庫在 XMS(FreeDOS 開機片以 HimemX 提供)。載入時(及再次執行 `XIAOMAO`)顯示常駐大小、可用傳統記憶體與 XMS(即時 MEM)。
|
| 21 |
+
|
| 22 |
+
## 附加軟體(開機片內)
|
| 23 |
+
|
| 24 |
+
- `XEDIT.EXE`:仿 PE2 的全螢幕文字編輯器,直接寫 `B800` 並用 INT 10h/16h,可編輯中文(`XEDIT DEMO.TXT`;F2 存檔、F3/Esc 離開)。
|
| 25 |
+
- FreeDOS:`COMMAND.COM`(FreeCOM)、`MEM`、`MORE`、`XCOPY`、`ATTRIB`、`DELTREE`;`HIMEMX.EXE`(XMS)。
|
| 26 |
+
- `DEMO.TXT`:中文示範文字(`TYPE DEMO.TXT`)。
|
| 27 |
|
| 28 |
## 操作
|
| 29 |
|
| 30 |
- 點選 DOS 畫面後使用實體鍵盤,或用畫面下方的虛擬鍵盤(含 Shift、`\`、`:`、Tab)。
|
| 31 |
+
- `Ctrl+Space` 切換注音/英數(網頁有按鈕);`Ctrl+\` 切換 Big5 顯示(網頁有按鈕)。
|
| 32 |
- 注音採倚天鍵位;輸入音節後按聲調鍵(`6 3 4 7`)或空白鍵出候選字,按 `1`–`9` 或空白鍵選字。
|
| 33 |
+
|
| 34 |
+
## 限制
|
| 35 |
+
|
| 36 |
+
- 不支援直接改寫 CRTC/VGA 暫存器做文字模式特效(游標位置暫存器除外)、載入自訂字型、閃爍屬性(以高亮背景代替)。
|
| 37 |
+
- 文字畫面以約 18 次/秒更新(隨計時器),極快速的畫面動畫會有延遲。
|
| 38 |
+
- 只有注音輸入法,候選字最多 9 個。
|
| 39 |
|
| 40 |
## 一鍵建置與部署
|
| 41 |
|
dev/HANDOFF.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
# 小貓中文 — 接手說明 (HANDOFF)
|
| 2 |
|
| 3 |
目標:DOS 常駐 (TSR) Big5 中文顯示 + 注音輸入,跑在 js-dos 6.22(瀏覽器/手機),部署為 HF static Space。
|
| 4 |
-
傳統記憶體越少越好(現況常駐約
|
| 5 |
|
| 6 |
## 一鍵流程
|
| 7 |
`HF_TOKEN=... HF_SPACE=user/space ./build.sh`(兩者是腳本開頭變數,token 不入檔)。空目錄也能跑:自動建 Python venv、下載並以雜湊驗證
|
|
@@ -12,31 +12,41 @@ Open Watcom 1.9、JWasm+HimemX 原始碼(現場編成 HIMEMX.EXE)、GNU Unif
|
|
| 12 |
- FreeDOS 開機 → `FDCONFIG.SYS` 載 HIMEMX → `FDAUTO.BAT` 載 XIAOMAO.EXE → 回到 `A:\>`;顯示 MEM;`Ctrl+Space` 注音;候選列在最底列;Backspace 刪整個中文字。
|
| 13 |
- 手機尺寸(390x844)+觸控虛擬鍵盤驗證過;DOSBox 核心與開機片都自架,不依賴外部 CDN。
|
| 14 |
|
| 15 |
-
## 架構
|
| 16 |
-
- `src/xiaomao.c`:TSR。`
|
| 17 |
-
- `src/xms.asm` (NASM OMF):XMS 包裝、
|
| 18 |
-
helper 只能改 AX;Watcom 暫存器呼叫 AX,DX,BX,CX;large model 用 `retf`;符號後綴 `_`。
|
| 19 |
-
- 顯示:
|
| 20 |
-
字
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
- 資料:`FONT.DAT`("ETNF" 18 byte 頭, ASCII 256x16, CJK 32 byte/字, 索引 (lead-0xA1)*157+trail索引) 與 `IME.DAT`("ETNI", 12 byte 頭, 5 byte 記錄 key/offset/count),載入 XMS。
|
| 23 |
-
key=(((ini*4+med)*14+fin)*5+tone);XMS 搬移長度必須為偶數。
|
| 24 |
-
- 常駐大小:build.sh 兩階段編譯,由連結 map 的 STACK 段位址 +0x10 得 `TSR_PARAS`;init 用大緩衝放堆疊(不常駐)。
|
| 25 |
- 防重複載入 INT2F AX=D7A0h→584Dh;再次執行 XIAOMAO 只顯示 MEM。
|
|
|
|
| 26 |
- 網頁:`index.html`。HF 對二進位檔回 302 無 CORS,所以 `emu.b64.js`(wdosbox.js+wasm 的 base64) 與 `dos.b64.js`(開機片 zip 的 base64) 以 Blob URL 供 js-dos;
|
| 27 |
-
|
| 28 |
-
- FreeDOS 開機片:取 FD14 `144m/x86BOOT.img`,刪除安裝用檔,換 `dos/FDCONFIG.SYS`、`dos/FDAUTO.BAT`(CRLF 由 build.sh 轉)。FreeDOS 主控台輸出
|
| 29 |
|
| 30 |
## 編譯注意
|
| 31 |
Open Watcom 1.9:`wcl -bt=dos -ml -0 -os -s -zu -k6144`(`-s` 必要:私有堆疊上不可堆疊檢查)。無 `_AX` 偽暫存器;`_psp` 需自行 `extern unsigned _psp;`。
|
| 32 |
|
| 33 |
## 測試方式
|
| 34 |
-
無本機 DOS 模擬器:
|
| 35 |
-
|
|
|
|
| 36 |
|
| 37 |
## 已知限制 / 待辦
|
| 38 |
-
1. 直接
|
| 39 |
-
2.
|
| 40 |
-
3. 輸入法只有注音;候選限 9 個無翻頁;無詞組/聯想。
|
| 41 |
-
4.
|
| 42 |
-
5.
|
|
|
|
|
|
| 1 |
# 小貓中文 — 接手說明 (HANDOFF)
|
| 2 |
|
| 3 |
目標:DOS 常駐 (TSR) Big5 中文顯示 + 注音輸入,跑在 js-dos 6.22(瀏覽器/手機),部署為 HF static Space。
|
| 4 |
+
傳統記憶體越少越好(現況常駐約 15.6 KB,不強制 600K)。授權 GPL-3.0-only。全部元件開源,不含專有軟體。
|
| 5 |
|
| 6 |
## 一鍵流程
|
| 7 |
`HF_TOKEN=... HF_SPACE=user/space ./build.sh`(兩者是腳本開頭變數,token 不入檔)。空目錄也能跑:自動建 Python venv、下載並以雜湊驗證
|
|
|
|
| 12 |
- FreeDOS 開機 → `FDCONFIG.SYS` 載 HIMEMX → `FDAUTO.BAT` 載 XIAOMAO.EXE → 回到 `A:\>`;顯示 MEM;`Ctrl+Space` 注音;候選列在最底列;Backspace 刪整個中文字。
|
| 13 |
- 手機尺寸(390x844)+觸控虛擬鍵盤驗證過;DOSBox 核心與開機片都自架,不依賴外部 CDN。
|
| 14 |
|
| 15 |
+
## 架構(傳統「中文系統」作法:文字影子 + 繪圖顯示)
|
| 16 |
+
- `src/xiaomao.c`:TSR。`video_service`(INT10 文字模式 BIOS 模擬)、`refresh`(渲染器)、`key_service`(INT16 + 注音)、載入器 `main`。
|
| 17 |
+
- `src/xms.asm` (NASM OMF):XMS 包裝、INT10/16/2F 掛勾(私有 2 KB 堆疊在 _TEXT,SS=CS;INT10 掛勾把暫存器框架以遠指標交給 C)、`bios10_call`、`get_rom_font`(INT10 AX=1130h)、`cpu_idle`。
|
| 18 |
+
helper 只能改 AX;Watcom 暫存器呼叫 AX,DX,BX,CX(第二參數在 DX);large model 用 `retf`;符號後綴 `_`。
|
| 19 |
+
- 顯示:BIOS 模式 12h(640x480x16),CRTC 垂直顯示結束改 399(只顯示 400 線=25 列x16)。程式眼中仍是模式 3:BDA(0040:xx)被改寫成模式 3/80 欄/25 列。
|
| 20 |
+
影子文字在 `B800:0000`;畫面像素在平面偏移 0000-7CFF,經 A000 視窗寫入。渲染器每 tick(INT16 輪詢、阻塞前強制)逐列比對影子與「已繪副本」(`B800:6000` 字元/屬性、`B800:7000` 種類),只重畫變動的格子。
|
| 21 |
+
Big5 偵測:前導 A1-F9 + 後隨 40-7E/A1-FE(`Ctrl+\` 可關);漢字 16x16 由 XMS 取(64 項快取),ASCII 用 ROM 字型。游標以底線疊畫,BDA/CRTC 游標暫存器皆會同步。
|
| 22 |
+
- **DOSBox 的 VGA 陷阱(都已實測,別再踩)**:
|
| 23 |
+
1. 模式 12h 下 `B800` 不是 RAM;GC 雜項暫存器(GC6) map=3(B8000) 會讓 DOSBox 變成 CGA 模式。
|
| 24 |
+
2. 解法:GC6=0x01(A0000 起 128KB 視窗)。此時 `B800:0000` 對應平面偏移 0x8000(寫入 map mask=0Fh、讀 plane 0,行為如 RAM),與 A000 低 32 KB 的畫面互不重疊,且完全不需切換視窗。
|
| 25 |
+
3. 寫模式 2 的背景填色在 DOSBox 無效;改用寫模式 0 + Set/Reset(GC0=色、GC1=0F、GC8=位元遮罩)。
|
| 26 |
+
4. 部分遮罩寫入前必須「真的讀一次」同位址以載入 latch(用 volatile sink 變數,否則編譯器會丟掉讀取,未遮罩位元會帶入垃圾)。
|
| 27 |
+
5. 畫完一列必須還原 GC1=0、GC8=FF、GC0=0,否則程式之後寫 B800 會被 Set/Reset 污染。
|
| 28 |
+
- 輸入:INT16 AH=0/1/0x10/0x11 掛勾 → IME 狀態機 → Big5 兩位元組入 16 字佇列。BS 加倍(一次退兩格)只在 DOS 內部讀鍵時(InDOS≠0)啟用,直接呼叫 INT16 的程式(PE2/XEDIT)收到單一 BS。
|
| 29 |
- 資料:`FONT.DAT`("ETNF" 18 byte 頭, ASCII 256x16, CJK 32 byte/字, 索引 (lead-0xA1)*157+trail索引) 與 `IME.DAT`("ETNI", 12 byte 頭, 5 byte 記錄 key/offset/count),載入 XMS。
|
| 30 |
+
key=(((ini*4+med)*14+fin)*5+tone);XMS 搬移長度必須為偶數;XMS 只在安全點(INT16/INT10 內)呼叫,不在計時器 ISR。
|
| 31 |
+
- 常駐大小:build.sh 兩階段編譯,由連結 map 的 STACK 段位址 +0x10 得 `TSR_PARAS`;init 用大緩衝(含 4000 B 舊畫面)放堆疊(`-k10240`,不常駐)。
|
| 32 |
- 防重複載入 INT2F AX=D7A0h→584Dh;再次執行 XIAOMAO 只顯示 MEM。
|
| 33 |
+
- 附加軟體:`src/addons/xedit.c`(PE2 風格直接寫 B800 的編輯器,用來驗證直接寫顯��記憶體相容性)。
|
| 34 |
- 網頁:`index.html`。HF 對二進位檔回 302 無 CORS,所以 `emu.b64.js`(wdosbox.js+wasm 的 base64) 與 `dos.b64.js`(開機片 zip 的 base64) 以 Blob URL 供 js-dos;
|
| 35 |
+
monkeypatch `XMLHttpRequest.prototype.open` 把 `wdosbox.js`/`wdosbox.wasm.js` 對應到 Blob URL(js-dos 以 `.js`→`.wasm.js` 推導 wasm 檔名)。
|
| 36 |
+
- FreeDOS 開機片:取 FD14 `144m/x86BOOT.img`,刪除安裝用檔,換 `dos/FDCONFIG.SYS`、`dos/FDAUTO.BAT`(CRLF 由 build.sh 轉)。FreeDOS 主控台輸出經 INT 10h TTY(已驗證)。
|
| 37 |
|
| 38 |
## 編譯注意
|
| 39 |
Open Watcom 1.9:`wcl -bt=dos -ml -0 -os -s -zu -k6144`(`-s` 必要:私有堆疊上不可堆疊檢查)。無 `_AX` 偽暫存器;`_psp` 需自行 `extern unsigned _psp;`。
|
| 40 |
|
| 41 |
## 測試方式
|
| 42 |
+
無本機 DOS 模擬器:`python3 tests/smoke.py --dist dist --chromium <chrome>`(Playwright,截圖到 /tmp/xm-shots,只做粗略像素斷言;請人工看截圖)。
|
| 43 |
+
自己寫測試 EXE 時:用 build 裡的 Open Watcom 編譯,放進映像(參考 build.sh 的組映像段),改 `dos/FDAUTO.BAT` 即可。
|
| 44 |
+
Chromium 旗標:`--no-sandbox --disable-gpu --disable-dev-shm-usage`。截圖檔寫入有延遲,讀檔前稍等。
|
| 45 |
|
| 46 |
## 已知限制 / 待辦
|
| 47 |
+
1. 不支援程式直接改 CRTC/VGA 暫存器做文字特效(游標暫存器除外)、載入文字模式字型、閃爍屬性。
|
| 48 |
+
2. 渲染為 tick 驅動(約 18 fps);可考慮在 INT10 寫入後立即小範圍重畫,或重新程式化 PIT 提高更新率。
|
| 49 |
+
3. 輸入法只有注音;候選限 9 個無翻頁;無詞組/聯想;可加倚天/倉頡/大易。
|
| 50 |
+
4. Big5 與 CP437 畫框字元衝突只能用 `Ctrl+\` 手動切換;可加自動偵測。
|
| 51 |
+
5. 可加 JEMM386/UMB 讓 TSR 載入高記憶體(現約 15 KB 傳統記憶體)。
|
| 52 |
+
6. 自動化測試只有 smoke test;可加截圖比對基準。
|
dev/xiaomao-dev.zip
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d159ee237190b9a30ace795167758842b46d095b5a78e4bf881cb665bf44c148
|
| 3 |
+
size 49837
|
dos.b64.js
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
index.html
CHANGED
|
@@ -45,6 +45,7 @@
|
|
| 45 |
<div id="screen-wrap"><canvas id="dosbox" width="640" height="480" aria-label="DOS emulator screen"></canvas></div>
|
| 46 |
<div class="toolbar">
|
| 47 |
<button id="ime-toggle" type="button">Ctrl+Space 注音切換</button>
|
|
|
|
| 48 |
<button id="fullscreen" type="button">全螢幕</button>
|
| 49 |
<span class="spacer"></span>
|
| 50 |
<span id="message" role="status">正在載入 FreeDOS…</span>
|
|
@@ -92,6 +93,9 @@
|
|
| 92 |
};
|
| 93 |
window.etImeToggle = toggleIme;
|
| 94 |
document.getElementById('ime-toggle').addEventListener('click', toggleIme);
|
|
|
|
|
|
|
|
|
|
| 95 |
document.getElementById('fullscreen').addEventListener('click', () => controller && controller.fullscreen());
|
| 96 |
document.getElementById('alt-x').addEventListener('click', () => {
|
| 97 |
send(18, true); tap(88); window.setTimeout(() => send(18, false), 130);
|
|
|
|
| 45 |
<div id="screen-wrap"><canvas id="dosbox" width="640" height="480" aria-label="DOS emulator screen"></canvas></div>
|
| 46 |
<div class="toolbar">
|
| 47 |
<button id="ime-toggle" type="button">Ctrl+Space 注音切換</button>
|
| 48 |
+
<button id="big5-toggle" type="button">Big5 顯示開關</button>
|
| 49 |
<button id="fullscreen" type="button">全螢幕</button>
|
| 50 |
<span class="spacer"></span>
|
| 51 |
<span id="message" role="status">正在載入 FreeDOS…</span>
|
|
|
|
| 93 |
};
|
| 94 |
window.etImeToggle = toggleIme;
|
| 95 |
document.getElementById('ime-toggle').addEventListener('click', toggleIme);
|
| 96 |
+
document.getElementById('big5-toggle').addEventListener('click', () => {
|
| 97 |
+
send(17, true); tap(220); window.setTimeout(() => send(17, false), 130);
|
| 98 |
+
});
|
| 99 |
document.getElementById('fullscreen').addEventListener('click', () => controller && controller.fullscreen());
|
| 100 |
document.getElementById('alt-x').addEventListener('click', () => {
|
| 101 |
send(18, true); tap(88); window.setTimeout(() => send(18, false), 130);
|