HelloSun commited on
Commit
d00bf3f
·
verified ·
1 Parent(s): ef177ba

Build and deploy ETEN DOS demo

Browse files
Files changed (6) hide show
  1. .gitattributes +1 -0
  2. ETEN.img +0 -0
  3. LICENSE +21 -0
  4. README.md +34 -5
  5. dos.zip +0 -0
  6. index.html +145 -138
.gitattributes CHANGED
@@ -35,3 +35,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  dos622.iso filter=lfs diff=lfs merge=lfs -text
37
  wdosbox.wasm.js filter=lfs diff=lfs merge=lfs -text
 
 
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  dos622.iso filter=lfs diff=lfs merge=lfs -text
37
  wdosbox.wasm.js filter=lfs diff=lfs merge=lfs -text
38
+ ETEN.img filter=lfs diff=lfs merge=lfs -text
ETEN.img ADDED
Binary file (132 Bytes). View file
 
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -1,10 +1,39 @@
1
  ---
2
- title: C Dos
3
- emoji: 🌖
4
  colorFrom: blue
5
- colorTo: red
6
  sdk: static
7
- pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: 倚天中文 DOS 6.22 復刻
3
+ emoji: 🖥️
4
  colorFrom: blue
5
+ colorTo: green
6
  sdk: static
 
7
  ---
8
 
9
+ # 倚天中文 DOS 6.22 復刻
10
+
11
+ 以瀏覽器執行 DOS 6.22 與倚天注音中文系統風格的互動示範。DOSBox 由 js-dos 6.22 提供;字型使用 GNU Unifont,詞庫由開放資料產生。
12
+
13
+ ## 操作方式
14
+
15
+ - 點選 DOS 畫面後使用實體鍵盤,或用畫面下方的虛擬鍵盤。
16
+ - `Ctrl+Space` 切換注音/英文輸入。
17
+ - 注音使用標準倚天鍵位;輸入音節後按空白鍵選字,按 `1`–`9` 選候選字。
18
+ - `F1` 查看系統資訊;`Alt+X` 離開。
19
+
20
+ 啟動時會顯示 DOS 狀態列中的傳統記憶體用量,目標為保留 600KB 以上。
21
+
22
+ ## 本機建置
23
+
24
+ 需求:OpenWatcom(預設 `/opt`,可用 `WATCOM` 指定)、NASM、mtools、curl、Python 3,以及可開機的 DOS 6.22 FAT12 映像(預設為 `dos/boot622.img`)。建置工具 `tools/genime.py` 需要 `pypinyin`、`jieba`、`opencc`;`tools/genfont.py` 預設讀取 `/usr/share/unifont/unifont.hex`。
25
+
26
+ ```sh
27
+ HF_TOKEN=hf_... ./build.sh
28
+ ```
29
+
30
+ 若未設定 `HF_TOKEN`,腳本會產生本機發佈檔但略過 Hugging Face 上傳。可用 `DOS_BASE_IMAGE=/path/to/boot.img` 指定其他基底映像。上傳目標為 `HelloSun/c_dos`,token 僅從環境讀取,不寫入專案。
31
+
32
+ `dist/dos.zip` 內含 `ETEN.img`,供頁面中的 js-dos 啟動;`dist/ETEN.img` 可直接用於本機測試。
33
+
34
+ ## 授權與來源
35
+
36
+ - GNU Unifont 字形:GPL-2.0-or-later,附 font embedding exception。GPL-2 授權文字可見 `/usr/share/common-licenses/GPL-2`。
37
+ - js-dos 6.22:使用其發佈檔案,請依上游授權條款使用。
38
+ - DOS 6.22 系統檔案:由使用者提供的 DOS 開機映像取得;建置時移除可選應用程式以騰出磁碟空間。
39
+ - 本專案 C/ASM、HTML 與生成工具:MIT License。
dos.zip ADDED
Binary file (131 Bytes). View file
 
index.html CHANGED
@@ -1,145 +1,152 @@
1
- <!DOCTYPE html>
2
  <html lang="zh-Hant">
3
  <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width,initial-scale=1">
6
- <title>ETen 中文 DOS 系統 · js-dos 6.22 Demo</title>
7
- <link rel="preconnect" href="https://fonts.googleapis.com">
8
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
- <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
10
- <style>
11
- :root{--bg:#000;--fg:#0f0;--amber:#ffb000;--cyan:#0ff}
12
- *{box-sizing:border-box}
13
- body{margin:0;background:var(--bg);color:var(--fg);font-family:'Share Tech Mono',monospace;min-height:100vh;display:flex;flex-direction:column}
14
- header{padding:8px 12px;border-bottom:1px solid #030;background:#010;display:flex;justify-content:space-between;align-items:center;font-size:12px}
15
- header .title{color:var(--amber);font-weight:700}
16
- .mem{color:var(--cyan)}
17
- main{flex:1;display:grid;grid-template-columns:1fr 320px;gap:8px;padding:8px}
18
- @media(max-width:800px){main{grid-template-columns:1fr}}
19
- .term{background:#010;border:1px solid #030;padding:10px;overflow:auto;min-height:360px;font-size:14px;line-height:1.4;white-space:pre-wrap}
20
- .term .prompt{color:var(--amber)}
21
- .term .zh{font-family:'Noto Sans TC',sans-serif;color:#fff}
22
- .side{background:#010;border:1px solid #030;padding:10px;font-size:12px}
23
- .side h3{color:var(--amber);margin:0 0 6px}
24
- .ime-status{display:inline-block;padding:2px 6px;border:1px solid #030;margin:4px 0}
25
- .ime-status.on{background:#030;color:var(--cyan)}
26
- .kbd{background:#111;border:1px solid #333;padding:1px 4px;border-radius:3px}
27
- #dosbox{width:100%;height:240px;background:#000;border:1px solid #030;margin-top:8px}
28
- footer{padding:6px 12px;border-top:1px solid #030;font-size:11px;color:#060}
29
- input#cmd{background:#000;color:var(--fg);border:none;outline:none;font:inherit;width:100%}
30
- </style>
 
 
 
 
 
31
  </head>
32
  <body>
33
- <header>
34
- <div class="title">倚天中文 DOS 6.22 · ETen Chinese System (復刻)</div>
35
- <div class="mem">Conventional Memory: <span id="memfree">624</span> KB free / 640 KB</div>
36
- </header>
37
- <main>
38
- <section class="term" id="term" tabindex="0">
39
- <div>ETen Chinese System v3.5 (demo) — js-dos 6.22</div>
40
- <div>Copyright (C) 1990-1994 E-TEN Information Systems Co.</div>
41
- <div>開源字型:Noto Sans TC (OFL-1.1)</div>
42
- <div>輸入法:注音 (Bopomofo) — 按 <span class="kbd">Ctrl+Space</span> 切換</div>
43
- <div>────────────────────────────────────────</div>
44
- <div id="log"></div>
45
- <div class="prompt">C:\&gt; <input id="cmd" autocomplete="off" spellcheck="false"></div>
46
- </section>
47
- <aside class="side">
48
- <h3>輸入法狀態</h3>
49
- <div id="ime" class="ime-status">EN</div>
50
- <div>模式:<span id="imemode">英文</span></div>
51
- <h3>注音對照</h3>
52
- <div style="font-family:'Noto Sans TC',sans-serif;font-size:13px">
53
- ㄅ b ㄆ p ㄇ m ㄈ f ㄉ d ㄊ t ㄋ n ㄌ l<br>
54
- ㄍ g ㄎ k ㄏ h ㄐ j ㄑ q ㄒ x ㄓ zh ㄔ ch<br>
55
- ㄕ sh ㄖ r ㄗ z ㄘ c ㄙ s ㄧ i ㄨ u ㄩ v<br>
56
- ㄚ a ㄛ o ㄜ e ㄝ ei ㄞ ai ㄠ ao ㄢ an<br>
57
- ㄣ en ㄤ ang ㄥ eng ㄦ er
58
  </div>
59
- <h3>js-dos 6.22</h3>
60
- <div id="dosbox"></div>
61
- <div id="dosstatus" style="margin-top:6px;color:#060">載入中…</div>
62
- </aside>
63
- </main>
64
- <footer>Build: 一鍵 build.sh · 上傳至 Hugging Face Static Space hellosun/c_dos · 600KB+ 傳統記憶體保留</footer>
65
-
66
- <script src="js-dos.js"></script>
67
- <script>
68
- // Conventional memory display
69
- let memFree = 624;
70
- function setMem(v){ memFree=v; document.getElementById('memfree').textContent=v; }
71
- setMem(624);
72
-
73
- // Zhuyin IME
74
- const zhuyinMap = {
75
- 'ㄅㄚ':'巴','ㄆㄚ':'怕','ㄇㄚ':'媽','ㄈㄚ':'發','ㄉㄚ':'大','ㄊㄚ':'他','ㄋㄚ':'拿','ㄌㄚ':'拉',
76
- 'ㄍㄚ':'家','ㄎㄚ':'卡','ㄏㄚ':'哈','ㄐㄧ':'機','ㄑㄧ':'七','ㄒㄧ':'西','ㄓ':'之','ㄔ':'吃',
77
- 'ㄕ':'是','ㄖ':'日','ㄗ':'字','ㄘ':'次','ㄙ':'四','ㄧ':'一','ㄨ':'五','ㄩ':'女',
78
- 'ㄠ':'好','ㄞ':'來','ㄢ':'安','ㄣ':'恩','ㄤ':'昂','ㄥ':'鞥','ㄦ':'兒','ㄅㄛ':'波','ㄆㄛ':'坡',
79
- 'ㄇㄛ':'摸','ㄈㄛ':'佛','ㄉㄜ':'的','ㄊㄜ':'特','ㄋㄜ':'呢','ㄌㄜ':'樂','ㄍㄜ':'哥','ㄎㄜ':'科',
80
- 'ㄏㄜ':'喝','ㄐㄜ':'歌','ㄑㄜ':'課','ㄒㄜ':'喝','ㄓㄜ':'者','ㄔㄜ':'車','ㄕㄜ':'社','ㄖㄜ':'熱',
81
- 'ㄗㄜ':'則','ㄘㄜ':'測','ㄙㄜ':'色','ㄅㄞ':'百','ㄆㄞ':'派','ㄇㄞ':'買','ㄈㄞ':'發','ㄉㄞ':'呆',
82
- 'ㄊㄞ':'台','ㄋㄞ':'乃','ㄌㄞ':'來','ㄍㄞ':'改','ㄎㄞ':'開','ㄏㄞ':'海','ㄐㄞ':'介','ㄑㄞ':'且',
83
- 'ㄒㄞ':'鞋','ㄓㄞ':'宅','ㄔㄞ':'差','ㄕㄞ':'晒','ㄖㄞ':'乳','ㄗㄞ':'再','ㄘㄞ':'才','ㄙㄞ':'塞'
84
- };
85
- let imeOn = false;
86
- let buf = '';
87
- const imeEl = document.getElementById('ime');
88
- const modeEl = document.getElementById('imemode');
89
- const logEl = document.getElementById('log');
90
- const cmdEl = document.getElementById('cmd');
91
-
92
- function toggleIme(){
93
- imeOn = !imeOn;
94
- imeEl.textContent = imeOn ? '注音' : 'EN';
95
- imeEl.classList.toggle('on', imeOn);
96
- modeEl.textContent = imeOn ? '注音' : '英文';
97
- buf = '';
98
- }
99
- document.addEventListener('keydown', (e)=>{
100
- if (e.ctrlKey && e.code === 'Space') { e.preventDefault(); toggleIme(); return; }
101
- if (!imeOn) return;
102
- // zhuyin symbols typed via bopomofo keys? We accept direct unicode zhuyin too.
103
- });
104
-
105
- cmdEl.addEventListener('keydown', (e)=>{
106
- if (e.key === 'Enter'){
107
- const v = cmdEl.value;
108
- const line = document.createElement('div');
109
- line.innerHTML = '<span class="prompt">C:\\&gt; </span>' + (imeOn && buf ? convert(buf) : '') + escapeHtml(v);
110
- logEl.appendChild(line);
111
- handleCmd(v);
112
- cmdEl.value=''; buf='';
113
- return;
114
- }
115
- if (imeOn){
116
- // accumulate last char as zhuyin buffer display
117
- }
118
- });
119
-
120
- function convert(b){ return zhuyinMap[b] || b; }
121
- function escapeHtml(s){ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;'); }
122
-
123
- function handleCmd(v){
124
- const t = v.trim().toLowerCase();
125
- if (t === 'mem'){ print('640 KB total conventional memory'); print(memFree+' KB free'); }
126
- else if (t === 'ver'){ print('MS-DOS 6.22 (ETen demo)'); }
127
- else if (t === 'help'){ print('命令: mem, ver, cls, exit'); }
128
- else if (t === 'cls'){ logEl.innerHTML=''; }
129
- else if (t){ print('Bad command or file name'); }
130
- const term = document.getElementById('term'); term.scrollTop = term.scrollHeight;
131
- }
132
- function print(s){ const d=document.createElement('div'); d.textContent=s; logEl.appendChild(d); }
133
-
134
- // js-dos 6.22 init
135
- (function(){
136
- const status = document.getElementById('dosstatus');
137
- if (typeof Dos === 'undefined'){ status.textContent='js-dos 未載入'; return; }
138
- try {
139
- const dos = Dos(document.getElementById('dosbox'), {wdosboxUrl:'wdosbox.js'});
140
- status.textContent = 'js-dos 6.22 已載入 (DOSBox 核心)';
141
- } catch(err){ status.textContent = 'js-dos 初始化: '+err.message; }
142
- })();
143
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  </body>
145
  </html>
 
1
+ <!doctype html>
2
  <html lang="zh-Hant">
3
  <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
6
+ <meta name="theme-color" content="#05070a">
7
+ <title>倚天中文 DOS · js-dos 6.22</title>
8
+ <style>
9
+ :root { color-scheme: dark; font-family: system-ui, sans-serif; background: #05070a; color: #e7edf5; }
10
+ * { box-sizing: border-box; }
11
+ body { margin: 0; min-height: 100vh; display: flex; flex-direction: column; }
12
+ header, footer { padding: .7rem 1rem; background: #0c1118; border-color: #202a36; }
13
+ header { border-bottom: 1px solid #202a36; display: flex; justify-content: space-between; gap: 1rem; align-items: center; }
14
+ header strong { color: #80d8ff; }
15
+ #memory { color: #a7b8ca; font-size: .85rem; }
16
+ main { width: min(100%, 1060px); margin: auto; padding: 1rem; }
17
+ #screen-wrap { width: 100%; background: #000; border: 1px solid #263342; border-radius: 8px; overflow: hidden; }
18
+ #dosbox { display: block; width: 100%; height: auto; aspect-ratio: 4 / 3; background: #000; image-rendering: pixelated; touch-action: none; }
19
+ #message { min-height: 1.4em; padding: .35rem .15rem; color: #93a4b6; font: .8rem ui-monospace, monospace; }
20
+ .toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin: .4rem 0 .75rem; }
21
+ button { border: 1px solid #344457; border-radius: 6px; background: #141e2a; color: #e7edf5; min-height: 40px; padding: .45rem .75rem; font: inherit; cursor: pointer; touch-action: manipulation; }
22
+ button:active, button.active { background: #164467; border-color: #52b9ec; }
23
+ .spacer { flex: 1; }
24
+ .keypad { display: grid; grid-template-columns: repeat(10, minmax(0, 1fr)); gap: .35rem; }
25
+ .keypad button { min-width: 0; padding: .45rem .15rem; font-size: .9rem; }
26
+ .keypad .wide { grid-column: span 2; }
27
+ .hint { color: #9aabba; font-size: .82rem; line-height: 1.5; margin: .8rem 0; }
28
+ footer { border-top: 1px solid #202a36; font-size: .78rem; color: #9aabba; text-align: center; }
29
+ @media (max-width: 620px) {
30
+ main { padding: .55rem; }
31
+ header { padding: .6rem .7rem; align-items: flex-start; flex-direction: column; gap: .2rem; }
32
+ .keypad { grid-template-columns: repeat(6, minmax(0, 1fr)); gap: .3rem; }
33
+ .keypad button { min-height: 42px; }
34
+ }
35
+ </style>
36
  </head>
37
  <body>
38
+ <header>
39
+ <strong>倚天中文 DOS 6.22 復刻</strong>
40
+ <span id="memory">DOS 啟動後顯示可用傳統記憶體</span>
41
+ </header>
42
+ <main>
43
+ <div id="screen-wrap"><canvas id="dosbox" width="640" height="480" aria-label="DOS emulator screen"></canvas></div>
44
+ <div class="toolbar">
45
+ <button id="ime-toggle" type="button">Ctrl+Space 注音切換</button>
46
+ <button id="fullscreen" type="button">全螢幕</button>
47
+ <span class="spacer"></span>
48
+ <span id="message" role="status">正在載入 DOS 6.22…</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  </div>
50
+ <p class="hint">點選 DOS 畫面後可使用實體鍵盤。手機可用下方按鍵輸入;注音鍵盤採標準倚天鍵位。F1 顯示系統資訊,Alt+X 離開。</p>
51
+ <section class="keypad" aria-label="虛擬鍵盤">
52
+ <button type="button" data-key="49">1 ㄅ</button><button type="button" data-key="81">Q ㄆ</button><button type="button" data-key="65">A ㄇ</button><button type="button" data-key="90">Z ㄈ</button><button type="button" data-key="50">2 ㄉ</button><button type="button" data-key="87">W ㄊ</button>
53
+ <button type="button" data-key="83">S ㄋ</button><button type="button" data-key="88">X ㄌ</button><button type="button" data-key="69">E ㄍ</button><button type="button" data-key="68">D ㄎ</button><button type="button" data-key="67">C ㄏ</button><button type="button" data-key="82">R ㄐ</button>
54
+ <button type="button" data-key="70">F ㄑ</button><button type="button" data-key="86">V ㄒ</button><button type="button" data-key="53">5 ㄓ</button><button type="button" data-key="84">T ㄔ</button><button type="button" data-key="71">G ㄕ</button><button type="button" data-key="66">B ㄖ</button>
55
+ <button type="button" data-key="89">Y ㄗ</button><button type="button" data-key="72">H ㄘ</button><button type="button" data-key="78">N ㄙ</button><button type="button" data-key="85">U ㄧ</button><button type="button" data-key="74">J ㄨ</button><button type="button" data-key="77">M ㄩ</button>
56
+ <button type="button" data-key="56">8 ㄚ</button><button type="button" data-key="73">I ㄛ</button><button type="button" data-key="75">K ㄜ</button><button type="button" data-key="188">, ㄝ</button><button type="button" data-key="57">9 ㄞ</button><button type="button" data-key="79">O ㄟ</button>
57
+ <button type="button" data-key="76">L ㄠ</button><button type="button" data-key="190">. ㄡ</button><button type="button" data-key="48">0 ㄢ</button><button type="button" data-key="80">P ㄣ</button><button type="button" data-key="186">; ㄤ</button><button type="button" data-key="191">/ ㄥ</button>
58
+ <button type="button" data-key="189">- ㄦ</button><button type="button" data-key="54">6 ˊ</button><button type="button" data-key="51">3 ˇ</button><button type="button" data-key="52">4 ˋ</button><button type="button" data-key="55">7 ˙</button><button type="button" data-key="8">⌫</button>
59
+ <button type="button" data-key="27">Esc</button><button type="button" data-key="13">Enter</button><button type="button" data-key="32" class="wide">Space</button><button type="button" data-key="37">←</button><button type="button" data-key="38">↑</button><button type="button" data-key="39">→</button><button type="button" data-key="40">↓</button>
60
+ <button type="button" data-key="112">F1</button><button id="alt-x" type="button" class="wide">Alt+X</button>
61
+ </section>
62
+ </main>
63
+ <footer>OpenWatcom · GNU Unifont · Big5 注音輸入 · 目標保留 600KB 以上傳統記憶體</footer>
64
+ <script src="js-dos.js"></script>
65
+ <script>
66
+ (() => {
67
+ const status = document.getElementById('message');
68
+ const memory = document.getElementById('memory');
69
+ const host = document.getElementById('dosbox');
70
+ let controller = null;
71
+ const canvas = host;
72
+ const send = (code, down) => {
73
+ if (!controller) return;
74
+ controller.simulateKeyEvent(code, down);
75
+ };
76
+ const tap = (code) => {
77
+ send(code, true);
78
+ window.setTimeout(() => send(code, false), 90);
79
+ };
80
+ const toggleIme = () => {
81
+ send(17, true);
82
+ tap(32);
83
+ window.setTimeout(() => send(17, false), 130);
84
+ };
85
+ window.etImeToggle = toggleIme;
86
+ document.getElementById('ime-toggle').addEventListener('click', toggleIme);
87
+ document.getElementById('fullscreen').addEventListener('click', () => controller && controller.fullscreen());
88
+ document.getElementById('alt-x').addEventListener('click', () => {
89
+ send(18, true); tap(88); window.setTimeout(() => send(18, false), 130);
90
+ });
91
+ document.querySelectorAll('.keypad [data-key]').forEach((button) => {
92
+ button.addEventListener('pointerdown', (event) => {
93
+ event.preventDefault();
94
+ const code = Number(button.dataset.key);
95
+ tap(code);
96
+ button.classList.add('active');
97
+ });
98
+ button.addEventListener('pointerup', () => button.classList.remove('active'));
99
+ button.addEventListener('pointercancel', () => button.classList.remove('active'));
100
+ button.addEventListener('pointerleave', () => button.classList.remove('active'));
101
+ });
102
+ canvas.tabIndex = 0;
103
+ canvas.addEventListener('pointerdown', () => canvas.focus());
104
+ document.addEventListener('keydown', (event) => {
105
+ if (!controller) return;
106
+ if (event.code === 'Space' && event.ctrlKey) {
107
+ if (canvas.contains(event.target)) return;
108
+ event.preventDefault();
109
+ toggleIme();
110
+ return;
111
+ }
112
+ if (canvas.contains(event.target)) {
113
+ if (event.isTrusted && event.key === 'x' && event.altKey) event.preventDefault();
114
+ return;
115
+ }
116
+ if (!event.isTrusted) return;
117
+ if (event.code === 'AltLeft' || event.code === 'AltRight') {
118
+ send(18, true);
119
+ return;
120
+ }
121
+ if (event.code === 'ControlLeft' || event.code === 'ControlRight') {
122
+ send(17, true);
123
+ return;
124
+ }
125
+ if (event.code === 'KeyX' && event.altKey) event.preventDefault();
126
+ if (!event.repeat) send(event.keyCode, true);
127
+ });
128
+ document.addEventListener('keyup', (event) => {
129
+ if (!controller) return;
130
+ if (canvas.contains(event.target)) return;
131
+ if (!event.isTrusted) return;
132
+ if (event.code === 'AltLeft' || event.code === 'AltRight') { send(18, false); return; }
133
+ if (event.code === 'ControlLeft' || event.code === 'ControlRight') { send(17, false); return; }
134
+ send(event.keyCode, false);
135
+ });
136
+ if (typeof Dos !== 'function') { status.textContent = 'js-dos 核心載入失敗'; return; }
137
+ Dos(host, { wdosboxUrl: 'wdosbox.js' }).ready((fs, main) => {
138
+ status.textContent = '正在載入 DOS 開機磁碟…';
139
+ fs.extract('dos.zip', '/').then(() => main(['-c', 'boot ETEN.img']))
140
+ .then((ci) => {
141
+ controller = ci;
142
+ window.etController = ci;
143
+ canvas.tabIndex = 0;
144
+ status.textContent = 'DOS 6.22 已啟動;點選畫面輸入';
145
+ memory.textContent = '倚天中文系統 · js-dos 6.22';
146
+ })
147
+ .catch((error) => { status.textContent = '啟動失敗:' + (error.message || error); });
148
+ });
149
+ })();
150
+ </script>
151
  </body>
152
  </html>