Download index.html from HelloSun/c_dos: direct link, hf CLI and curl.
- Browser
- Download file 16.6 kB
-
https://huggingface.co/spaces/HelloSun/c_dos/resolve/main/index.html
- Command line
-
hf download hf://spaces/HelloSun/c_dos/index.html
-
curl -L -o index.html https://huggingface.co/spaces/HelloSun/c_dos/resolve/main/index.html
16.6 kB
| <!-- 小貓中文 DOS interface; Copyright (C) 2026 HelloSun. Author: HelloSun. First version: 1.0, 2026-10-25. | |
| SPDX-License-Identifier: GPL-3.0-only, with GPLv3 section 7(b) additional term: keep the author attribution. See LICENSE. --> | |
| <html lang="zh-Hant"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"> | |
| <meta name="theme-color" content="#05070a"> | |
| <title>小貓中文 DOS · FreeDOS</title> | |
| <style> | |
| :root { color-scheme: dark; font-family: system-ui, sans-serif; background: #05070a; color: #e7edf5; } | |
| * { box-sizing: border-box; } | |
| body { margin: 0; min-height: 100vh; display: flex; flex-direction: column; } | |
| header, footer { padding: .7rem 1rem; background: #0c1118; border-color: #202a36; } | |
| header { border-bottom: 1px solid #202a36; display: flex; justify-content: space-between; gap: 1rem; align-items: center; } | |
| header strong { color: #80d8ff; } | |
| #memory { color: #a7b8ca; font-size: .85rem; } | |
| main { width: min(100%, 1060px); margin: auto; padding: 1rem; } | |
| #screen-wrap { width: 100%; background: #000; border: 1px solid #263342; border-radius: 8px; overflow: hidden; } | |
| #dosbox { display: block; width: 100%; height: auto; aspect-ratio: 4 / 3; background: #000; image-rendering: pixelated; touch-action: none; } | |
| #message { min-height: 1.4em; padding: .35rem .15rem; color: #93a4b6; font: .8rem ui-monospace, monospace; } | |
| .toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin: .4rem 0 .75rem; } | |
| 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; } | |
| button:active, button.active { background: #164467; border-color: #52b9ec; } | |
| .spacer { flex: 1; } | |
| #caption { min-height: 2.6em; margin: .5rem 0 0; padding: .55rem .8rem; border-left: 3px solid #52b9ec; background: #0e1722; color: #d7e6f5; font-size: 1.02rem; line-height: 1.5; border-radius: 0 6px 6px 0; } | |
| #caption:empty { visibility: hidden; } | |
| #demo.running { background: #5a1d1d; border-color: #e06c6c; } | |
| .keypad { display: grid; grid-template-columns: repeat(10, minmax(0, 1fr)); gap: .35rem; } | |
| .keypad button { min-width: 0; padding: .45rem .15rem; font-size: .9rem; } | |
| .keypad .wide { grid-column: span 2; } | |
| .hint { color: #9aabba; font-size: .82rem; line-height: 1.5; margin: .8rem 0; } | |
| footer { border-top: 1px solid #202a36; font-size: .78rem; color: #9aabba; text-align: center; } | |
| @media (max-width: 620px) { | |
| main { padding: .55rem; } | |
| header { padding: .6rem .7rem; align-items: flex-start; flex-direction: column; gap: .2rem; } | |
| .keypad { grid-template-columns: repeat(6, minmax(0, 1fr)); gap: .3rem; } | |
| .keypad button { min-height: 42px; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <strong>小貓中文 FreeDOS</strong> | |
| <span id="memory">載入時於 DOS 畫面顯示即時 MEM</span> | |
| </header> | |
| <main> | |
| <div id="screen-wrap"><canvas id="dosbox" width="640" height="480" aria-label="DOS emulator screen"></canvas></div> | |
| <div id="caption" role="status" aria-live="polite"></div> | |
| <div class="toolbar"> | |
| <button id="ime-toggle" type="button">Ctrl+Space 注音切換</button> | |
| <button id="big5-toggle" type="button">Big5 顯示開關</button> | |
| <button id="demo" type="button">▶ 示範 Demo</button> | |
| <button id="fullscreen" type="button">全螢幕</button> | |
| <span class="spacer"></span> | |
| <span id="message" role="status">正在載入 FreeDOS…</span> | |
| </div> | |
| <p class="hint">點選 DOS 畫面後可使用實體鍵盤。手機可用下方按鍵輸入;注音使用相容鍵位。F1 顯示系統資訊,Alt+X 離開。</p> | |
| <section class="keypad" aria-label="虛擬鍵盤"> | |
| <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> | |
| <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> | |
| <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> | |
| <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> | |
| <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> | |
| <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> | |
| <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> | |
| <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> | |
| <button type="button" data-key="220">\</button><button type="button" data-key="186" data-shift="1">:</button><button type="button" data-key="9">Tab</button><button id="shift" type="button">Shift</button> | |
| <button type="button" data-key="112">F1</button><button id="alt-x" type="button" class="wide">Alt+X</button> | |
| </section> | |
| </main> | |
| <footer>小貓中文 v1.0 · 作者 HelloSun · 2026-10-25 · GPL-3.0(§7(b):須保留作者聲明)· OpenWatcom · GNU Unifont · Big5 注音 · 載入時顯示即時 MEM · <a href="LICENSE">License</a></footer> | |
| <script src="js-dos.js"></script> | |
| <script src="emu.b64.js"></script> | |
| <script src="dos.b64.js"></script> | |
| <script> | |
| (() => { | |
| const status = document.getElementById('message'); | |
| const memory = document.getElementById('memory'); | |
| const host = document.getElementById('dosbox'); | |
| let controller = null; | |
| const canvas = host; | |
| const send = (code, down) => { | |
| if (!controller) return; | |
| controller.simulateKeyEvent(code, down); | |
| }; | |
| const tap = (code) => { | |
| send(code, true); | |
| window.setTimeout(() => send(code, false), 90); | |
| }; | |
| let shiftOn = false; | |
| const setShift = (on) => { shiftOn = on; document.getElementById('shift').classList.toggle('active', on); }; | |
| document.getElementById('shift').addEventListener('click', () => setShift(!shiftOn)); | |
| const toggleIme = () => { | |
| send(17, true); | |
| tap(32); | |
| window.setTimeout(() => send(17, false), 130); | |
| }; | |
| window.etImeToggle = toggleIme; | |
| document.getElementById('ime-toggle').addEventListener('click', toggleIme); | |
| document.getElementById('big5-toggle').addEventListener('click', () => { | |
| send(17, true); tap(220); window.setTimeout(() => send(17, false), 130); | |
| }); | |
| document.getElementById('fullscreen').addEventListener('click', () => controller && controller.fullscreen()); | |
| document.getElementById('alt-x').addEventListener('click', () => { | |
| send(18, true); tap(88); window.setTimeout(() => send(18, false), 130); | |
| }); | |
| document.querySelectorAll('.keypad [data-key]').forEach((button) => { | |
| button.addEventListener('pointerdown', (event) => { | |
| event.preventDefault(); | |
| const code = Number(button.dataset.key); | |
| if (button.dataset.shift || shiftOn) { | |
| send(16, true); | |
| window.setTimeout(() => { tap(code); window.setTimeout(() => send(16, false), 130); }, 40); | |
| setShift(false); | |
| } else tap(code); | |
| button.classList.add('active'); | |
| }); | |
| button.addEventListener('pointerup', () => button.classList.remove('active')); | |
| button.addEventListener('pointercancel', () => button.classList.remove('active')); | |
| button.addEventListener('pointerleave', () => button.classList.remove('active')); | |
| }); | |
| // ---- guided demo: paced for human reading (captions + generous pauses) ---- | |
| const sleep = (ms) => new Promise((r) => window.setTimeout(r, ms)); | |
| const KEYS = { ' ': 32, '\n': 13, '.': 190, ',': 188, ';': 186, '/': 191, '-': 189 }; | |
| const keyCode = (ch) => KEYS[ch] || (/[a-z]/.test(ch) ? ch.toUpperCase().charCodeAt(0) : ch.charCodeAt(0)); | |
| const caption = document.getElementById('caption'); | |
| const demoBtn = document.getElementById('demo'); | |
| let demoRun = 0; | |
| const SPEED = 4; // demo speed-up factor (DOS needs >= ~90 ms between keys) | |
| const runDemo = async () => { | |
| if (demoRun) { demoRun = 0; return; } | |
| if (!controller) { status.textContent = '請等 DOS 啟動完成'; return; } | |
| const me = demoRun = Date.now(); | |
| const alive = () => demoRun === me; | |
| const say = async (text, ms) => { caption.textContent = text; await sleep(ms / SPEED); }; | |
| const press = async (code, hold = 50) => { send(code, true); await sleep(hold); send(code, false); await sleep(30); }; | |
| const keys = async (str, gap = 650) => { for (const ch of str) { if (!alive()) return; await press(keyCode(ch)); await sleep(Math.max(gap / SPEED, 50)); } }; | |
| const imeToggle = async () => { send(17, true); await sleep(60); await press(32); send(17, false); await sleep(200); }; | |
| const step = async (text, readMs, action) => { if (!alive()) return; await say(text, 400); if (action) await action(); if (alive()) await sleep(readMs / SPEED); }; | |
| demoBtn.classList.add('running'); demoBtn.textContent = '■ 停止示範'; | |
| canvas.focus(); | |
| try { | |
| await step('1. 小貓中文是 DOS 常駐程式:用 TYPE 顯示含中文的檔案', 5500, () => keys('type demo.txt\n', 140)); | |
| await step('2. 按 Ctrl+Space 開啟注音輸入(畫面最底列會顯示「注音」)', 3500, imeToggle); | |
| await step('3. 輸入「你」:ㄋㄧˇ = S U 3,底列出現候選字', 4500, () => keys('su3')); | |
| await step('4. 按數字 1 選第一個候選字', 3000, () => keys('1')); | |
| await step('5. 輸入「好」:ㄏㄠˇ = C L 3', 3500, () => keys('cl3')); | |
| await step('按 1 選字', 2500, () => keys('1')); | |
| await step('6. 再輸入「電腦」:ㄉㄧㄢˋ = 2 U 0 4,ㄋㄠˇ = S L 3', 3500, () => keys('2u04')); | |
| await step('按 1 選字', 2500, () => keys('1')); | |
| await step('接著「腦」:ㄋㄠˇ = S L 3', 2500, () => keys('sl3')); | |
| await step('按 1 選字', 3000, () => keys('1')); | |
| await step('7. 一個音超過 9 個候選字時會分頁,按空白鍵環形切換(右側 1/7 是頁碼)', 4500, () => keys('u4', 800)); | |
| await step('按空白鍵:下一頁', 3200, () => keys(' ')); | |
| await step('再按一次:再下一頁', 3200, () => keys(' ')); | |
| await step('按 Esc 取消這次輸入', 2800, () => press(27)); | |
| await step('8. 再按 Ctrl+Space 回到英文輸入', 3000, imeToggle); | |
| await step('9. 執行 XEDIT:仿 PE2 的全螢幕編輯器,直接寫螢幕記憶體,一樣能顯示中文', 6500, async () => { await keys('\n', 100); await keys('xedit demo.txt\n', 140); }); | |
| await step('10. 在編輯器裡也能用注音輸入:Ctrl+Space,「我」= J I 3', 4500, async () => { await imeToggle(); await keys('ji3'); }); | |
| await step('按 1 選字', 4000, () => keys('1')); | |
| await step('按 F3 離開編輯器,回到 DOS', 3500, async () => { await imeToggle(); await press(114); }); | |
| await say('示範結束。傳統記憶體只常駐約 16 KB,字型與詞庫放在 XMS。', 6000); | |
| } finally { | |
| if (alive() || demoRun === 0) { caption.textContent = ''; } | |
| demoBtn.classList.remove('running'); demoBtn.textContent = '▶ 示範 Demo'; | |
| demoRun = 0; | |
| } | |
| }; | |
| demoBtn.addEventListener('click', runDemo); | |
| canvas.tabIndex = 0; | |
| canvas.addEventListener('pointerdown', () => canvas.focus()); | |
| document.addEventListener('keydown', (event) => { | |
| if (!controller) return; | |
| if (event.code === 'Space' && event.ctrlKey) { | |
| if (canvas.contains(event.target)) return; | |
| event.preventDefault(); | |
| toggleIme(); | |
| return; | |
| } | |
| if (canvas.contains(event.target)) { | |
| if (event.isTrusted && event.key === 'x' && event.altKey) event.preventDefault(); | |
| return; | |
| } | |
| if (!event.isTrusted) return; | |
| if (event.code === 'AltLeft' || event.code === 'AltRight') { | |
| send(18, true); | |
| return; | |
| } | |
| if (event.code === 'ControlLeft' || event.code === 'ControlRight') { | |
| send(17, true); | |
| return; | |
| } | |
| if (event.code === 'KeyX' && event.altKey) event.preventDefault(); | |
| if (!event.repeat) send(event.keyCode, true); | |
| }); | |
| document.addEventListener('keyup', (event) => { | |
| if (!controller) return; | |
| if (canvas.contains(event.target)) return; | |
| if (!event.isTrusted) return; | |
| if (event.code === 'AltLeft' || event.code === 'AltRight') { send(18, false); return; } | |
| if (event.code === 'ControlLeft' || event.code === 'ControlRight') { send(17, false); return; } | |
| send(event.keyCode, false); | |
| }); | |
| if (typeof Dos !== 'function') { status.textContent = 'js-dos 核心載入失敗'; return; } | |
| // Self-hosted emulator: HF static Spaces send binary files through a CORS-less | |
| // redirect, so the DOSBox core ships as base64 text and is served from Blob URLs. | |
| const b64blob = (b64, type) => { | |
| const bin = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)); | |
| return URL.createObjectURL(new Blob([bin], { type })); | |
| }; | |
| if (!window.XM_WDOSBOX_B64 || !window.XM_WASM_B64) { status.textContent = '啟動失敗:emu.b64.js'; return; } | |
| const virtual = { | |
| 'wdosbox.js': b64blob(window.XM_WDOSBOX_B64, 'application/javascript'), | |
| 'wdosbox.wasm.js': b64blob(window.XM_WASM_B64, 'application/octet-stream'), | |
| }; | |
| const xhrOpen = XMLHttpRequest.prototype.open; | |
| XMLHttpRequest.prototype.open = function (method, url, ...rest) { | |
| const name = String(url).split('?')[0].split('/').pop(); | |
| return xhrOpen.call(this, method, virtual[name] || url, ...rest); | |
| }; | |
| Dos(host, { wdosboxUrl: 'wdosbox.js' }).ready((fs, main) => { | |
| status.textContent = '正在載入 DOS 開機磁碟…'; | |
| if (!window.XIAOMAO_B64) { status.textContent = '啟動失敗:dos.b64.js'; return; } | |
| const bin = Uint8Array.from(atob(window.XIAOMAO_B64), (c) => c.charCodeAt(0)); | |
| const zipUrl = URL.createObjectURL(new Blob([bin], { type: 'application/zip' })); | |
| fs.extract(zipUrl, '/').then(() => main(['-c', 'boot XIAOMAO.img'])) | |
| .then((ci) => { | |
| controller = ci; | |
| window.etController = ci; | |
| canvas.tabIndex = 0; | |
| status.textContent = 'FreeDOS 已啟動;點選畫面輸入'; | |
| }) | |
| .catch((error) => { status.textContent = '啟動失敗:' + (error.message || error); }); | |
| }); | |
| })(); | |
| </script> | |
| </body> | |
| </html> | |