qwerty-cli 0.0.1-alpha.12 → 0.0.1-alpha.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/ConfigEditor-U6VNM4X5.js +2 -0
- package/dist/ConfigEditor-U6VNM4X5.js.map +1 -0
- package/dist/DictBrowser-EJ4N3W5P.js +2 -0
- package/dist/DictBrowser-EJ4N3W5P.js.map +1 -0
- package/dist/{HelpScreen-P4U5O4OP.js → HelpScreen-QNZRWVXA.js} +2 -2
- package/dist/PracticeScreen-S5BGC4VI.js +2 -0
- package/dist/PracticeScreen-S5BGC4VI.js.map +1 -0
- package/dist/{StatsViewer-3CUMIAV4.js → StatsViewer-RZ4NNMSI.js} +2 -2
- package/dist/{WordLookup-YIU6LP4H.js → WordLookup-KEC2G7EI.js} +2 -2
- package/dist/chunk-3TQFRACL.js +2 -0
- package/dist/chunk-3TQFRACL.js.map +1 -0
- package/dist/{chunk-2GTGXODM.js → chunk-4GNY4LY7.js} +2 -2
- package/dist/chunk-6ROGUGNX.js +2 -0
- package/dist/chunk-6ROGUGNX.js.map +1 -0
- package/dist/chunk-BIBS2Q3E.js +3 -0
- package/dist/chunk-BIBS2Q3E.js.map +1 -0
- package/dist/chunk-E6BBQALJ.js +4 -0
- package/dist/chunk-E6BBQALJ.js.map +1 -0
- package/dist/{chunk-UPA4JFCH.js → chunk-G3DQB7FI.js} +2 -2
- package/dist/{chunk-MPE25TTQ.js → chunk-HJIGZU3E.js} +2 -2
- package/dist/chunk-K6THKMFC.js +3 -0
- package/dist/chunk-K6THKMFC.js.map +1 -0
- package/dist/{chunk-6QICLHIY.js → chunk-NA5UNUVL.js} +2 -2
- package/dist/{chunk-IUFBN3RD.js → chunk-QG7ZTS2G.js} +2 -2
- package/dist/chunk-QLTHO2HH.js +2 -0
- package/dist/chunk-QLTHO2HH.js.map +1 -0
- package/dist/{chunk-TP77EGJ2.js → chunk-V3TAMY5K.js} +2 -2
- package/dist/cli.js +1 -1
- package/dist/{config.impl-IYJ4ZUPE.js → config.impl-G25YTIQY.js} +2 -2
- package/dist/{dict.impl-Y66SRRZL.js → dict.impl-7AE6RFW2.js} +2 -2
- package/dist/doctor.impl-KTNLVHGF.js +4 -0
- package/dist/doctor.impl-KTNLVHGF.js.map +1 -0
- package/dist/menu.impl-SRQ6MJY3.js +2 -0
- package/dist/practice.impl-KRFKX4AS.js +2 -0
- package/dist/practice.impl-KRFKX4AS.js.map +1 -0
- package/dist/{stats.impl-IXVF3Q5Y.js → stats.impl-3I7BB7X3.js} +2 -2
- package/dist/{word.impl-C4AYZ3NC.js → word.impl-NOC72Y32.js} +2 -2
- package/package.json +1 -1
- package/dist/ConfigEditor-HAP5C2Y7.js +0 -2
- package/dist/ConfigEditor-HAP5C2Y7.js.map +0 -1
- package/dist/DictBrowser-7CIISVDN.js +0 -2
- package/dist/DictBrowser-7CIISVDN.js.map +0 -1
- package/dist/PracticeScreen-LM6M2OYD.js +0 -2
- package/dist/PracticeScreen-LM6M2OYD.js.map +0 -1
- package/dist/chunk-6KRVNT2S.js +0 -4
- package/dist/chunk-6KRVNT2S.js.map +0 -1
- package/dist/chunk-7RMRK5MO.js +0 -2
- package/dist/chunk-7RMRK5MO.js.map +0 -1
- package/dist/chunk-ELWVQGDK.js +0 -2
- package/dist/chunk-ELWVQGDK.js.map +0 -1
- package/dist/chunk-KBRGNL2D.js +0 -3
- package/dist/chunk-KBRGNL2D.js.map +0 -1
- package/dist/chunk-R6HQWKXU.js +0 -2
- package/dist/chunk-R6HQWKXU.js.map +0 -1
- package/dist/chunk-ZXMHFRCR.js +0 -3
- package/dist/chunk-ZXMHFRCR.js.map +0 -1
- package/dist/doctor.impl-5UHLQ4SZ.js +0 -4
- package/dist/doctor.impl-5UHLQ4SZ.js.map +0 -1
- package/dist/menu.impl-PUAAZGHA.js +0 -2
- package/dist/practice.impl-CJLWRT5Z.js +0 -2
- package/dist/practice.impl-CJLWRT5Z.js.map +0 -1
- /package/dist/{HelpScreen-P4U5O4OP.js.map → HelpScreen-QNZRWVXA.js.map} +0 -0
- /package/dist/{StatsViewer-3CUMIAV4.js.map → StatsViewer-RZ4NNMSI.js.map} +0 -0
- /package/dist/{WordLookup-YIU6LP4H.js.map → WordLookup-KEC2G7EI.js.map} +0 -0
- /package/dist/{chunk-2GTGXODM.js.map → chunk-4GNY4LY7.js.map} +0 -0
- /package/dist/{chunk-UPA4JFCH.js.map → chunk-G3DQB7FI.js.map} +0 -0
- /package/dist/{chunk-MPE25TTQ.js.map → chunk-HJIGZU3E.js.map} +0 -0
- /package/dist/{chunk-6QICLHIY.js.map → chunk-NA5UNUVL.js.map} +0 -0
- /package/dist/{chunk-IUFBN3RD.js.map → chunk-QG7ZTS2G.js.map} +0 -0
- /package/dist/{chunk-TP77EGJ2.js.map → chunk-V3TAMY5K.js.map} +0 -0
- /package/dist/{config.impl-IYJ4ZUPE.js.map → config.impl-G25YTIQY.js.map} +0 -0
- /package/dist/{dict.impl-Y66SRRZL.js.map → dict.impl-7AE6RFW2.js.map} +0 -0
- /package/dist/{menu.impl-PUAAZGHA.js.map → menu.impl-SRQ6MJY3.js.map} +0 -0
- /package/dist/{stats.impl-IXVF3Q5Y.js.map → stats.impl-3I7BB7X3.js.map} +0 -0
- /package/dist/{word.impl-C4AYZ3NC.js.map → word.impl-NOC72Y32.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ui/nav.tsx","../src/i18n/context.tsx","../src/i18n/strings.ts","../src/i18n/locale.ts","../src/ui/components/BigWord.tsx"],"sourcesContent":["import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';\n\nexport type ScreenName =\n | 'main'\n | 'practice'\n | 'dict'\n | 'config'\n | 'stats'\n | 'word'\n | 'help';\n\nexport type PracticeParams = {\n dictId: string;\n chapterIndex: number;\n mode: 'order' | 'dictation' | 'review' | 'random' | 'loop';\n stealth?: boolean;\n};\n\nexport type DictParams = {\n pickerMode?: 'set-default' | 'choose-then-practice';\n};\n\nexport type ScreenFrame =\n | { name: 'main' }\n | { name: 'practice'; params: PracticeParams }\n | { name: 'dict'; params?: DictParams }\n | { name: 'config' }\n | { name: 'stats' }\n | { name: 'word' }\n | { name: 'help' };\n\ntype NavContextValue = {\n current: ScreenFrame;\n stack: ScreenFrame[];\n navigate: (frame: ScreenFrame) => void;\n replace: (frame: ScreenFrame) => void;\n back: () => void;\n reset: (frame: ScreenFrame) => void;\n};\n\nconst NavContext = createContext<NavContextValue | null>(null);\n\nexport function NavProvider({ initial, children }: { initial: ScreenFrame; children: ReactNode }) {\n const [stack, setStack] = useState<ScreenFrame[]>([initial]);\n\n const navigate = useCallback((frame: ScreenFrame) => {\n setStack((s) => [...s, frame]);\n }, []);\n const replace = useCallback((frame: ScreenFrame) => {\n setStack((s) => (s.length === 0 ? [frame] : [...s.slice(0, -1), frame]));\n }, []);\n const back = useCallback(() => {\n setStack((s) => (s.length > 1 ? s.slice(0, -1) : s));\n }, []);\n const reset = useCallback((frame: ScreenFrame) => {\n setStack([frame]);\n }, []);\n\n const current = stack[stack.length - 1]!;\n return (\n <NavContext.Provider value={{ current, stack, navigate, replace, back, reset }}>\n {children}\n </NavContext.Provider>\n );\n}\n\nexport function useNav(): NavContextValue {\n const ctx = useContext(NavContext);\n if (!ctx) throw new Error('useNav must be used inside NavProvider');\n return ctx;\n}\n","import { createContext, useContext, useMemo, type ReactNode } from 'react';\nimport { en, zh, type Strings } from './strings.js';\nimport { detectLocale, type Lang, type LangPref } from './locale.js';\n\nconst StringsContext = createContext<{ lang: Lang; t: Strings } | null>(null);\n\nexport function StringsProvider({\n pref,\n children,\n}: {\n pref: LangPref;\n children: ReactNode;\n}) {\n const value = useMemo(() => {\n const lang = detectLocale(pref);\n return { lang, t: lang === 'zh' ? zh : en };\n }, [pref]);\n return <StringsContext.Provider value={value}>{children}</StringsContext.Provider>;\n}\n\nexport function useStrings(): Strings {\n const ctx = useContext(StringsContext);\n if (!ctx) throw new Error('useStrings must be used inside StringsProvider');\n return ctx.t;\n}\n\nexport function useLang(): Lang {\n const ctx = useContext(StringsContext);\n if (!ctx) throw new Error('useLang must be used inside StringsProvider');\n return ctx.lang;\n}\n\nexport function pickStrings(pref: LangPref): { lang: Lang; t: Strings } {\n const lang = detectLocale(pref);\n return { lang, t: lang === 'zh' ? zh : en };\n}\n","// Centralized string table for the qwerty-cli TUI.\n//\n// Print-only subcommands (qwerty config list, qwerty dict list, etc.) keep\n// English output regardless of locale so shell scripts stay predictable.\n// Only the interactive TUI is localized via this table.\n\nexport type Strings = {\n app: {\n title: string;\n subtitle: string;\n };\n common: {\n back: string;\n quit: string;\n on: string;\n off: string;\n cancel: string;\n };\n mainMenu: {\n items: {\n practiceLabel: string;\n practiceHintWith: (name: string) => string;\n practiceHintNone: string;\n dictLabel: string;\n dictHint: string;\n wordLabel: string;\n wordHint: string;\n statsLabel: string;\n statsHint: string;\n configLabel: string;\n configHint: string;\n stealthLabel: string;\n stealthHint: string;\n quitLabel: string;\n quitHint: string;\n };\n hint: string;\n helpHint: string;\n };\n dict: {\n title: string;\n loading: string;\n entries: (n: number) => string;\n filterPlaceholder: string;\n local: string;\n notLocal: string;\n defaultMark: string;\n tagsLabel: (tags: string) => string;\n wordsLabel: (n: number) => string;\n pulling: (id: string) => string;\n removing: (id: string) => string;\n errorOn: (id: string, msg: string) => string;\n footer: string;\n action: {\n title: string;\n setDefault: string;\n practice: string;\n delete: string;\n };\n command: {\n title: string;\n pull: string;\n import: string;\n refreshList: string;\n };\n };\n config: {\n title: string;\n fields: {\n defaultDict: string;\n defaultMode: string;\n accent: string;\n mirror: string;\n chapterSize: string;\n autoplayPronunciation: string;\n soundsMaster: string;\n soundsKeystroke: string;\n soundsFeedback: string;\n soundsKeySound: string;\n soundsPronunciationRate: string;\n soundsPronunciationSource: string;\n language: string;\n stealth: string;\n };\n enumValues: {\n stealth: { off: string; menu: string; default: string };\n soundsPronunciationSource: { youdao: string; dictapi: string };\n };\n hints: {\n editing: string;\n bool: string;\n enum: string;\n dictRef: string;\n stringOrInt: string;\n };\n };\n stats: {\n title: string;\n loading: string;\n none: string;\n nonePractice: string;\n lifetime: string;\n sessions: string;\n words: string;\n errors: string;\n wpm: string;\n accuracy: string;\n streak: string;\n last: (n: number) => string;\n cycleWindow: string;\n recent: string;\n topMistakes: string;\n footer: string;\n maxLabel: string;\n recentUnits: { words: string; errors: string; wpm: string };\n multiDictSuffix: (n: number) => string;\n bars: { speed: string; accuracy: string; sessions: string };\n };\n word: {\n title: string;\n indexing: string;\n none: string;\n pullFirst: string;\n countAcross: (n: number) => string;\n noMatches: (q: string) => string;\n inDict: (name: string) => string;\n mistakes: (n: number, date: string) => string;\n footer: string;\n };\n practice: {\n loading: string;\n paused: string;\n chapterComplete: string;\n chapterLabel: (c: number, t: number) => string;\n reviewLabel: string;\n statusBar: {\n mode: string;\n accent: string;\n };\n modes: {\n order: string;\n dictation: string;\n review: string;\n random: string;\n loop: string;\n };\n accents: {\n us: string;\n uk: string;\n };\n statCards: {\n words: string;\n errors: string;\n wpm: string;\n accuracy: string;\n elapsed: (t: string) => string;\n };\n pause: {\n title: string;\n chapter: (c: number, t: number) => string;\n progress: (completed: number, total: number) => string;\n hint: string;\n };\n summary: {\n loopAgain: string;\n nextChapter: string;\n reviewMistakes: string;\n backMenu: string;\n };\n footers: {\n typing: string;\n };\n errors: {\n noMistakes: string;\n dictEmpty: (id: string) => string;\n unknown: string;\n };\n imeWarning: string;\n imeWarningShort: string;\n audioWarningShort: string;\n };\n audio: {\n noPlayer: string;\n };\n report: {\n title: string;\n duration: string;\n practiced: string;\n chapters: string;\n words: string;\n accuracy: string;\n wpm: string;\n newMistakes: string;\n farewell: string;\n notPracticed: string;\n };\n help: {\n title: string;\n subtitle: string;\n sections: {\n main: string;\n practice: string;\n dict: string;\n config: string;\n stats: string;\n word: string;\n global: string;\n };\n keys: {\n navigate: string;\n select: string;\n letterJump: string;\n pause: string;\n skip: string;\n replay: string;\n resume: string;\n backMenu: string;\n backScreen: string;\n nextChapter: string;\n reviewMistakes: string;\n filter: string;\n itemActions: string;\n moreActions: string;\n cycleWindow: string;\n stealthToggle: string;\n helpScreen: string;\n quit: string;\n };\n footer: string;\n };\n stealth: {\n paused: string;\n chapterDone: string;\n resumeHint: string;\n nextHint: string;\n pausedHintRight: string;\n nextHintRight: string;\n infoChipLabel: string;\n infoFmt: (\n dict: string,\n chapter: string,\n completed: number,\n total: number,\n wpm: number,\n accPct: number,\n ) => string;\n };\n};\n\nexport const en: Strings = {\n app: {\n title: 'qwerty',\n subtitle: 'typing practice for the terminal',\n },\n common: {\n back: 'back',\n quit: 'quit',\n on: 'on',\n off: 'off',\n cancel: 'cancel',\n },\n mainMenu: {\n items: {\n practiceLabel: 'Practice',\n practiceHintWith: (name) => `start ${name}`,\n practiceHintNone: 'pick a dictionary',\n dictLabel: 'Dictionaries',\n dictHint: 'browse, pull, set default',\n wordLabel: 'Word lookup',\n wordHint: 'search local dicts',\n statsLabel: 'Stats',\n statsHint: 'history & trends',\n configLabel: 'Config',\n configHint: 'edit preferences',\n stealthLabel: 'Stealth',\n stealthHint: 'quiet practice mode',\n quitLabel: 'Quit',\n quitHint: 'Esc or Ctrl+C also exits',\n },\n hint: '↑/↓ navigate · Enter select · letters jump',\n helpHint: '? help',\n },\n dict: {\n title: 'Dictionaries',\n loading: 'loading dictionaries…',\n entries: (n) => `${n} entries`,\n filterPlaceholder: 'type to filter',\n local: 'local ✓',\n notLocal: 'not local',\n defaultMark: 'default ★',\n tagsLabel: (tags) => `tags: ${tags}`,\n wordsLabel: (n) => `${n} words`,\n pulling: (id) => `pulling ${id}…`,\n removing: (id) => `removing ${id}…`,\n errorOn: (id, msg) => `error on ${id}: ${msg}`,\n footer: '↑/↓ select · Enter actions · Ctrl+K more · Esc back',\n action: {\n title: 'current dictionary',\n setDefault: 'set as default',\n practice: 'practice now',\n delete: 'delete local',\n },\n command: {\n title: 'more actions',\n pull: 'pull selected',\n import: 'import .json',\n refreshList: 'update dictionary list',\n },\n },\n config: {\n title: 'Config',\n fields: {\n defaultDict: 'default dict',\n defaultMode: 'default mode',\n accent: 'accent',\n mirror: 'dict mirror',\n chapterSize: 'chapter size',\n autoplayPronunciation: 'autoplay pronunciation',\n soundsMaster: 'sounds master',\n soundsKeystroke: 'sounds keystroke',\n soundsFeedback: 'sounds feedback',\n soundsKeySound: 'sounds key sound',\n soundsPronunciationRate: 'pronunciation rate',\n soundsPronunciationSource: 'pronunciation source',\n language: 'language',\n stealth: 'stealth mode',\n },\n enumValues: {\n stealth: { off: 'off', menu: 'show in menu', default: 'default practice' },\n soundsPronunciationSource: { youdao: 'Youdao', dictapi: 'Wiktionary' },\n },\n hints: {\n editing: 'type to edit · Enter save · Esc cancel',\n bool: 'space toggle · ↑/↓ move · Esc back',\n enum: '←/→ cycle · ↑/↓ move · Esc back',\n dictRef: 'Enter pick dict · ↑/↓ move · Esc back',\n stringOrInt: 'Enter edit · ↑/↓ move · Esc back',\n },\n },\n stats: {\n title: 'Stats · overview',\n loading: 'loading stats…',\n none: 'No practice history yet.',\n nonePractice: 'Run a practice session first.',\n lifetime: 'lifetime',\n sessions: 'sessions',\n words: 'words',\n errors: 'errors',\n wpm: 'wpm',\n accuracy: 'accuracy',\n streak: 'streak',\n last: (n) => `last ${n} days (←/→ cycle window)`,\n cycleWindow: '←/→ cycle window · Esc back',\n recent: 'recent sessions',\n topMistakes: 'top mistakes',\n footer: '←/→ cycle window · Esc back',\n maxLabel: 'max',\n recentUnits: { words: 'w', errors: 'err', wpm: 'wpm' },\n multiDictSuffix: (n) => ` +${n} more`,\n bars: { speed: 'speed', accuracy: 'accuracy', sessions: 'sessions' },\n },\n word: {\n title: 'Word lookup',\n indexing: 'indexing local dictionaries…',\n none: 'No local dictionaries.',\n pullFirst: 'Pull one in Dictionaries first.',\n countAcross: (n) => `${n} words across local dicts`,\n noMatches: (q) => `no matches for \"${q}\"`,\n inDict: (name) => `in: ${name}`,\n mistakes: (n, date) => `mistakes: ${n} (last ${date})`,\n footer: 'type to filter · ↑/↓ select · Esc back',\n },\n practice: {\n loading: 'loading…',\n paused: 'PAUSED',\n chapterComplete: 'CHAPTER COMPLETE',\n chapterLabel: (c, t) => `chapter ${c}/${t}`,\n reviewLabel: 'review',\n statusBar: {\n mode: 'mode',\n accent: 'accent',\n },\n modes: {\n order: 'order',\n dictation: 'dictation',\n review: 'review',\n random: 'random',\n loop: 'loop',\n },\n accents: {\n us: 'us',\n uk: 'uk',\n },\n statCards: {\n words: 'words',\n errors: 'errors',\n wpm: 'wpm',\n accuracy: 'accuracy',\n elapsed: (t) => `elapsed ${t}`,\n },\n pause: {\n title: 'PAUSED',\n chapter: (c, t) => `chapter ${c}/${t}`,\n progress: (completed, total) => `${completed}/${total}`,\n hint: 'Enter resume · Esc back to menu',\n },\n summary: {\n loopAgain: 'again',\n nextChapter: 'next chapter',\n reviewMistakes: 'review mistakes',\n backMenu: 'back to menu',\n },\n footers: {\n typing: 'Ctrl+N skip · Esc pause · Tab replay',\n },\n errors: {\n noMistakes: 'No mistakes to review yet. Practice some chapters first.',\n dictEmpty: (id) => `Dictionary ${id} is empty.`,\n unknown: 'Unknown error',\n },\n imeWarning: '! Non-English input detected — switch your IME to English.',\n imeWarningShort: '! IME',\n audioWarningShort: '! audio',\n },\n audio: {\n noPlayer: '! No audio player found on PATH (looked for afplay/ffplay/mpg123/paplay/aplay/powershell). Sounds disabled.',\n },\n report: {\n title: 'Session summary',\n duration: 'duration',\n practiced: 'practiced',\n chapters: 'chapters',\n words: 'words',\n accuracy: 'accuracy',\n wpm: 'wpm',\n newMistakes: 'new mistakes',\n farewell: 'see you next time.',\n notPracticed: 'no practice this run',\n },\n help: {\n title: 'Help',\n subtitle: 'all shortcuts',\n sections: {\n main: 'main menu',\n practice: 'practice',\n dict: 'dictionaries',\n config: 'config',\n stats: 'stats',\n word: 'word lookup',\n global: 'global',\n },\n keys: {\n navigate: '↑/↓ navigate items',\n select: 'Enter confirm / continue',\n letterJump: 'letter jump to menu item',\n pause: 'Esc pause practice',\n skip: 'Ctrl+N skip current word (neutral)',\n replay: 'Tab replay pronunciation',\n resume: 'Enter resume from pause',\n backMenu: 'Esc back to previous screen',\n backScreen: 'Esc close panel or back',\n nextChapter: 'Enter next chapter',\n reviewMistakes: 'm review mistakes',\n filter: 'type to filter list',\n itemActions: 'Enter open actions panel',\n moreActions: 'Ctrl+K more actions panel',\n cycleWindow: '←/→ cycle day window',\n stealthToggle: 'Ctrl+I toggle stealth info row',\n helpScreen: '? open this help screen',\n quit: 'Ctrl+C quit immediately',\n },\n footer: 'Esc back',\n },\n stealth: {\n paused: 'paused',\n chapterDone: 'chapter done',\n resumeHint: 'Enter resume · Esc menu',\n nextHint: 'Enter next · Esc menu',\n pausedHintRight: 'Enter resume',\n nextHintRight: 'Enter next',\n infoChipLabel: 'info',\n infoFmt: (dict, chapter, completed, total, wpm, accPct) =>\n `${dict} · ${chapter} · ${completed}/${total} · ${wpm} wpm · ${accPct}%`,\n },\n};\n\nexport const zh: Strings = {\n app: {\n title: 'qwerty',\n subtitle: '终端键盘练习',\n },\n common: {\n back: '返回',\n quit: '退出',\n on: '开',\n off: '关',\n cancel: '取消',\n },\n mainMenu: {\n items: {\n practiceLabel: '练习',\n practiceHintWith: (name) => `开始 ${name}`,\n practiceHintNone: '请先选词典',\n dictLabel: '词典',\n dictHint: '浏览、下载、设为默认',\n wordLabel: '查词',\n wordHint: '在本地词典中搜索',\n statsLabel: '统计',\n statsHint: '历史与趋势',\n configLabel: '设置',\n configHint: '修改偏好',\n stealthLabel: '神隐',\n stealthHint: '神隐练习模式',\n quitLabel: '退出',\n quitHint: 'Esc 或 Ctrl+C 退出',\n },\n hint: '↑/↓ 移动 · Enter 确认 · 字母直达',\n helpHint: '? 帮助',\n },\n dict: {\n title: '词典',\n loading: '加载词典中…',\n entries: (n) => `${n} 部词典`,\n filterPlaceholder: '输入过滤',\n local: '已下载 ✓',\n notLocal: '未下载',\n defaultMark: '默认 ★',\n tagsLabel: (tags) => `标签:${tags}`,\n wordsLabel: (n) => `${n} 词`,\n pulling: (id) => `拉取 ${id} 中…`,\n removing: (id) => `删除 ${id} 中…`,\n errorOn: (id, msg) => `${id} 出错:${msg}`,\n footer: '↑/↓ 选择 · Enter 操作 · Ctrl+K 更多 · Esc 返回',\n action: {\n title: '当前词典',\n setDefault: '设为默认',\n practice: '立即练习',\n delete: '删除本地',\n },\n command: {\n title: '更多功能',\n pull: '拉取选中',\n import: '导入 .json',\n refreshList: '更新词典列表',\n },\n },\n config: {\n title: '设置',\n fields: {\n defaultDict: '默认词典',\n defaultMode: '默认模式',\n accent: '发音',\n mirror: '词典镜像源',\n chapterSize: '章节单词数',\n autoplayPronunciation: '自动播放发音',\n soundsMaster: '音效总开关',\n soundsKeystroke: '按键音',\n soundsFeedback: '反馈音',\n soundsKeySound: '按键音色',\n soundsPronunciationRate: '发音速率',\n soundsPronunciationSource: '发音音源',\n language: '语言',\n stealth: '神隐模式',\n },\n enumValues: {\n stealth: { off: '关闭', menu: '主菜单显示', default: '默认练习模式' },\n soundsPronunciationSource: { youdao: '有道词典', dictapi: '维基词典' },\n },\n hints: {\n editing: '输入修改 · Enter 保存 · Esc 取消',\n bool: '空格切换 · ↑/↓ 移动 · Esc 返回',\n enum: '←/→ 切换 · ↑/↓ 移动 · Esc 返回',\n dictRef: 'Enter 选词典 · ↑/↓ 移动 · Esc 返回',\n stringOrInt: 'Enter 编辑 · ↑/↓ 移动 · Esc 返回',\n },\n },\n stats: {\n title: '统计 · 概览',\n loading: '加载统计中…',\n none: '还没有练习记录。',\n nonePractice: '先来一次练习吧。',\n lifetime: '累计',\n sessions: '会话',\n words: '词数',\n errors: '错误',\n wpm: '速度',\n accuracy: '准确率',\n streak: '连续天数',\n last: (n) => `最近 ${n} 天 (←/→ 切换窗口)`,\n cycleWindow: '←/→ 切换窗口 · Esc 返回',\n recent: '最近会话',\n topMistakes: '高频错词',\n footer: '←/→ 切换窗口 · Esc 返回',\n maxLabel: '最大',\n recentUnits: { words: '词', errors: '错', wpm: '速' },\n multiDictSuffix: (n) => ` 等 ${n} 部`,\n bars: { speed: '速度', accuracy: '准确率', sessions: '会话' },\n },\n word: {\n title: '查词',\n indexing: '索引本地词典中…',\n none: '没有本地词典。',\n pullFirst: '先在「词典」中拉取一部。',\n countAcross: (n) => `本地词典共 ${n} 词`,\n noMatches: (q) => `没有匹配「${q}」的词`,\n inDict: (name) => `来源:${name}`,\n mistakes: (n, date) => `错过 ${n} 次 (最近 ${date})`,\n footer: '输入过滤 · ↑/↓ 选择 · Esc 返回',\n },\n practice: {\n loading: '加载中…',\n paused: '已暂停',\n chapterComplete: '本章完成',\n chapterLabel: (c, t) => `第 ${c}/${t} 章`,\n reviewLabel: '复习',\n statusBar: {\n mode: '模式',\n accent: '发音',\n },\n modes: {\n order: '顺序',\n dictation: '默写',\n review: '复习',\n random: '乱序',\n loop: '循环',\n },\n accents: {\n us: '美',\n uk: '英',\n },\n statCards: {\n words: '词数',\n errors: '错误',\n wpm: '速度',\n accuracy: '准确率',\n elapsed: (t) => `耗时 ${t}`,\n },\n pause: {\n title: '已暂停',\n chapter: (c, t) => `第 ${c}/${t} 章`,\n progress: (completed, total) => `${completed}/${total}`,\n hint: 'Enter 继续 · Esc 返回菜单',\n },\n summary: {\n loopAgain: '再来一遍',\n nextChapter: '下一章',\n reviewMistakes: '复习错词',\n backMenu: '返回菜单',\n },\n footers: {\n typing: 'Ctrl+N 跳过 · Esc 暂停 · Tab 重播',\n },\n errors: {\n noMistakes: '错词本是空的。先练习几章吧。',\n dictEmpty: (id) => `词典 ${id} 是空的。`,\n unknown: '未知错误',\n },\n imeWarning: '! 检测到中文/日文/韩文输入,请切换到英文输入法',\n imeWarningShort: '! 中文输入',\n audioWarningShort: '! 无音效',\n },\n audio: {\n noPlayer: '! 未在 PATH 中找到音频播放器(尝试 afplay/ffplay/mpg123/paplay/aplay/powershell)。音效已禁用。',\n },\n report: {\n title: '本次会话',\n duration: '总时长',\n practiced: '练习用时',\n chapters: '完成章节',\n words: '词数',\n accuracy: '准确率',\n wpm: '速度',\n newMistakes: '新错词',\n farewell: '下次见。',\n notPracticed: '本次未练习',\n },\n help: {\n title: '帮助',\n subtitle: '全部快捷键',\n sections: {\n main: '主菜单',\n practice: '练习',\n dict: '词典',\n config: '设置',\n stats: '统计',\n word: '查词',\n global: '全局',\n },\n keys: {\n navigate: '↑/↓ 移动选项',\n select: 'Enter 确认 / 继续',\n letterJump: '字母键 直达菜单项',\n pause: 'Esc 暂停练习',\n skip: 'Ctrl+N 跳过当前词(不计错)',\n replay: 'Tab 重播发音',\n resume: 'Enter 继续练习',\n backMenu: 'Esc 返回上一屏',\n backScreen: 'Esc 关闭面板 / 返回',\n nextChapter: 'Enter 下一章',\n reviewMistakes: 'm 复习错词',\n filter: '输入 过滤列表',\n itemActions: 'Enter 弹出动作面板',\n moreActions: 'Ctrl+K 弹出更多功能',\n cycleWindow: '←/→ 切换日窗口',\n stealthToggle: 'Ctrl+I 切换神隐信息行',\n helpScreen: '? 打开本帮助页',\n quit: 'Ctrl+C 立即退出',\n },\n footer: 'Esc 返回',\n },\n stealth: {\n paused: 'paused',\n chapterDone: 'chapter done',\n resumeHint: 'Enter resume · Esc menu',\n nextHint: 'Enter next · Esc menu',\n pausedHintRight: 'Enter 继续',\n nextHintRight: 'Enter 下一章',\n infoChipLabel: '信息',\n infoFmt: (dict, chapter, completed, total, wpm, accPct) =>\n `${dict} · ${chapter} · ${completed}/${total} · ${wpm} wpm · ${accPct}%`,\n },\n};\n","export type Lang = 'zh' | 'en';\nexport type LangPref = 'auto' | Lang;\n\nfunction pickFromString(s: string | undefined): Lang | null {\n if (!s) return null;\n const lower = s.toLowerCase();\n if (lower.startsWith('zh')) return 'zh';\n if (lower.startsWith('en')) return 'en';\n return null;\n}\n\nexport function detectLocale(pref: LangPref): Lang {\n if (pref === 'zh' || pref === 'en') return pref;\n const env =\n process.env.LC_ALL ||\n process.env.LC_MESSAGES ||\n process.env.LANG ||\n process.env.LANGUAGE;\n const fromEnv = pickFromString(env);\n if (fromEnv) return fromEnv;\n try {\n const intlLocale = Intl.DateTimeFormat().resolvedOptions().locale;\n const fromIntl = pickFromString(intlLocale);\n if (fromIntl) return fromIntl;\n } catch {\n // ignore\n }\n return 'en';\n}\n","import { Box, Text } from 'ink';\n\nexport const PALETTE = {\n accent: '#5eead4',\n muted: '#6b7280',\n text: '#e5e7eb',\n primary: '#7dcfff',\n success: '#86efac',\n warning: '#fbbf24',\n error: '#f87171',\n} as const;\n\ntype Props = {\n target: string;\n typed: string;\n error?: boolean;\n hideTarget?: boolean;\n};\n\nexport function BigWord({ target, typed, error = false, hideTarget = false }: Props) {\n const chars = [...target];\n const typedChars = [...typed];\n\n return (\n <Box paddingY={4} justifyContent=\"center\">\n {chars.map((ch, i) => {\n const isTyped = i < typedChars.length;\n const display = hideTarget && !isTyped ? '_' : isTyped ? typedChars[i]! : ch;\n const color = error\n ? PALETTE.error\n : isTyped\n ? PALETTE.accent\n : PALETTE.muted;\n return (\n <Text key={i} bold color={color}>\n {display}\n </Text>\n );\n })}\n </Box>\n );\n}\n"],"mappings":"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","names":["createContext","useContext","useState","useCallback","jsx","NavContext","NavProvider","initial","children","stack","setStack","navigate","frame","s","replace","back","reset","current","useNav","ctx","createContext","useContext","useMemo","en","name","n","tags","id","msg","q","date","c","t","completed","total","dict","chapter","wpm","accPct","zh","pickFromString","s","lower","detectLocale","pref","env","fromEnv","intlLocale","fromIntl","jsx","StringsContext","createContext","StringsProvider","pref","children","value","useMemo","lang","detectLocale","zh","en","useStrings","ctx","useContext","pickStrings","pref","lang","detectLocale","zh","en","Box","Text","jsx","PALETTE","BigWord","target","typed","error","hideTarget","chars","typedChars","ch","i","isTyped","display","color"]}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{b as o}from"./chunk-
|
|
2
|
-
//# sourceMappingURL=chunk-
|
|
1
|
+
import{b as o}from"./chunk-QLTHO2HH.js";import{createContext as s,useCallback as f,useContext as C,useState as c}from"react";import{jsx as g}from"react/jsx-runtime";var n=s(null);function d({initialCfg:t,children:i}){let[p,r]=c(t),a=f(async e=>{r(e),await o(e)},[]);return g(n.Provider,{value:{cfg:p,setCfg:a},children:i})}function l(){let t=C(n);if(!t)throw new Error("useAppState must be used inside AppStateProvider");return t}export{d as a,l as b};
|
|
2
|
+
//# sourceMappingURL=chunk-4GNY4LY7.js.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{a as p,g as m}from"./chunk-BIBS2Q3E.js";import{createContext as h,useContext as b,useEffect as y,useState as w}from"react";import{jsx as S}from"react/jsx-runtime";var f=h({warning:null,ready:!1});function C({disabled:t,children:r}){let[n,a]=w({warning:null,ready:!1});return y(()=>{let u=!1;return p(t).then(()=>{u||a({warning:m(),ready:!0})}).catch(()=>{u||a({warning:null,ready:!0})}),()=>{u=!0}},[t]),S(f.Provider,{value:n,children:r})}function M(){return b(f)}var d=null;function W(t){d=t}function D(){let t=d;return d=null,t}var c=!1;function R(t){c=t}function I(){let t=c;return c=!1,t}var e={startedAt:null,chapters:[]};function z(t=Date.now()){e.startedAt===null&&(e.startedAt=t)}function N(t){e.startedAt===null&&(e.startedAt=Date.now()),e.chapters.push(t)}function j(t=Date.now()){let r=e.chapters,n=r.reduce((o,s)=>o+s.wordCount,0),a=r.reduce((o,s)=>o+s.errors,0),u=r.reduce((o,s)=>o+s.durationMs,0),l=u/6e4,x=l>0?Math.round(n/l*10)/10:0,i=new Set;for(let o of r)for(let s of Object.keys(o.perWordErrors))i.add(s);let A=n===0?1:Math.max(0,(n-i.size)/n);return{startedAt:e.startedAt,totalDurationMs:e.startedAt===null?0:t-e.startedAt,chaptersCompleted:r.length,wordCount:n,errors:a,wpm:x,accuracy:A,newMistakeWords:i.size,practiceMs:u}}export{C as a,M as b,W as c,D as d,R as e,I as f,z as g,N as h,j as i};
|
|
2
|
+
//# sourceMappingURL=chunk-6ROGUGNX.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ui/audio-context.tsx","../src/util/post-exit-action.ts","../src/infra/session-tracker.ts"],"sourcesContent":["import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';\nimport { initAudio, audioWarning } from '../infra/audio.js';\n\ntype AudioStatus = { warning: string | null; ready: boolean };\n\nconst AudioStatusContext = createContext<AudioStatus>({ warning: null, ready: false });\n\nexport function AudioStatusProvider({\n disabled,\n children,\n}: {\n disabled: boolean;\n children: ReactNode;\n}) {\n const [status, setStatus] = useState<AudioStatus>({ warning: null, ready: false });\n\n useEffect(() => {\n let cancelled = false;\n initAudio(disabled)\n .then(() => {\n if (cancelled) return;\n setStatus({ warning: audioWarning(), ready: true });\n })\n .catch(() => {\n if (cancelled) return;\n setStatus({ warning: null, ready: true });\n });\n return () => {\n cancelled = true;\n };\n }, [disabled]);\n\n return <AudioStatusContext.Provider value={status}>{children}</AudioStatusContext.Provider>;\n}\n\nexport function useAudioStatus(): AudioStatus {\n return useContext(AudioStatusContext);\n}\n","// Single-shot handoff store: lets a UI screen request that the CLI re-enter a\n// different mode after Ink unmounts. Used by MainMenu → stealth, where the\n// menu runs inside an alt-screen render() and we cannot switch to inline mode\n// mid-flight. The menu screen calls setPostExitAction({ type: 'stealth', ... })\n// then exit(); the command entry point consumes the action after\n// waitUntilExit() and routes accordingly.\n//\n// Lifecycle: consumePostExitAction() reads-and-clears so a leftover action\n// from a previous run can never leak into the next one.\n\nimport type { Mode } from '../domain/chapters.js';\n\nexport type PostExitAction =\n | { type: 'stealth'; dictId: string; chapterIndex: number; mode: Mode };\n\nlet pending: PostExitAction | null = null;\n\nexport function setPostExitAction(action: PostExitAction): void {\n pending = action;\n}\n\nexport function consumePostExitAction(): PostExitAction | null {\n const a = pending;\n pending = null;\n return a;\n}\n\n// Signal from the Ink UI to the CLI entry point: \"the user hit Ctrl+C on the\n// stealth paused screen — exit without leaving the 3 inline rows behind and\n// without printing the post-session boxen report.\" Same single-shot lifecycle\n// as PostExitAction: consume reads and clears.\nlet silentExit = false;\n\nexport function setSilentExit(v: boolean): void {\n silentExit = v;\n}\n\nexport function consumeSilentExit(): boolean {\n const v = silentExit;\n silentExit = false;\n return v;\n}\n","type ChapterEntry = {\n dictId: string;\n chapterIndex: number;\n mode: string;\n wordCount: number;\n errors: number;\n durationMs: number;\n perWordErrors: Record<string, number>;\n};\n\ntype State = {\n startedAt: number | null;\n chapters: ChapterEntry[];\n};\n\nconst state: State = {\n startedAt: null,\n chapters: [],\n};\n\nexport function start(now = Date.now()): void {\n if (state.startedAt === null) state.startedAt = now;\n}\n\nexport function addChapter(entry: ChapterEntry): void {\n if (state.startedAt === null) state.startedAt = Date.now();\n state.chapters.push(entry);\n}\n\nexport type SessionReport = {\n startedAt: number | null;\n totalDurationMs: number;\n chaptersCompleted: number;\n wordCount: number;\n errors: number;\n wpm: number;\n accuracy: number;\n newMistakeWords: number;\n practiceMs: number;\n};\n\nexport function report(now = Date.now()): SessionReport {\n const chapters = state.chapters;\n const wordCount = chapters.reduce((a, c) => a + c.wordCount, 0);\n const errors = chapters.reduce((a, c) => a + c.errors, 0);\n const practiceMs = chapters.reduce((a, c) => a + c.durationMs, 0);\n const minutes = practiceMs / 60000;\n const wpm = minutes > 0 ? Math.round((wordCount / minutes) * 10) / 10 : 0;\n const errorWordSet = new Set<string>();\n for (const c of chapters) {\n for (const w of Object.keys(c.perWordErrors)) errorWordSet.add(w);\n }\n const accuracy = wordCount === 0 ? 1 : Math.max(0, (wordCount - errorWordSet.size) / wordCount);\n return {\n startedAt: state.startedAt,\n totalDurationMs: state.startedAt === null ? 0 : now - state.startedAt,\n chaptersCompleted: chapters.length,\n wordCount,\n errors,\n wpm,\n accuracy,\n newMistakeWords: errorWordSet.size,\n practiceMs,\n };\n}\n\nexport function reset(): void {\n state.startedAt = null;\n state.chapters = [];\n}\n"],"mappings":"+CAAA,OAAS,iBAAAA,EAAe,cAAAC,EAAY,aAAAC,EAAW,YAAAC,MAAgC,QAgCtE,cAAAC,MAAA,oBA3BT,IAAMC,EAAqBC,EAA2B,CAAE,QAAS,KAAM,MAAO,EAAM,CAAC,EAE9E,SAASC,EAAoB,CAClC,SAAAC,EACA,SAAAC,CACF,EAGG,CACD,GAAM,CAACC,EAAQC,CAAS,EAAIC,EAAsB,CAAE,QAAS,KAAM,MAAO,EAAM,CAAC,EAEjF,OAAAC,EAAU,IAAM,CACd,IAAIC,EAAY,GAChB,OAAAC,EAAUP,CAAQ,EACf,KAAK,IAAM,CACNM,GACJH,EAAU,CAAE,QAASK,EAAa,EAAG,MAAO,EAAK,CAAC,CACpD,CAAC,EACA,MAAM,IAAM,CACPF,GACJH,EAAU,CAAE,QAAS,KAAM,MAAO,EAAK,CAAC,CAC1C,CAAC,EACI,IAAM,CACXG,EAAY,EACd,CACF,EAAG,CAACN,CAAQ,CAAC,EAENJ,EAACC,EAAmB,SAAnB,CAA4B,MAAOK,EAAS,SAAAD,EAAS,CAC/D,CAEO,SAASQ,GAA8B,CAC5C,OAAOC,EAAWb,CAAkB,CACtC,CCtBA,IAAIc,EAAiC,KAE9B,SAASC,EAAkBC,EAA8B,CAC9DF,EAAUE,CACZ,CAEO,SAASC,GAA+C,CAC7D,IAAMC,EAAIJ,EACV,OAAAA,EAAU,KACHI,CACT,CAMA,IAAIC,EAAa,GAEV,SAASC,EAAcC,EAAkB,CAC9CF,EAAaE,CACf,CAEO,SAASC,GAA6B,CAC3C,IAAMD,EAAIF,EACV,OAAAA,EAAa,GACNE,CACT,CC1BA,IAAME,EAAe,CACnB,UAAW,KACX,SAAU,CAAC,CACb,EAEO,SAASC,EAAMC,EAAM,KAAK,IAAI,EAAS,CACxCF,EAAM,YAAc,OAAMA,EAAM,UAAYE,EAClD,CAEO,SAASC,EAAWC,EAA2B,CAChDJ,EAAM,YAAc,OAAMA,EAAM,UAAY,KAAK,IAAI,GACzDA,EAAM,SAAS,KAAKI,CAAK,CAC3B,CAcO,SAASC,EAAOH,EAAM,KAAK,IAAI,EAAkB,CACtD,IAAMI,EAAWN,EAAM,SACjBO,EAAYD,EAAS,OAAO,CAACE,EAAGC,IAAMD,EAAIC,EAAE,UAAW,CAAC,EACxDC,EAASJ,EAAS,OAAO,CAACE,EAAGC,IAAMD,EAAIC,EAAE,OAAQ,CAAC,EAClDE,EAAaL,EAAS,OAAO,CAACE,EAAGC,IAAMD,EAAIC,EAAE,WAAY,CAAC,EAC1DG,EAAUD,EAAa,IACvBE,EAAMD,EAAU,EAAI,KAAK,MAAOL,EAAYK,EAAW,EAAE,EAAI,GAAK,EAClEE,EAAe,IAAI,IACzB,QAAWL,KAAKH,EACd,QAAWS,KAAK,OAAO,KAAKN,EAAE,aAAa,EAAGK,EAAa,IAAIC,CAAC,EAElE,IAAMC,EAAWT,IAAc,EAAI,EAAI,KAAK,IAAI,GAAIA,EAAYO,EAAa,MAAQP,CAAS,EAC9F,MAAO,CACL,UAAWP,EAAM,UACjB,gBAAiBA,EAAM,YAAc,KAAO,EAAIE,EAAMF,EAAM,UAC5D,kBAAmBM,EAAS,OAC5B,UAAAC,EACA,OAAAG,EACA,IAAAG,EACA,SAAAG,EACA,gBAAiBF,EAAa,KAC9B,WAAAH,CACF,CACF","names":["createContext","useContext","useEffect","useState","jsx","AudioStatusContext","createContext","AudioStatusProvider","disabled","children","status","setStatus","useState","useEffect","cancelled","initAudio","audioWarning","useAudioStatus","useContext","pending","setPostExitAction","action","consumePostExitAction","a","silentExit","setSilentExit","v","consumeSilentExit","state","start","now","addChapter","entry","report","chapters","wordCount","a","c","errors","practiceMs","minutes","wpm","errorWordSet","w","accuracy"]}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{a as y,b as m,c as l,d as b}from"./chunk-E6BBQALJ.js";import{spawn as w}from"child_process";import{appendFile as S,mkdir as k,rename as Q,writeFile as $}from"fs/promises";import{join as d,dirname as v}from"path";function A(e){return e.replace(/'/g,"''")}function h(e,n,t=1){let r=A(e);if(n==="wav")return["-NoProfile","-Command",`(New-Object System.Media.SoundPlayer '${r}').PlaySync();`];let a=Math.round(3e3/t),i=t===1?"":`$p.SpeedRatio = ${t}; `;return["-NoProfile","-ExecutionPolicy","Bypass","-Command",`Add-Type -AssemblyName presentationCore; $p = New-Object System.Windows.Media.MediaPlayer; $p.Open([Uri]::new('${r}')); ${i}$p.Play(); Start-Sleep -Milliseconds ${a}`]}var C=[{kind:"afplay",platform:"unix",cmd:"afplay",args:(e,n,t=1)=>t!==1?["-r",String(t),e]:[e],supports:"both"},{kind:"ffplay",platform:"unix",cmd:"ffplay",args:(e,n,t=1)=>t!==1?["-nodisp","-autoexit","-loglevel","quiet","-af",`atempo=${t}`,e]:["-nodisp","-autoexit","-loglevel","quiet",e],supports:"both"},{kind:"mpg123",platform:"unix",cmd:"mpg123",args:e=>["-q",e],supports:"mp3"},{kind:"paplay",platform:"unix",cmd:"paplay",args:e=>[e],supports:"wav"},{kind:"aplay",platform:"unix",cmd:"aplay",args:e=>["-q",e],supports:"wav"},{kind:"powershell",platform:"win",cmd:"powershell",args:h,supports:"both"},{kind:"pwsh",platform:"win",cmd:"pwsh",args:h,supports:"both"}];function T(e,n){return`https://dict.youdao.com/dictvoice?audio=${encodeURIComponent(e)}&type=${n}`}function R(e){return`https://api.dictionaryapi.dev/api/v2/entries/en/${encodeURIComponent(e.toLowerCase())}`}function q(e,n){if(!Array.isArray(e))return null;let t=[];for(let r of e){let a=r?.phonetics;if(Array.isArray(a))for(let i of a)typeof i?.audio=="string"&&i.audio&&t.push(i.audio)}return t.length===0?null:t.find(r=>r.includes(`-${n}.mp3`))??t[0]}async function B(e,n,t){let a=T(e,n==="us"?2:1),i=await t(a,{headersTimeout:8e3,bodyTimeout:2e4});if(i.statusCode>=400)return null;let u=Buffer.from(await i.body.arrayBuffer());return u.length>=1024?u:null}async function F(e,n,t){let r=await t(R(e),{headersTimeout:8e3,bodyTimeout:2e4});if(r.statusCode>=400)return null;let a;try{a=await r.body.json()}catch{return null}let i=q(a,n);if(!i)return null;let u=await t(i,{headersTimeout:8e3,bodyTimeout:2e4});if(u.statusCode>=400)return null;let s=Buffer.from(await u.body.arrayBuffer());return s.length>=256?s:null}var D={youdao:B,dictapi:F},o=null,c=null;async function U(e){return new Promise(n=>{let t=w("where.exe",[e],{stdio:"ignore",windowsHide:!0});t.on("error",()=>n(!1)),t.on("exit",r=>n(r===0)),setTimeout(()=>{t.kill(),n(!1)},150)})}async function N(e){return new Promise(n=>{let t=w(e,["--version"],{stdio:"ignore"});t.on("error",()=>n(!1)),t.on("exit",()=>n(!0)),setTimeout(()=>{t.kill(),n(!1)},150)})}async function E(e){return process.platform==="win32"?U(e):N(e)}async function I(){let e=process.platform==="win32"?"win":"unix",n=C.filter(i=>i.platform===e),t=await Promise.all(n.map(async i=>[i,await E(i.cmd)])),r=null,a=null;for(let[i,u]of t)if(u&&(!r&&(i.supports==="wav"||i.supports==="both")&&(r=i),!a&&(i.supports==="mp3"||i.supports==="both")&&(a=i),r&&a))break;return{wav:r,mp3:a}}async function M(){return(await import("p-queue")).default}async function J(e){if(o)return o;let n=await M();if(e)return o={disabled:!0,wavPlayer:null,mp3Player:null,warning:null,keyQueue:new n({concurrency:1}),feedbackQueue:new n({concurrency:1}),pronQueue:new n({concurrency:1})},o;let{wav:t,mp3:r}=await I(),a=null;return!t&&!r?a=process.platform==="win32"?"No audio player found on PATH (looked for pwsh/powershell). Install PowerShell or add ffplay to PATH. Sounds disabled.":"No audio player found on PATH (looked for afplay/ffplay/mpg123/paplay/aplay). Sounds disabled.":r||(a="No MP3 player found; word pronunciations will be skipped."),o={disabled:!t&&!r,wavPlayer:t,mp3Player:r,warning:a,keyQueue:new n({concurrency:2}),feedbackQueue:new n({concurrency:1}),pronQueue:new n({concurrency:1})},o}function O(){let e=process.env.QWERTY_AUDIO_DEBUG;return!!e&&e!=="0"&&e.toLowerCase()!=="false"}function _(){return d(y.root,"cache","audio-debug.log")}async function j(e){try{let n=_();await k(v(n),{recursive:!0}),await S(n,`${e}
|
|
2
|
+
`)}catch{}}function H(e,n,t,r=1){let a=O();try{let i=e.args(n,t,r),u=w(e.cmd,i,{detached:!0,stdio:a?["ignore","pipe","pipe"]:"ignore",windowsHide:!0});if(u.on("error",()=>{}),a){let s=new Date().toISOString(),P=[];u.stderr?.on("data",f=>P.push(f)),u.stdout?.resume(),u.on("exit",f=>{let g=Buffer.concat(P).toString("utf8").trim();j(`[${s}] ${e.cmd} ${i.join(" ")} | exit=${f??"null"}${g?` | stderr=${g.replace(/\n/g," ")}`:""}`)})}u.unref()}catch{}}function p(e,n,t=1){if(!o||o.disabled)return;let r=n==="wav"?o.wavPlayer:o.mp3Player;r&&H(r,e,n,t)}function V(){!o||o.disabled||o.keyQueue.size>=2||o.keyQueue.add(async()=>{p(d(l(),"sounds","key-default.wav"),"wav"),await new Promise(e=>setTimeout(e,30))})}function X(){!o||o.disabled||o.feedbackQueue.add(async()=>{p(d(l(),"sounds","correct.wav"),"wav"),await new Promise(e=>setTimeout(e,50))})}function Z(){!o||o.disabled||o.feedbackQueue.add(async()=>{p(d(l(),"sounds","wrong.wav"),"wav"),await new Promise(e=>setTimeout(e,50))})}async function K(){return c||(c=(await import("undici")).request,c)}async function x(e,n,t){let r=y.audioCache(e,n,t);if(await b(r))return r;await k(v(r),{recursive:!0});try{let a=await K(),i=await D[t](e,n,a);if(!i)return null;let u=`${r}.tmp`;return await $(u,i),await Q(u,r),r}catch{return null}}async function ee(e,n,t=1,r="youdao"){!o||o.disabled||!o.mp3Player||(await m(),await o.pronQueue.add(async()=>{let a=await x(e,n,r);a&&p(a,"mp3",t)}))}async function ne(e,n,t="youdao"){!o||o.disabled||!o.mp3Player||(await m(),o.pronQueue.add(async()=>{await x(e,n,t)}))}function te(){return o?.warning??null}function re(){return o?.disabled??!0}function oe(){let e=o?.wavPlayer,n=o?.mp3Player;return{wav:e?{kind:e.kind,cmd:e.cmd}:null,mp3:n?{kind:n.kind,cmd:n.cmd}:null}}export{J as a,V as b,X as c,Z as d,ee as e,ne as f,te as g,re as h,oe as i};
|
|
3
|
+
//# sourceMappingURL=chunk-BIBS2Q3E.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/infra/audio.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\nimport { appendFile, mkdir, rename, writeFile } from 'node:fs/promises';\nimport { join, dirname } from 'node:path';\nimport { paths, packageAssetsDir, ensureDirs } from './paths.js';\nimport { exists } from './fs-store.js';\n\n// undici and p-queue are dynamically imported at first use to keep them out\n// of the boot module graph — saves ~50ms at startup for the menu/--version\n// paths that don't touch audio at all.\ntype RequestFn = typeof import('undici').request;\ntype PQueueCtor = typeof import('p-queue').default;\n\ntype PlayerKind = 'afplay' | 'paplay' | 'aplay' | 'mpg123' | 'ffplay' | 'powershell' | 'pwsh';\n\ntype Player = {\n kind: PlayerKind;\n cmd: string;\n args: (file: string, fileKind: 'wav' | 'mp3', rate?: number) => string[];\n supports: 'wav' | 'mp3' | 'both';\n platform: 'unix' | 'win';\n};\n\n// Escape single quotes inside PowerShell single-quoted string literal: ' → ''.\n// This is the only escaping required because single-quoted PowerShell strings\n// don't interpolate $(...) or backticks.\nfunction escapePwshPath(file: string): string {\n return file.replace(/'/g, \"''\");\n}\n\n// Build PowerShell args. WAV uses SoundPlayer.PlaySync() which blocks the\n// child until audio finishes (clean, fast exit). MP3 uses MediaPlayer with a\n// 3s Start-Sleep — MediaPlayer.Play() is non-blocking, so we keep the process\n// alive long enough for typical pronunciations to finish.\n// rate is the MP3 playback speed (0.5–1.5). WAV ignores rate because\n// SoundPlayer has no speed control and keystroke clicks are too short to\n// benefit. At rate=0.5 audio is 2x as long, so the sleep is scaled inversely.\nfunction buildPwshArgs(file: string, fileKind: 'wav' | 'mp3', rate = 1): string[] {\n const escaped = escapePwshPath(file);\n if (fileKind === 'wav') {\n return [\n '-NoProfile',\n '-Command',\n `(New-Object System.Media.SoundPlayer '${escaped}').PlaySync();`,\n ];\n }\n const sleepMs = Math.round(3000 / rate);\n const speedRatioStmt = rate === 1 ? '' : `$p.SpeedRatio = ${rate}; `;\n return [\n '-NoProfile',\n '-ExecutionPolicy', 'Bypass',\n '-Command',\n `Add-Type -AssemblyName presentationCore; $p = New-Object System.Windows.Media.MediaPlayer; $p.Open([Uri]::new('${escaped}')); ${speedRatioStmt}$p.Play(); Start-Sleep -Milliseconds ${sleepMs}`,\n ];\n}\n\nconst CANDIDATES: Player[] = [\n {\n kind: 'afplay',\n platform: 'unix',\n cmd: 'afplay',\n args: (f, _k, r = 1) => (r !== 1 ? ['-r', String(r), f] : [f]),\n supports: 'both',\n },\n {\n kind: 'ffplay',\n platform: 'unix',\n cmd: 'ffplay',\n args: (f, _k, r = 1) =>\n r !== 1\n ? ['-nodisp', '-autoexit', '-loglevel', 'quiet', '-af', `atempo=${r}`, f]\n : ['-nodisp', '-autoexit', '-loglevel', 'quiet', f],\n supports: 'both',\n },\n // mpg123 has no clean fractional-rate flag; pronunciationRate is silently\n // ignored when this player is selected. ffplay is preferred on Linux so this\n // affects only the rare install with mpg123 but no ffplay.\n { kind: 'mpg123', platform: 'unix', cmd: 'mpg123', args: (f) => ['-q', f], supports: 'mp3' },\n { kind: 'paplay', platform: 'unix', cmd: 'paplay', args: (f) => [f], supports: 'wav' },\n { kind: 'aplay', platform: 'unix', cmd: 'aplay', args: (f) => ['-q', f], supports: 'wav' },\n // Windows: powershell.exe (PowerShell 5.1) first — ships in every Windows\n // install and runs on .NET Framework, where System.Media.SoundPlayer and\n // PresentationCore (WPF) MediaPlayer are always reachable. pwsh (PowerShell\n // 7+) is the fallback: it only exists when the user installed it, and on a\n // bare .NET runtime SoundPlayer/MediaPlayer can't load because they live in\n // the Microsoft.WindowsDesktop.App shared framework. Preferring pwsh first\n // caused silent failure for users without the Desktop Runtime.\n { kind: 'powershell', platform: 'win', cmd: 'powershell', args: buildPwshArgs, supports: 'both' },\n { kind: 'pwsh', platform: 'win', cmd: 'pwsh', args: buildPwshArgs, supports: 'both' },\n];\n\ntype PronSource = 'youdao' | 'dictapi';\n\ntype SourceFetcher = (\n word: string,\n accent: 'us' | 'uk',\n request: RequestFn,\n) => Promise<Buffer | null>;\n\nfunction youdaoBuildUrl(word: string, type: 1 | 2): string {\n return `https://dict.youdao.com/dictvoice?audio=${encodeURIComponent(word)}&type=${type}`;\n}\n\nfunction dictapiBuildLookupUrl(word: string): string {\n return `https://api.dictionaryapi.dev/api/v2/entries/en/${encodeURIComponent(word.toLowerCase())}`;\n}\n\n// dictionaryapi.dev returns Array<{ phonetics: Array<{ audio?: string }> }>.\n// Prefer the audio URL whose filename ends in -<accent>.mp3 (us/uk); fall back\n// to any non-empty audio URL — better to play a different accent than nothing.\nfunction pickDictapiAudioUrl(json: unknown, accent: 'us' | 'uk'): string | null {\n if (!Array.isArray(json)) return null;\n const audios: string[] = [];\n for (const e of json) {\n const ps = (e as { phonetics?: Array<{ audio?: string }> })?.phonetics;\n if (!Array.isArray(ps)) continue;\n for (const p of ps) {\n if (typeof p?.audio === 'string' && p.audio) audios.push(p.audio);\n }\n }\n if (audios.length === 0) return null;\n return audios.find((u) => u.includes(`-${accent}.mp3`)) ?? audios[0]!;\n}\n\nasync function youdaoFetch(\n word: string,\n accent: 'us' | 'uk',\n request: RequestFn,\n): Promise<Buffer | null> {\n const type: 1 | 2 = accent === 'us' ? 2 : 1;\n const url = youdaoBuildUrl(word, type);\n const res = await request(url, { headersTimeout: 8000, bodyTimeout: 20000 });\n if (res.statusCode >= 400) return null;\n const buf = Buffer.from(await res.body.arrayBuffer());\n return buf.length >= 1024 ? buf : null;\n}\n\nasync function dictapiFetch(\n word: string,\n accent: 'us' | 'uk',\n request: RequestFn,\n): Promise<Buffer | null> {\n const idx = await request(dictapiBuildLookupUrl(word), {\n headersTimeout: 8000,\n bodyTimeout: 20000,\n });\n if (idx.statusCode >= 400) return null;\n let json: unknown;\n try {\n json = await idx.body.json();\n } catch {\n return null;\n }\n const audioUrl = pickDictapiAudioUrl(json, accent);\n if (!audioUrl) return null;\n const res = await request(audioUrl, { headersTimeout: 8000, bodyTimeout: 20000 });\n if (res.statusCode >= 400) return null;\n const buf = Buffer.from(await res.body.arrayBuffer());\n // Short words (\"a\", \"I\") legitimately produce small files at Wiktionary;\n // 256 bytes still catches HTML error pages while not rejecting real clips.\n return buf.length >= 256 ? buf : null;\n}\n\nconst PRON_SOURCES: Record<PronSource, SourceFetcher> = {\n youdao: youdaoFetch,\n dictapi: dictapiFetch,\n};\n\ntype AudioRuntime = {\n disabled: boolean;\n wavPlayer: Player | null;\n mp3Player: Player | null;\n warning: string | null;\n keyQueue: InstanceType<PQueueCtor>;\n feedbackQueue: InstanceType<PQueueCtor>;\n pronQueue: InstanceType<PQueueCtor>;\n};\n\nlet runtime: AudioRuntime | null = null;\nlet cachedRequest: RequestFn | null = null;\n\n// Windows probe: where.exe is reliable for PATH lookup (exit 0 = found).\n// PowerShell 5.x doesn't accept --version, so the old Unix-style probe was\n// timing out and miscategorising every Windows install as \"no audio\".\nasync function isExecutableWin(cmd: string): Promise<boolean> {\n return new Promise((resolve) => {\n const probe = spawn('where.exe', [cmd], { stdio: 'ignore', windowsHide: true });\n probe.on('error', () => resolve(false));\n probe.on('exit', (code) => resolve(code === 0));\n setTimeout(() => {\n probe.kill();\n resolve(false);\n }, 150);\n });\n}\n\nasync function isExecutableUnix(cmd: string): Promise<boolean> {\n return new Promise((resolve) => {\n const probe = spawn(cmd, ['--version'], { stdio: 'ignore' });\n probe.on('error', () => resolve(false));\n probe.on('exit', () => resolve(true));\n setTimeout(() => {\n probe.kill();\n resolve(false);\n }, 150);\n });\n}\n\nasync function isExecutable(cmd: string): Promise<boolean> {\n return process.platform === 'win32' ? isExecutableWin(cmd) : isExecutableUnix(cmd);\n}\n\nasync function detect(): Promise<{ wav: Player | null; mp3: Player | null }> {\n // Filter by platform — no point probing afplay on Windows or pwsh on Linux.\n // Parallel probe within the filtered set keeps boot time at ~150ms regardless.\n const wantPlatform: 'unix' | 'win' = process.platform === 'win32' ? 'win' : 'unix';\n const eligible = CANDIDATES.filter((p) => p.platform === wantPlatform);\n const checks = await Promise.all(\n eligible.map(async (p) => [p, await isExecutable(p.cmd)] as const),\n );\n let wav: Player | null = null;\n let mp3: Player | null = null;\n for (const [p, ok] of checks) {\n if (!ok) continue;\n if (!wav && (p.supports === 'wav' || p.supports === 'both')) wav = p;\n if (!mp3 && (p.supports === 'mp3' || p.supports === 'both')) mp3 = p;\n if (wav && mp3) break;\n }\n return { wav, mp3 };\n}\n\nasync function loadPQueue(): Promise<PQueueCtor> {\n const mod = await import('p-queue');\n return mod.default;\n}\n\nexport async function initAudio(disabledByConfig: boolean): Promise<AudioRuntime> {\n if (runtime) return runtime;\n const PQueue = await loadPQueue();\n if (disabledByConfig) {\n runtime = {\n disabled: true,\n wavPlayer: null,\n mp3Player: null,\n warning: null,\n keyQueue: new PQueue({ concurrency: 1 }),\n feedbackQueue: new PQueue({ concurrency: 1 }),\n pronQueue: new PQueue({ concurrency: 1 }),\n };\n return runtime;\n }\n const { wav, mp3 } = await detect();\n let warning: string | null = null;\n if (!wav && !mp3) {\n warning =\n process.platform === 'win32'\n ? 'No audio player found on PATH (looked for pwsh/powershell). Install PowerShell or add ffplay to PATH. Sounds disabled.'\n : 'No audio player found on PATH (looked for afplay/ffplay/mpg123/paplay/aplay). Sounds disabled.';\n } else if (!mp3) {\n warning = 'No MP3 player found; word pronunciations will be skipped.';\n }\n runtime = {\n disabled: !wav && !mp3,\n wavPlayer: wav,\n mp3Player: mp3,\n warning,\n keyQueue: new PQueue({ concurrency: 2 }),\n feedbackQueue: new PQueue({ concurrency: 1 }),\n pronQueue: new PQueue({ concurrency: 1 }),\n };\n return runtime;\n}\n\nfunction audioDebugEnabled(): boolean {\n const v = process.env.QWERTY_AUDIO_DEBUG;\n return !!v && v !== '0' && v.toLowerCase() !== 'false';\n}\n\nfunction audioDebugLogPath(): string {\n return join(paths.root, 'cache', 'audio-debug.log');\n}\n\nasync function appendAudioDebug(line: string): Promise<void> {\n // Best-effort write; never block the audio path and never surface errors.\n try {\n const p = audioDebugLogPath();\n await mkdir(dirname(p), { recursive: true });\n await appendFile(p, `${line}\\n`);\n } catch {\n /* ignore */\n }\n}\n\nfunction spawnPlay(player: Player, file: string, fileKind: 'wav' | 'mp3', rate = 1): void {\n const debug = audioDebugEnabled();\n try {\n const args = player.args(file, fileKind, rate);\n const child = spawn(player.cmd, args, {\n detached: true,\n stdio: debug ? ['ignore', 'pipe', 'pipe'] : 'ignore',\n windowsHide: true,\n });\n child.on('error', () => {\n // swallow; runtime will be disabled if many failures stack up\n });\n if (debug) {\n const ts = new Date().toISOString();\n const stderrChunks: Buffer[] = [];\n child.stderr?.on('data', (b: Buffer) => stderrChunks.push(b));\n child.stdout?.resume();\n child.on('exit', (code) => {\n const err = Buffer.concat(stderrChunks).toString('utf8').trim();\n void appendAudioDebug(\n `[${ts}] ${player.cmd} ${args.join(' ')} | exit=${code ?? 'null'}${err ? ` | stderr=${err.replace(/\\n/g, ' ')}` : ''}`,\n );\n });\n }\n child.unref();\n } catch {\n /* fail-soft */\n }\n}\n\nfunction playFile(file: string, kind: 'wav' | 'mp3', rate = 1): void {\n if (!runtime || runtime.disabled) return;\n const player = kind === 'wav' ? runtime.wavPlayer : runtime.mp3Player;\n if (!player) return;\n spawnPlay(player, file, kind, rate);\n}\n\nexport function playKeystroke(): void {\n if (!runtime || runtime.disabled) return;\n // Drop if queue is saturated; we never want to delay typing.\n if (runtime.keyQueue.size >= 2) return;\n void runtime.keyQueue.add(async () => {\n playFile(join(packageAssetsDir(), 'sounds', 'key-default.wav'), 'wav');\n await new Promise((r) => setTimeout(r, 30));\n });\n}\n\nexport function playCorrect(): void {\n if (!runtime || runtime.disabled) return;\n void runtime.feedbackQueue.add(async () => {\n playFile(join(packageAssetsDir(), 'sounds', 'correct.wav'), 'wav');\n await new Promise((r) => setTimeout(r, 50));\n });\n}\n\nexport function playWrong(): void {\n if (!runtime || runtime.disabled) return;\n void runtime.feedbackQueue.add(async () => {\n playFile(join(packageAssetsDir(), 'sounds', 'wrong.wav'), 'wav');\n await new Promise((r) => setTimeout(r, 50));\n });\n}\n\nasync function loadRequest(): Promise<RequestFn> {\n if (cachedRequest) return cachedRequest;\n const mod = await import('undici');\n cachedRequest = mod.request;\n return cachedRequest;\n}\n\nasync function downloadPronunciation(\n word: string,\n accent: 'us' | 'uk',\n source: PronSource,\n): Promise<string | null> {\n const cacheFile = paths.audioCache(word, accent, source);\n if (await exists(cacheFile)) return cacheFile;\n await mkdir(dirname(cacheFile), { recursive: true });\n try {\n const request = await loadRequest();\n const buf = await PRON_SOURCES[source](word, accent, request);\n if (!buf) return null;\n const tmp = `${cacheFile}.tmp`;\n await writeFile(tmp, buf);\n await rename(tmp, cacheFile);\n return cacheFile;\n } catch {\n return null;\n }\n}\n\nexport async function playPronunciation(\n word: string,\n accent: 'us' | 'uk',\n rate = 1,\n source: PronSource = 'youdao',\n): Promise<void> {\n if (!runtime || runtime.disabled || !runtime.mp3Player) return;\n await ensureDirs();\n await runtime.pronQueue.add(async () => {\n const file = await downloadPronunciation(word, accent, source);\n if (file) playFile(file, 'mp3', rate);\n });\n}\n\nexport async function prefetchPronunciation(\n word: string,\n accent: 'us' | 'uk',\n source: PronSource = 'youdao',\n): Promise<void> {\n if (!runtime || runtime.disabled || !runtime.mp3Player) return;\n await ensureDirs();\n void runtime.pronQueue.add(async () => {\n await downloadPronunciation(word, accent, source);\n });\n}\n\nexport function audioWarning(): string | null {\n return runtime?.warning ?? null;\n}\n\nexport function audioDisabled(): boolean {\n return runtime?.disabled ?? true;\n}\n\nexport type PlayerSummary = { kind: PlayerKind; cmd: string } | null;\n\nexport function audioPlayers(): { wav: PlayerSummary; mp3: PlayerSummary } {\n const w = runtime?.wavPlayer;\n const m = runtime?.mp3Player;\n return {\n wav: w ? { kind: w.kind, cmd: w.cmd } : null,\n mp3: m ? { kind: m.kind, cmd: m.cmd } : null,\n };\n}\n\n// Exposed for unit tests only — verifies PowerShell command construction and\n// candidate priority order.\nexport const __test = {\n buildPwshArgs,\n escapePwshPath,\n CANDIDATES,\n youdaoBuildUrl,\n dictapiBuildLookupUrl,\n pickDictapiAudioUrl,\n};\n"],"mappings":"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","names":["spawn","appendFile","mkdir","rename","writeFile","join","dirname","escapePwshPath","file","buildPwshArgs","fileKind","rate","escaped","sleepMs","speedRatioStmt","CANDIDATES","f","_k","r","youdaoBuildUrl","word","type","dictapiBuildLookupUrl","pickDictapiAudioUrl","json","accent","audios","e","ps","p","u","youdaoFetch","request","url","res","buf","dictapiFetch","idx","audioUrl","PRON_SOURCES","runtime","cachedRequest","isExecutableWin","cmd","resolve","probe","spawn","code","isExecutableUnix","isExecutable","detect","wantPlatform","eligible","checks","wav","mp3","ok","loadPQueue","initAudio","disabledByConfig","PQueue","warning","audioDebugEnabled","v","audioDebugLogPath","join","paths","appendAudioDebug","line","mkdir","dirname","appendFile","spawnPlay","player","debug","args","child","ts","stderrChunks","b","err","playFile","kind","playKeystroke","packageAssetsDir","playCorrect","playWrong","loadRequest","downloadPronunciation","source","cacheFile","exists","tmp","writeFile","rename","playPronunciation","ensureDirs","prefetchPronunciation","audioWarning","audioDisabled","audioPlayers","w","m"]}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{homedir as d}from"os";import{join as e,resolve as p}from"path";import{mkdir as s}from"fs/promises";import{fileURLToPath as f}from"url";var i=e(d(),".qwerty-cli"),a={root:i,config:e(i,"config.json"),registry:e(i,"registry.json"),dictsDir:e(i,"dicts"),dictFile:r=>e(i,"dicts",`${r}.json`),dictMeta:r=>e(i,"dicts",`${r}.meta.json`),stats:e(i,"stats.jsonl"),mistakes:e(i,"mistakes.json"),audioCacheDir:e(i,"cache","audio"),audioCache:(r,t,o="youdao")=>{let n=`${encodeURIComponent(r.toLowerCase())}-${t}.mp3`;return o==="youdao"?e(i,"cache","audio",n):e(i,"cache","audio",o,n)}};async function k(){await s(a.dictsDir,{recursive:!0}),await s(a.audioCacheDir,{recursive:!0})}function v(){let r=f(import.meta.url);return p(r,"..","..","assets")}import{mkdir as c,readFile as u,rename as l,writeFile as g,appendFile as w,access as y}from"fs/promises";import{dirname as m}from"path";import{randomBytes as h}from"crypto";async function S(r){try{return await y(r),!0}catch{return!1}}async function $(r){try{let t=await u(r,"utf8");return JSON.parse(t)}catch(t){if(t.code==="ENOENT")return null;throw t}}async function D(r,t){await c(m(r),{recursive:!0});let o=`${r}.${process.pid}.${h(4).toString("hex")}.tmp`;await g(o,JSON.stringify(t,null,2),"utf8"),await l(o,r)}async function C(r,t){await c(m(r),{recursive:!0}),await w(r,JSON.stringify(t)+`
|
|
2
|
+
`,"utf8")}async function b(r){try{return(await u(r,"utf8")).split(`
|
|
3
|
+
`).filter(o=>o.trim().length>0).map(o=>JSON.parse(o))}catch(t){if(t.code==="ENOENT")return[];throw t}}export{a,k as b,v as c,S as d,$ as e,D as f,C as g,b as h};
|
|
4
|
+
//# sourceMappingURL=chunk-E6BBQALJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/infra/paths.ts","../src/infra/fs-store.ts"],"sourcesContent":["import { homedir } from 'node:os';\nimport { join, resolve } from 'node:path';\nimport { mkdir } from 'node:fs/promises';\nimport { fileURLToPath } from 'node:url';\n\nconst ROOT = join(homedir(), '.qwerty-cli');\n\nexport const paths = {\n root: ROOT,\n config: join(ROOT, 'config.json'),\n registry: join(ROOT, 'registry.json'),\n dictsDir: join(ROOT, 'dicts'),\n dictFile: (id: string) => join(ROOT, 'dicts', `${id}.json`),\n dictMeta: (id: string) => join(ROOT, 'dicts', `${id}.meta.json`),\n stats: join(ROOT, 'stats.jsonl'),\n mistakes: join(ROOT, 'mistakes.json'),\n audioCacheDir: join(ROOT, 'cache', 'audio'),\n audioCache: (\n word: string,\n accent: 'us' | 'uk',\n source: 'youdao' | 'dictapi' = 'youdao',\n ) => {\n const file = `${encodeURIComponent(word.toLowerCase())}-${accent}.mp3`;\n return source === 'youdao'\n ? join(ROOT, 'cache', 'audio', file)\n : join(ROOT, 'cache', 'audio', source, file);\n },\n};\n\nexport async function ensureDirs(): Promise<void> {\n await mkdir(paths.dictsDir, { recursive: true });\n await mkdir(paths.audioCacheDir, { recursive: true });\n}\n\nexport function packageAssetsDir(): string {\n // Works for both bundled (dist/cli.js) and source (src/infra/paths.ts) layouts:\n // 2 levels up from this file lands at the package root.\n const here = fileURLToPath(import.meta.url);\n return resolve(here, '..', '..', 'assets');\n}\n","import { mkdir, readFile, rename, writeFile, appendFile, access } from 'node:fs/promises';\nimport { dirname } from 'node:path';\nimport { randomBytes } from 'node:crypto';\n\nexport async function exists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n\nexport async function readJson<T>(path: string): Promise<T | null> {\n try {\n const buf = await readFile(path, 'utf8');\n return JSON.parse(buf) as T;\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null;\n throw err;\n }\n}\n\nexport async function writeJsonAtomic(path: string, value: unknown): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n const tmp = `${path}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`;\n await writeFile(tmp, JSON.stringify(value, null, 2), 'utf8');\n await rename(tmp, path);\n}\n\nexport async function appendJsonl(path: string, line: unknown): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n await appendFile(path, JSON.stringify(line) + '\\n', 'utf8');\n}\n\nexport async function readJsonl<T>(path: string): Promise<T[]> {\n try {\n const buf = await readFile(path, 'utf8');\n return buf\n .split('\\n')\n .filter((l) => l.trim().length > 0)\n .map((l) => JSON.parse(l) as T);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return [];\n throw err;\n }\n}\n"],"mappings":"AAAA,OAAS,WAAAA,MAAe,KACxB,OAAS,QAAAC,EAAM,WAAAC,MAAe,OAC9B,OAAS,SAAAC,MAAa,cACtB,OAAS,iBAAAC,MAAqB,MAE9B,IAAMC,EAAOJ,EAAKD,EAAQ,EAAG,aAAa,EAE7BM,EAAQ,CACnB,KAAMD,EACN,OAAQJ,EAAKI,EAAM,aAAa,EAChC,SAAUJ,EAAKI,EAAM,eAAe,EACpC,SAAUJ,EAAKI,EAAM,OAAO,EAC5B,SAAWE,GAAeN,EAAKI,EAAM,QAAS,GAAGE,CAAE,OAAO,EAC1D,SAAWA,GAAeN,EAAKI,EAAM,QAAS,GAAGE,CAAE,YAAY,EAC/D,MAAON,EAAKI,EAAM,aAAa,EAC/B,SAAUJ,EAAKI,EAAM,eAAe,EACpC,cAAeJ,EAAKI,EAAM,QAAS,OAAO,EAC1C,WAAY,CACVG,EACAC,EACAC,EAA+B,WAC5B,CACH,IAAMC,EAAO,GAAG,mBAAmBH,EAAK,YAAY,CAAC,CAAC,IAAIC,CAAM,OAChE,OAAOC,IAAW,SACdT,EAAKI,EAAM,QAAS,QAASM,CAAI,EACjCV,EAAKI,EAAM,QAAS,QAASK,EAAQC,CAAI,CAC/C,CACF,EAEA,eAAsBC,GAA4B,CAChD,MAAMT,EAAMG,EAAM,SAAU,CAAE,UAAW,EAAK,CAAC,EAC/C,MAAMH,EAAMG,EAAM,cAAe,CAAE,UAAW,EAAK,CAAC,CACtD,CAEO,SAASO,GAA2B,CAGzC,IAAMC,EAAOV,EAAc,YAAY,GAAG,EAC1C,OAAOF,EAAQY,EAAM,KAAM,KAAM,QAAQ,CAC3C,CCvCA,OAAS,SAAAC,EAAO,YAAAC,EAAU,UAAAC,EAAQ,aAAAC,EAAW,cAAAC,EAAY,UAAAC,MAAc,cACvE,OAAS,WAAAC,MAAe,OACxB,OAAS,eAAAC,MAAmB,SAE5B,eAAsBC,EAAOC,EAAgC,CAC3D,GAAI,CACF,aAAMJ,EAAOI,CAAI,EACV,EACT,MAAQ,CACN,MAAO,EACT,CACF,CAEA,eAAsBC,EAAYD,EAAiC,CACjE,GAAI,CACF,IAAME,EAAM,MAAMV,EAASQ,EAAM,MAAM,EACvC,OAAO,KAAK,MAAME,CAAG,CACvB,OAASC,EAAK,CACZ,GAAKA,EAA8B,OAAS,SAAU,OAAO,KAC7D,MAAMA,CACR,CACF,CAEA,eAAsBC,EAAgBJ,EAAcK,EAA+B,CACjF,MAAMd,EAAMM,EAAQG,CAAI,EAAG,CAAE,UAAW,EAAK,CAAC,EAC9C,IAAMM,EAAM,GAAGN,CAAI,IAAI,QAAQ,GAAG,IAAIF,EAAY,CAAC,EAAE,SAAS,KAAK,CAAC,OACpE,MAAMJ,EAAUY,EAAK,KAAK,UAAUD,EAAO,KAAM,CAAC,EAAG,MAAM,EAC3D,MAAMZ,EAAOa,EAAKN,CAAI,CACxB,CAEA,eAAsBO,EAAYP,EAAcQ,EAA8B,CAC5E,MAAMjB,EAAMM,EAAQG,CAAI,EAAG,CAAE,UAAW,EAAK,CAAC,EAC9C,MAAML,EAAWK,EAAM,KAAK,UAAUQ,CAAI,EAAI;AAAA,EAAM,MAAM,CAC5D,CAEA,eAAsBC,EAAaT,EAA4B,CAC7D,GAAI,CAEF,OADY,MAAMR,EAASQ,EAAM,MAAM,GAEpC,MAAM;AAAA,CAAI,EACV,OAAQU,GAAMA,EAAE,KAAK,EAAE,OAAS,CAAC,EACjC,IAAKA,GAAM,KAAK,MAAMA,CAAC,CAAM,CAClC,OAASP,EAAK,CACZ,GAAKA,EAA8B,OAAS,SAAU,MAAO,CAAC,EAC9D,MAAMA,CACR,CACF","names":["homedir","join","resolve","mkdir","fileURLToPath","ROOT","paths","id","word","accent","source","file","ensureDirs","packageAssetsDir","here","mkdir","readFile","rename","writeFile","appendFile","access","dirname","randomBytes","exists","path","readJson","buf","err","writeJsonAtomic","value","tmp","appendJsonl","line","readJsonl","l"]}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{a as i,e as a,f as c}from"./chunk-
|
|
2
|
-
//# sourceMappingURL=chunk-
|
|
1
|
+
import{a as i,e as a,f as c}from"./chunk-E6BBQALJ.js";import{z as o}from"zod";var u=o.record(o.string(),o.object({count:o.number().int().nonnegative(),lastSeen:o.string(),dictIds:o.array(o.string()).default([])}));async function f(){let t=await a(i.mistakes);if(!t)return{};let e=u.safeParse(t);return e.success?e.data:(console.warn("Mistake book is corrupt; starting fresh"),{})}async function M(t){await c(i.mistakes,t)}function g(t,e,s,r=1){let n=t[e]??{count:0,lastSeen:new Date(0).toISOString(),dictIds:[]},k=n.dictIds.includes(s)?n.dictIds:[...n.dictIds,s];return{...t,[e]:{count:n.count+r,lastSeen:new Date().toISOString(),dictIds:k}}}function B(t,e){return Object.entries(t).sort((s,r)=>r[1].count-s[1].count).slice(0,e)}export{f as a,M as b,g as c,B as d};
|
|
2
|
+
//# sourceMappingURL=chunk-G3DQB7FI.js.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{a as w,g as h,h as S}from"./chunk-
|
|
2
|
-
//# sourceMappingURL=chunk-
|
|
1
|
+
import{a as w,g as h,h as S}from"./chunk-E6BBQALJ.js";import{z as i}from"zod";var D=i.object({ts:i.string(),dictId:i.string(),chapter:i.number().int().nonnegative(),mode:i.string(),wordCount:i.number().int().nonnegative(),errors:i.number().int().nonnegative(),durationMs:i.number().int().nonnegative(),perWordErrors:i.record(i.string(),i.number().int().nonnegative()).default({})});async function k(e){await h(w.stats,e)}async function C(){return(await S(w.stats)).map(t=>D.safeParse(t)).filter(t=>t.success).map(t=>t.data)}function y(e){if(e.durationMs===0)return 0;let t=e.durationMs/6e4;return Math.round(e.wordCount/t*10)/10}function b(e){if(e.wordCount===0)return 1;let t=Object.keys(e.perWordErrors).length;return Math.max(0,Math.min(1,(e.wordCount-t)/e.wordCount))}function R(e,t=new Date){if(e.length===0)return 0;let a=new Set;for(let n of e)a.add(n.ts.slice(0,10));let r=0,s=new Date(t);for(;;){let n=s.toISOString().slice(0,10);if(!a.has(n))break;r++,s.setUTCDate(s.getUTCDate()-1)}return r}var l=["\u2581","\u2582","\u2583","\u2584","\u2585","\u2586","\u2587","\u2588"];function v(e){if(e.length===0)return"";let t=Math.max(...e,1),a=Math.min(...e,0),r=Math.max(1,t-a);return e.map(s=>{let n=Math.floor((s-a)/r*(l.length-1));return l[Math.max(0,Math.min(l.length-1,n))]}).join("")}function P(e,t,a=new Date){if(e.length===0||t<=0)return{avgWpm:0,peakWpmLifetime:0,avgAccPct:0,sessionsInWindow:0,days:t};let r=new Date(a);r.setUTCDate(r.getUTCDate()-(t-1));let s=r.toISOString().slice(0,10),n=0,m=0,u=0,o=0,p=0;for(let c of e){let f=y(c);f>p&&(p=f),!(c.ts.slice(0,10)<s)&&(o++,n+=c.wordCount,m+=f*c.wordCount,u+=b(c)*c.wordCount)}if(n===0)return{avgWpm:0,peakWpmLifetime:p,avgAccPct:0,sessionsInWindow:o,days:t};let g=Math.round(m/n*10)/10,d=Math.round(u/n*1e3)/10;return{avgWpm:g,peakWpmLifetime:p,avgAccPct:d,sessionsInWindow:o,days:t}}function A(e,t,a=new Date){let r=[],s=new Map;for(let m of e){let u=m.ts.slice(0,10),o=s.get(u)??[];o.push(m),s.set(u,o)}let n=new Date(a);n.setUTCDate(n.getUTCDate()-(t-1));for(let m=0;m<t;m++){let u=n.toISOString().slice(0,10),o=s.get(u)??[];if(o.length===0)r.push({date:u,wpm:0,accuracy:0,sessions:0});else{let p=o.reduce((d,c)=>d+y(c),0)/o.length,g=o.reduce((d,c)=>d+b(c),0)/o.length;r.push({date:u,wpm:p,accuracy:g,sessions:o.length})}n.setUTCDate(n.getUTCDate()+1)}return r}export{k as a,C as b,y as c,b as d,R as e,v as f,P as g,A as h};
|
|
2
|
+
//# sourceMappingURL=chunk-HJIGZU3E.js.map
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{a as L,b as R,c as H}from"./chunk-6ROGUGNX.js";import{a as C,b as k}from"./chunk-4GNY4LY7.js";import{a as B,b as D,c as w,e as I}from"./chunk-QG7ZTS2G.js";import{a as P,b as M,c as E,d as N,f as a}from"./chunk-3TQFRACL.js";var J="\x1B[?1049h\x1B[?25l\x1B[2J\x1B[H",z="\x1B[?25h\x1B[?1049l",v=!1,$=!1;function K(){if($)return;$=!0;let e=()=>{if(v){try{process.stdout.write(z)}catch{}v=!1}};process.once("exit",e),process.once("SIGINT",()=>{e(),process.exit(130)}),process.once("SIGTERM",()=>{e(),process.exit(143)})}function W(){v||process.stdout.isTTY&&process.env.QWERTY_NO_ALTSCREEN!=="1"&&(K(),process.stdout.write(J),v=!0)}function A(){if(v){try{process.stdout.write(z)}catch{}v=!1}}import{Suspense as re,lazy as T,useRef as oe}from"react";import{Box as ne,Text as ie,useApp as se,useInput as ae}from"ink";import{useEffect as _,useState as O}from"react";import{Box as Q,useStdout as X}from"ink";import{jsx as U}from"react/jsx-runtime";function F({children:e}){let{stdout:t}=X(),[s,r]=O(()=>({rows:t?.rows??24,cols:t?.columns??80}));return _(()=>{W();let n=()=>{r({rows:process.stdout.rows??24,cols:process.stdout.columns??80})};return process.stdout.on("resize",n),()=>{process.stdout.off("resize",n),A()}},[]),U(Q,{width:s.cols,height:s.rows,flexDirection:"column",children:e})}import{useState as Z}from"react";import{Box as y,Text as m,useApp as ee,useInput as te}from"ink";import{jsx as h,jsxs as g}from"react/jsx-runtime";function Y({cfg:e}){let[t,s]=Z(0),{exit:r}=ee(),n=M(),f=R(),p=N(),l=D(e.defaultDict),o=p.mainMenu.items,b=c=>{if(c&&e.defaultDict){H({type:"stealth",dictId:e.defaultDict,chapterIndex:0,mode:e.defaultMode}),r();return}e.defaultDict?n.navigate({name:"practice",params:{dictId:e.defaultDict,chapterIndex:0,mode:e.defaultMode,stealth:c}}):n.navigate({name:"dict",params:{pickerMode:"choose-then-practice"}})},d=[{key:"p",label:o.practiceLabel,hint:e.defaultDict?o.practiceHintWith(I(l,24)):o.practiceHintNone,run:()=>b(e.stealth==="default")}];(e.stealth==="menu"||e.stealth==="default")&&d.push({key:"b",label:o.stealthLabel,hint:o.stealthHint,run:()=>b(!0)}),d.push({key:"d",label:o.dictLabel,hint:o.dictHint,run:()=>n.navigate({name:"dict"})},{key:"w",label:o.wordLabel,hint:o.wordHint,run:()=>n.navigate({name:"word"})},{key:"s",label:o.statsLabel,hint:o.statsHint,run:()=>n.navigate({name:"stats"})},{key:"c",label:o.configLabel,hint:o.configHint,run:()=>n.navigate({name:"config"})},{key:"q",label:o.quitLabel,hint:o.quitHint,run:()=>r()});let V=Math.max(...d.map(c=>w(c.label)))+4;return te((c,S)=>{if(S.escape){r();return}if(S.upArrow&&s(u=>(u-1+d.length)%d.length),S.downArrow&&s(u=>(u+1)%d.length),S.return){d[t].run();return}if(c==="?"){n.navigate({name:"help"});return}for(let u of d)if(c===u.key){u.run();return}}),g(y,{flexDirection:"column",paddingX:2,paddingY:1,width:"100%",children:[e.stealth!=="default"&&g(y,{children:[h(m,{bold:!0,color:a.accent,children:p.app.title}),g(m,{color:a.muted,children:[" \xB7 ",p.app.subtitle]})]}),h(y,{marginTop:e.stealth==="default"?0:2,flexDirection:"column",children:d.map((c,S)=>{let u=S===t,G=" ".repeat(Math.max(0,V-w(c.label)));return g(y,{children:[h(m,{color:u?a.accent:a.muted,children:u?"\u258C ":" "}),g(m,{color:u?a.accent:a.muted,children:["[",c.key,"]"]}),h(m,{children:" "}),g(m,{bold:u,color:u?a.text:a.muted,children:[c.label,G]}),h(m,{color:a.muted,children:c.hint})]},c.key)})}),h(y,{marginTop:2,children:g(m,{color:a.muted,children:[p.mainMenu.hint," \xB7 ",p.mainMenu.helpHint]})}),f.warning&&h(y,{marginTop:1,children:h(m,{color:a.warning,children:p.audio.noPlayer})})]})}import{jsx as i}from"react/jsx-runtime";var ce=T(()=>import("./PracticeScreen-S5BGC4VI.js").then(e=>({default:e.PracticeScreen}))),ue=T(()=>import("./DictBrowser-EJ4N3W5P.js").then(e=>({default:e.DictBrowser}))),pe=T(()=>import("./ConfigEditor-U6VNM4X5.js").then(e=>({default:e.ConfigEditor}))),le=T(()=>import("./StatsViewer-RZ4NNMSI.js").then(e=>({default:e.StatsViewer}))),de=T(()=>import("./WordLookup-KEC2G7EI.js").then(e=>({default:e.WordLookup}))),me=T(()=>import("./HelpScreen-QNZRWVXA.js").then(e=>({default:e.HelpScreen})));function fe(){return i(ne,{alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:i(ie,{color:a.muted,children:"\u2026"})})}function _e({initial:e,initialCfg:t,inline:s=!1}){return i(C,{initialCfg:t,children:i(he,{children:i(B,{children:i(L,{disabled:!t.sounds.master,children:i(P,{initial:e,children:s?i(j,{inline:!0}):i(F,{children:i(j,{})})})})})})})}function he({children:e}){let{cfg:t}=k();return i(E,{pref:t.language,children:e})}function ge(e){if(e.name==="practice"){let t=e.params;return`practice:${t.dictId}:${t.chapterIndex}:${t.mode}:${t.stealth?"s":"n"}`}return e.name}function j({inline:e=!1}){let t=M(),{cfg:s}=k(),{exit:r}=se(),n=oe(null);ae((o,b)=>{b.ctrl&&o==="c"&&r()});let f=t.current,p=ge(f);n.current!==p&&(!e&&process.stdout.isTTY&&process.stdout.write("\x1B[2J\x1B[H"),n.current=p);let l=(()=>{switch(f.name){case"main":return i(Y,{cfg:s});case"practice":return i(ce,{params:f.params});case"dict":return i(ue,{params:f.params});case"config":return i(pe,{});case"stats":return i(le,{});case"word":return i(de,{});case"help":return i(me,{})}})();return i(re,{fallback:i(fe,{}),children:l})}import x from"chalk";import xe from"boxen";function tt(){A()}function q(e,t){let s=Math.floor(e/1e3),r=Math.floor(s/60),n=s%60;return t==="zh"?r===0?`${n} \u79D2`:`${r} \u5206 ${n} \u79D2`:r===0?`${n}s`:`${r}m ${n}s`}function be(e){return e>=90?x.green:e<75?x.dim:t=>t}function Se(e,t,s){let r=[],n=[t.report.duration];e.chaptersCompleted===0?n.push(t.report.notPracticed):(n.push(t.report.practiced,t.report.chapters,t.report.words,t.report.accuracy,t.report.wpm),e.newMistakeWords>0&&n.push(t.report.newMistakes));let f=Math.max(...n.map(w)),p=o=>o+" ".repeat(Math.max(0,f-w(o))),l=(o,b)=>`${x.dim(p(o))} ${b}`;if(r.push(l(t.report.duration,q(e.totalDurationMs,s))),e.chaptersCompleted===0)r.push(x.dim(t.report.notPracticed));else{r.push(l(t.report.practiced,q(e.practiceMs,s))),r.push(l(t.report.chapters,String(e.chaptersCompleted))),r.push(l(t.report.words,String(e.wordCount)));let o=Math.round(e.accuracy*1e3)/10;r.push(l(t.report.accuracy,be(o)(`${o}%`))),r.push(l(t.report.wpm,String(e.wpm))),e.newMistakeWords>0&&r.push(l(t.report.newMistakes,x.red(String(e.newMistakeWords))))}return r.push(""),r.push(x.dim.italic(t.report.farewell)),r}function rt(e,t,s){if(e.startedAt===null&&e.chaptersCompleted===0)return;let r=Se(e,t,s).join(`
|
|
2
|
+
`);console.log(xe(r,{title:x.bold.cyan(t.report.title),titleAlignment:"left",borderStyle:"round",borderColor:"gray",padding:{top:0,bottom:0,left:3,right:3},margin:{top:1,bottom:1,left:2,right:0}}))}export{W as a,_e as b,tt as c,rt as d};
|
|
3
|
+
//# sourceMappingURL=chunk-K6THKMFC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/util/altscreen.ts","../src/ui/App.tsx","../src/ui/Fullscreen.tsx","../src/ui/screens/MainMenu.tsx","../src/util/report.ts"],"sourcesContent":["// Shared alt-screen state. Both the command entry point (which writes\n// ENTER before render() to avoid Ink's first frame leaking onto the main\n// screen) and Fullscreen.tsx (which used to write ENTER inside useEffect)\n// route through this module so enter/leave are idempotent.\n\nconst ENTER = '\\x1b[?1049h\\x1b[?25l\\x1b[2J\\x1b[H';\nconst LEAVE = '\\x1b[?25h\\x1b[?1049l';\n\nlet active = false;\nlet signalsRegistered = false;\n\nfunction ensureSignals(): void {\n if (signalsRegistered) return;\n signalsRegistered = true;\n const cleanup = () => {\n if (!active) return;\n try {\n process.stdout.write(LEAVE);\n } catch {\n // stream may be closed\n }\n active = false;\n };\n process.once('exit', cleanup);\n process.once('SIGINT', () => {\n cleanup();\n process.exit(130);\n });\n process.once('SIGTERM', () => {\n cleanup();\n process.exit(143);\n });\n}\n\nexport function enterAltScreen(): void {\n if (active) return;\n if (!process.stdout.isTTY) return;\n if (process.env.QWERTY_NO_ALTSCREEN === '1') return;\n ensureSignals();\n process.stdout.write(ENTER);\n active = true;\n}\n\nexport function leaveAltScreen(): void {\n if (!active) return;\n try {\n process.stdout.write(LEAVE);\n } catch {\n // stream may be closed\n }\n active = false;\n}\n\nexport function isAltScreenActive(): boolean {\n return active;\n}\n","import { Suspense, lazy, useRef, type ReactNode } from 'react';\nimport { Box, Text, useApp, useInput } from 'ink';\nimport { NavProvider, useNav, type ScreenFrame } from './nav.js';\nimport { Fullscreen } from './Fullscreen.js';\nimport { AudioStatusProvider } from './audio-context.js';\nimport { AppStateProvider, useAppState } from './app-state.js';\nimport { StringsProvider } from '../i18n/context.js';\nimport { RegistryProvider } from './registry-context.js';\nimport { MainMenu } from './screens/MainMenu.js';\nimport { PALETTE } from './components/BigWord.js';\nimport type { Config } from '../infra/config-store.js';\n\n// MainMenu stays eager — it's the initial screen for `qwerty` (no args) and\n// must render instantly. All other screens are lazy so their module graphs\n// (DictBrowser pulls registry helpers, StatsViewer pulls stats domain, etc.)\n// only load when the user navigates to them.\nconst PracticeScreen = lazy(() =>\n import('./screens/PracticeScreen.js').then((m) => ({ default: m.PracticeScreen })),\n);\nconst DictBrowser = lazy(() =>\n import('./screens/DictBrowser.js').then((m) => ({ default: m.DictBrowser })),\n);\nconst ConfigEditor = lazy(() =>\n import('./screens/ConfigEditor.js').then((m) => ({ default: m.ConfigEditor })),\n);\nconst StatsViewer = lazy(() =>\n import('./screens/StatsViewer.js').then((m) => ({ default: m.StatsViewer })),\n);\nconst WordLookup = lazy(() =>\n import('./screens/WordLookup.js').then((m) => ({ default: m.WordLookup })),\n);\nconst HelpScreen = lazy(() =>\n import('./screens/HelpScreen.js').then((m) => ({ default: m.HelpScreen })),\n);\n\nfunction LazyFallback() {\n return (\n <Box alignItems=\"center\" justifyContent=\"center\" width=\"100%\" height=\"100%\">\n <Text color={PALETTE.muted}>…</Text>\n </Box>\n );\n}\n\nexport function App({\n initial,\n initialCfg,\n inline = false,\n}: {\n initial: ScreenFrame;\n initialCfg: Config;\n inline?: boolean;\n}) {\n return (\n <AppStateProvider initialCfg={initialCfg}>\n <LangBridge>\n <RegistryProvider>\n <AudioStatusProvider disabled={!initialCfg.sounds.master}>\n <NavProvider initial={initial}>\n {inline ? <Router inline /> : <Fullscreen><Router /></Fullscreen>}\n </NavProvider>\n </AudioStatusProvider>\n </RegistryProvider>\n </LangBridge>\n </AppStateProvider>\n );\n}\n\nfunction LangBridge({ children }: { children: ReactNode }) {\n const { cfg } = useAppState();\n return <StringsProvider pref={cfg.language}>{children}</StringsProvider>;\n}\n\nfunction screenKey(frame: ScreenFrame): string {\n if (frame.name === 'practice') {\n const p = frame.params;\n return `practice:${p.dictId}:${p.chapterIndex}:${p.mode}:${p.stealth ? 's' : 'n'}`;\n }\n return frame.name;\n}\n\nfunction Router({ inline = false }: { inline?: boolean }) {\n const nav = useNav();\n const { cfg } = useAppState();\n const { exit } = useApp();\n const lastKeyRef = useRef<string | null>(null);\n\n useInput((input, key) => {\n if (key.ctrl && input === 'c') exit();\n });\n\n const frame = nav.current;\n const key = screenKey(frame);\n if (lastKeyRef.current !== key) {\n if (!inline && process.stdout.isTTY) process.stdout.write('\\x1b[2J\\x1b[H');\n lastKeyRef.current = key;\n }\n\n const screen = (() => {\n switch (frame.name) {\n case 'main':\n return <MainMenu cfg={cfg} />;\n case 'practice':\n return <PracticeScreen params={frame.params} />;\n case 'dict':\n return <DictBrowser params={frame.params} />;\n case 'config':\n return <ConfigEditor />;\n case 'stats':\n return <StatsViewer />;\n case 'word':\n return <WordLookup />;\n case 'help':\n return <HelpScreen />;\n }\n })();\n\n return <Suspense fallback={<LazyFallback />}>{screen}</Suspense>;\n}\n","import { useEffect, useState, type ReactNode } from 'react';\nimport { Box, useStdout } from 'ink';\nimport { enterAltScreen, leaveAltScreen } from '../util/altscreen.js';\n\nexport function Fullscreen({ children }: { children: ReactNode }) {\n const { stdout } = useStdout();\n const [size, setSize] = useState(() => ({\n rows: stdout?.rows ?? 24,\n cols: stdout?.columns ?? 80,\n }));\n\n useEffect(() => {\n // enterAltScreen is idempotent — if the command entry already wrote the\n // ENTER sequence pre-render (the normal path for menu / non-stealth\n // practice), this is a no-op. Acts as a safety net for any code path\n // that mounts the App without pre-render alt-screen setup.\n enterAltScreen();\n\n const onResize = () => {\n setSize({ rows: process.stdout.rows ?? 24, cols: process.stdout.columns ?? 80 });\n };\n process.stdout.on('resize', onResize);\n\n return () => {\n process.stdout.off('resize', onResize);\n leaveAltScreen();\n };\n }, []);\n\n return (\n <Box width={size.cols} height={size.rows} flexDirection=\"column\">\n {children}\n </Box>\n );\n}\n","import { useState } from 'react';\nimport { Box, Text, useApp, useInput } from 'ink';\nimport { useNav } from '../nav.js';\nimport { useAudioStatus } from '../audio-context.js';\nimport { useStrings } from '../../i18n/context.js';\nimport { useDictName } from '../registry-context.js';\nimport { PALETTE } from '../components/BigWord.js';\nimport { visibleWidth } from '../../util/text.js';\nimport { truncateName } from '../../util/dict-name.js';\nimport { setPostExitAction } from '../../util/post-exit-action.js';\nimport type { Config } from '../../infra/config-store.js';\n\ntype Item = { key: string; label: string; hint: string; run: () => void };\n\nexport function MainMenu({ cfg }: { cfg: Config }) {\n const [selected, setSelected] = useState(0);\n const { exit } = useApp();\n const nav = useNav();\n const audio = useAudioStatus();\n const t = useStrings();\n const defaultDictName = useDictName(cfg.defaultDict);\n\n const m = t.mainMenu.items;\n const startPractice = (stealth: boolean) => {\n // Stealth needs Ink rendered in inline mode (no alt-screen), but the menu\n // is currently running inside an alt-screen render(). Hand the dict +\n // mode off via a single-shot store and exit Ink; the CLI entry point\n // consumes the action and re-renders inline.\n if (stealth && cfg.defaultDict) {\n setPostExitAction({\n type: 'stealth',\n dictId: cfg.defaultDict,\n chapterIndex: 0,\n mode: cfg.defaultMode,\n });\n exit();\n return;\n }\n if (cfg.defaultDict) {\n nav.navigate({\n name: 'practice',\n params: {\n dictId: cfg.defaultDict,\n chapterIndex: 0,\n mode: cfg.defaultMode,\n stealth,\n },\n });\n } else {\n nav.navigate({ name: 'dict', params: { pickerMode: 'choose-then-practice' } });\n }\n };\n\n const items: Item[] = [\n {\n key: 'p',\n label: m.practiceLabel,\n hint: cfg.defaultDict\n ? m.practiceHintWith(truncateName(defaultDictName, 24))\n : m.practiceHintNone,\n run: () => startPractice(cfg.stealth === 'default'),\n },\n ];\n\n if (cfg.stealth === 'menu' || cfg.stealth === 'default') {\n items.push({\n key: 'b',\n label: m.stealthLabel,\n hint: m.stealthHint,\n run: () => startPractice(true),\n });\n }\n\n items.push(\n { key: 'd', label: m.dictLabel, hint: m.dictHint, run: () => nav.navigate({ name: 'dict' }) },\n { key: 'w', label: m.wordLabel, hint: m.wordHint, run: () => nav.navigate({ name: 'word' }) },\n { key: 's', label: m.statsLabel, hint: m.statsHint, run: () => nav.navigate({ name: 'stats' }) },\n { key: 'c', label: m.configLabel, hint: m.configHint, run: () => nav.navigate({ name: 'config' }) },\n { key: 'q', label: m.quitLabel, hint: m.quitHint, run: () => exit() },\n );\n\n const labelW = Math.max(...items.map((it) => visibleWidth(it.label))) + 4;\n\n useInput((input, key) => {\n if (key.escape) {\n exit();\n return;\n }\n if (key.upArrow) setSelected((i) => (i - 1 + items.length) % items.length);\n if (key.downArrow) setSelected((i) => (i + 1) % items.length);\n if (key.return) {\n items[selected]!.run();\n return;\n }\n if (input === '?') {\n nav.navigate({ name: 'help' });\n return;\n }\n for (const it of items) {\n if (input === it.key) {\n it.run();\n return;\n }\n }\n });\n\n return (\n <Box flexDirection=\"column\" paddingX={2} paddingY={1} width=\"100%\">\n {cfg.stealth !== 'default' && (\n <Box>\n <Text bold color={PALETTE.accent}>\n {t.app.title}\n </Text>\n <Text color={PALETTE.muted}> · {t.app.subtitle}</Text>\n </Box>\n )}\n\n <Box marginTop={cfg.stealth === 'default' ? 0 : 2} flexDirection=\"column\">\n {items.map((it, i) => {\n const active = i === selected;\n const pad = ' '.repeat(Math.max(0, labelW - visibleWidth(it.label)));\n return (\n <Box key={it.key}>\n <Text color={active ? PALETTE.accent : PALETTE.muted}>{active ? '▌ ' : ' '}</Text>\n <Text color={active ? PALETTE.accent : PALETTE.muted}>[{it.key}]</Text>\n <Text>{' '}</Text>\n <Text bold={active} color={active ? PALETTE.text : PALETTE.muted}>\n {it.label}\n {pad}\n </Text>\n <Text color={PALETTE.muted}>{it.hint}</Text>\n </Box>\n );\n })}\n </Box>\n\n <Box marginTop={2}>\n <Text color={PALETTE.muted}>\n {t.mainMenu.hint}\n {' · '}\n {t.mainMenu.helpHint}\n </Text>\n </Box>\n\n {audio.warning && (\n <Box marginTop={1}>\n <Text color={PALETTE.warning}>{t.audio.noPlayer}</Text>\n </Box>\n )}\n </Box>\n );\n}\n","import chalk from 'chalk';\nimport boxen from 'boxen';\nimport type { SessionReport } from '../infra/session-tracker.js';\nimport type { Strings } from '../i18n/strings.js';\nimport { leaveAltScreen } from './altscreen.js';\nimport { visibleWidth } from './text.js';\n\nexport function ensureMainScreen(): void {\n leaveAltScreen();\n}\n\nfunction fmtDuration(ms: number, lang: 'zh' | 'en'): string {\n const total = Math.floor(ms / 1000);\n const m = Math.floor(total / 60);\n const s = total % 60;\n if (lang === 'zh') {\n if (m === 0) return `${s} 秒`;\n return `${m} 分 ${s} 秒`;\n }\n if (m === 0) return `${s}s`;\n return `${m}m ${s}s`;\n}\n\nfunction accColor(pct: number): (s: string) => string {\n if (pct >= 90) return chalk.green;\n if (pct < 75) return chalk.dim;\n return (s) => s;\n}\n\nfunction buildLines(r: SessionReport, t: Strings, lang: 'zh' | 'en'): string[] {\n const lines: string[] = [];\n\n // Align labels in a single visual column. Use the longest label's\n // visible width as the target (CJK counted at 2 cols by visibleWidth).\n const allLabels: string[] = [t.report.duration];\n if (r.chaptersCompleted === 0) {\n allLabels.push(t.report.notPracticed);\n } else {\n allLabels.push(\n t.report.practiced,\n t.report.chapters,\n t.report.words,\n t.report.accuracy,\n t.report.wpm,\n );\n if (r.newMistakeWords > 0) allLabels.push(t.report.newMistakes);\n }\n const labelW = Math.max(...allLabels.map(visibleWidth));\n const padLabel = (s: string) =>\n s + ' '.repeat(Math.max(0, labelW - visibleWidth(s)));\n const row = (label: string, value: string) =>\n `${chalk.dim(padLabel(label))} ${value}`;\n\n lines.push(row(t.report.duration, fmtDuration(r.totalDurationMs, lang)));\n\n if (r.chaptersCompleted === 0) {\n lines.push(chalk.dim(t.report.notPracticed));\n } else {\n lines.push(row(t.report.practiced, fmtDuration(r.practiceMs, lang)));\n lines.push(row(t.report.chapters, String(r.chaptersCompleted)));\n lines.push(row(t.report.words, String(r.wordCount)));\n const accPct = Math.round(r.accuracy * 1000) / 10;\n lines.push(row(t.report.accuracy, accColor(accPct)(`${accPct}%`)));\n lines.push(row(t.report.wpm, String(r.wpm)));\n if (r.newMistakeWords > 0) {\n lines.push(row(t.report.newMistakes, chalk.red(String(r.newMistakeWords))));\n }\n }\n\n lines.push('');\n lines.push(chalk.dim.italic(t.report.farewell));\n return lines;\n}\n\nexport function printSessionReport(r: SessionReport, t: Strings, lang: 'zh' | 'en'): void {\n if (r.startedAt === null && r.chaptersCompleted === 0) return;\n const content = buildLines(r, t, lang).join('\\n');\n console.log(\n boxen(content, {\n title: chalk.bold.cyan(t.report.title),\n titleAlignment: 'left',\n borderStyle: 'round',\n borderColor: 'gray',\n padding: { top: 0, bottom: 0, left: 3, right: 3 },\n margin: { top: 1, bottom: 1, left: 2, right: 0 },\n }),\n );\n}\n"],"mappings":"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","names":["ENTER","LEAVE","active","signalsRegistered","ensureSignals","cleanup","enterAltScreen","leaveAltScreen","Suspense","lazy","useRef","Box","Text","useApp","useInput","useEffect","useState","Box","useStdout","jsx","Fullscreen","children","stdout","useStdout","size","setSize","useState","useEffect","enterAltScreen","onResize","leaveAltScreen","Box","useState","Box","Text","useApp","useInput","jsx","jsxs","MainMenu","cfg","selected","setSelected","useState","exit","useApp","nav","useNav","audio","useAudioStatus","t","useStrings","defaultDictName","useDictName","m","startPractice","stealth","setPostExitAction","items","truncateName","labelW","it","visibleWidth","useInput","input","key","i","Box","Text","PALETTE","active","pad","jsx","PracticeScreen","lazy","m","DictBrowser","ConfigEditor","StatsViewer","WordLookup","HelpScreen","LazyFallback","Box","Text","PALETTE","App","initial","initialCfg","inline","AppStateProvider","LangBridge","RegistryProvider","AudioStatusProvider","NavProvider","Router","Fullscreen","children","cfg","useAppState","StringsProvider","screenKey","frame","p","nav","useNav","exit","useApp","lastKeyRef","useRef","useInput","input","key","screen","MainMenu","Suspense","chalk","boxen","ensureMainScreen","leaveAltScreen","fmtDuration","ms","lang","total","m","s","accColor","pct","chalk","buildLines","r","lines","allLabels","labelW","visibleWidth","padLabel","row","label","value","accPct","printSessionReport","content","boxen"]}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{a as s,c,e as u}from"./chunk-
|
|
2
|
-
//# sourceMappingURL=chunk-
|
|
1
|
+
import{a as s,c,e as u}from"./chunk-E6BBQALJ.js";import{z as r}from"zod";var l=r.object({id:r.string(),name:r.string(),description:r.string().default(""),category:r.string().default(""),tags:r.array(r.string()).default([]),url:r.string(),length:r.number().int().nonnegative().default(0),language:r.string().default("en"),languageCategory:r.string().default("en"),defaultPronIndex:r.number().int().optional()}),g=r.array(l),p=r.object({name:r.string(),trans:r.array(r.string()).default([]),usphone:r.string().optional(),ukphone:r.string().optional(),notation:r.string().optional()}).passthrough(),w=r.array(p);function x(a,o,t={}){let e=o.trim().toLowerCase();return a.filter(n=>t.category&&n.category!==t.category||t.language&&n.language!==t.language?!1:e?n.id.toLowerCase().includes(e)||n.name.toLowerCase().includes(e)||n.description.toLowerCase().includes(e)||n.category.toLowerCase().includes(e)||n.tags.some(y=>y.toLowerCase().includes(e)):!0)}import{readFile as f}from"fs/promises";import{join as d}from"path";var i=null;async function m(){if(i)return i;let a=await u(s.registry);if(a){let e=g.safeParse(a);if(e.success)return i=e.data,i;console.warn(`Ignoring corrupt user registry at ${s.registry}: ${e.error.message}`)}let o=await f(d(c(),"registry.snapshot.json"),"utf8");return i=g.parse(JSON.parse(o)),i}async function b(a){return(await m()).find(t=>t.id===a)}export{w as a,x as b,m as c,b as d};
|
|
2
|
+
//# sourceMappingURL=chunk-NA5UNUVL.js.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{c as y}from"./chunk-
|
|
2
|
-
//# sourceMappingURL=chunk-
|
|
1
|
+
import{c as y}from"./chunk-NA5UNUVL.js";import{createContext as l,useContext as d,useEffect as p,useState as f}from"react";import{jsx as R}from"react/jsx-runtime";var g=l(null);function v({children:t}){let[e,r]=f(null),[o,n]=f(new Map);return p(()=>{let i=!1;return(async()=>{try{let s=await y();if(i)return;let u=new Map;for(let a of s)u.set(a.id,a);r(s),n(u)}catch{i||(r([]),n(new Map))}})(),()=>{i=!0}},[]),R(g.Provider,{value:{registry:e,byId:o},children:t})}function x(){let t=d(g);if(!t)throw new Error("useRegistry must be used inside RegistryProvider");return t}function C(t){let{byId:e}=x();return t?e.get(t)?.name??t:""}var b=/\x1b\[[0-9;]*m/g;function m(t){return t.replace(b,"")}function c(t){let e=m(t),r=0;for(let o of e){let n=o.codePointAt(0);r+=n>11904&&n<64256?2:1}return r}function M(t,e){return t+" ".repeat(Math.max(0,e-c(t)))}function I(t,e){if(c(t)<=e)return t;let r="",o=0;for(let n of t){let i=n.codePointAt(0),s=i>11904&&i<64256?2:1;if(o+s>e-1)break;r+=n,o+=s}return r+"\u2026"}export{v as a,C as b,c,M as d,I as e};
|
|
2
|
+
//# sourceMappingURL=chunk-QG7ZTS2G.js.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{a,e as g,f as m}from"./chunk-E6BBQALJ.js";import{z as n}from"zod";var d=n.object({mirror:n.enum(["jsdelivr","github"]).default("jsdelivr"),accent:n.enum(["us","uk"]).default("us"),chapterSize:n.number().int().positive().max(200).default(20),sounds:n.object({master:n.boolean().default(!0),keystroke:n.boolean().default(!0),feedback:n.boolean().default(!0),keySoundName:n.string().default("default"),pronunciationRate:n.number().refine(e=>[.5,.75,1,1.25,1.5].includes(e),{message:"pronunciationRate must be 0.5, 0.75, 1, 1.25, or 1.5"}).default(1),pronunciationSource:n.enum(["youdao","dictapi"]).default("youdao")}).default({master:!0,keystroke:!0,feedback:!0,keySoundName:"default",pronunciationRate:1,pronunciationSource:"youdao"}),autoplayPronunciation:n.boolean().default(!0),defaultMode:n.enum(["order","dictation","review","random","loop"]).default("order"),defaultDict:n.string().optional(),language:n.enum(["auto","zh","en"]).default("auto"),stealth:n.enum(["off","menu","default"]).default("off")}),i=null;function k(){return i||(i=d.parse({}),i)}async function S(){let e=await g(a.config);if(!e)return k();let o=d.safeParse(e);if(!o.success)throw new Error(`Invalid config at ${a.config}: ${o.error.message}`);return o.data}async function j(e){await m(a.config,e)}function x(e,o){let s=o.split("."),t=e;for(let r of s){if(t===null||typeof t!="object")return;t=t[r]}return t}function $(e,o,s){let t=o.split(".");if(t.length===0)throw new Error("Empty config key");let r=JSON.parse(JSON.stringify(e)),f=r;for(let u=0;u<t.length-1;u++){let w=t[u],l=f[w];if(typeof l!="object"||l===null)throw new Error(`Cannot set ${o}: ${t.slice(0,u+1).join(".")} is not an object`);f=l}let p=t[t.length-1];f[p]=y(s);let c=d.safeParse(r);if(!c.success)throw new Error(`Invalid value for ${o}: ${c.error.issues[0]?.message??"unknown"}`);return c.data}function y(e){return e==="true"?!0:e==="false"?!1:e==="null"?null:/^-?\d+$/.test(e)||/^-?\d+\.\d+$/.test(e)?Number(e):e}export{S as a,j as b,x as c,$ as d};
|
|
2
|
+
//# sourceMappingURL=chunk-QLTHO2HH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/infra/config-store.ts"],"sourcesContent":["import { z } from 'zod';\nimport { paths } from './paths.js';\nimport { readJson, writeJsonAtomic } from './fs-store.js';\n\nexport const ConfigSchema = z.object({\n mirror: z.enum(['jsdelivr', 'github']).default('jsdelivr'),\n accent: z.enum(['us', 'uk']).default('us'),\n chapterSize: z.number().int().positive().max(200).default(20),\n sounds: z\n .object({\n master: z.boolean().default(true),\n keystroke: z.boolean().default(true),\n feedback: z.boolean().default(true),\n keySoundName: z.string().default('default'),\n pronunciationRate: z\n .number()\n .refine((v) => [0.5, 0.75, 1, 1.25, 1.5].includes(v), {\n message: 'pronunciationRate must be 0.5, 0.75, 1, 1.25, or 1.5',\n })\n .default(1),\n pronunciationSource: z.enum(['youdao', 'dictapi']).default('youdao'),\n })\n .default({\n master: true,\n keystroke: true,\n feedback: true,\n keySoundName: 'default',\n pronunciationRate: 1,\n pronunciationSource: 'youdao',\n }),\n autoplayPronunciation: z.boolean().default(true),\n defaultMode: z.enum(['order', 'dictation', 'review', 'random', 'loop']).default('order'),\n defaultDict: z.string().optional(),\n language: z.enum(['auto', 'zh', 'en']).default('auto'),\n stealth: z.enum(['off', 'menu', 'default']).default('off'),\n});\n\nexport type Config = z.infer<typeof ConfigSchema>;\n\n// Defaults are computed lazily at first loadConfig() call to keep zod default\n// synthesis (~5-10ms) out of the boot module-graph evaluation.\nlet cachedDefaults: Config | null = null;\nfunction getDefaults(): Config {\n if (cachedDefaults) return cachedDefaults;\n cachedDefaults = ConfigSchema.parse({});\n return cachedDefaults;\n}\n\nexport async function loadConfig(): Promise<Config> {\n const raw = await readJson<unknown>(paths.config);\n if (!raw) return getDefaults();\n const result = ConfigSchema.safeParse(raw);\n if (!result.success) {\n throw new Error(`Invalid config at ${paths.config}: ${result.error.message}`);\n }\n return result.data;\n}\n\nexport async function saveConfig(cfg: Config): Promise<void> {\n await writeJsonAtomic(paths.config, cfg);\n}\n\nexport function getByPath(cfg: Config, path: string): unknown {\n const parts = path.split('.');\n let cur: unknown = cfg;\n for (const p of parts) {\n if (cur === null || typeof cur !== 'object') return undefined;\n cur = (cur as Record<string, unknown>)[p];\n }\n return cur;\n}\n\nexport function setByPath(cfg: Config, path: string, rawValue: string): Config {\n const parts = path.split('.');\n if (parts.length === 0) throw new Error('Empty config key');\n const clone: Record<string, unknown> = JSON.parse(JSON.stringify(cfg));\n let cur: Record<string, unknown> = clone;\n for (let i = 0; i < parts.length - 1; i++) {\n const k = parts[i]!;\n const next = cur[k];\n if (typeof next !== 'object' || next === null) {\n throw new Error(`Cannot set ${path}: ${parts.slice(0, i + 1).join('.')} is not an object`);\n }\n cur = next as Record<string, unknown>;\n }\n const leaf = parts[parts.length - 1]!;\n cur[leaf] = coerce(rawValue);\n const validated = ConfigSchema.safeParse(clone);\n if (!validated.success) {\n throw new Error(`Invalid value for ${path}: ${validated.error.issues[0]?.message ?? 'unknown'}`);\n }\n return validated.data;\n}\n\nfunction coerce(v: string): unknown {\n if (v === 'true') return true;\n if (v === 'false') return false;\n if (v === 'null') return null;\n if (/^-?\\d+$/.test(v)) return Number(v);\n if (/^-?\\d+\\.\\d+$/.test(v)) return Number(v);\n return v;\n}\n"],"mappings":"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","names":["z","ConfigSchema","z","v","cachedDefaults","getDefaults","loadConfig","raw","readJson","paths","result","saveConfig","cfg","writeJsonAtomic","getByPath","path","parts","cur","p","setByPath","rawValue","clone","i","k","next","leaf","coerce","validated"]}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{a as y}from"./chunk-
|
|
2
|
-
//# sourceMappingURL=chunk-
|
|
1
|
+
import{a as y}from"./chunk-QLTHO2HH.js";import{a as w,d as p}from"./chunk-NA5UNUVL.js";import{a as s,b as m,d as f,e as h,f as u}from"./chunk-E6BBQALJ.js";import{createHash as g}from"crypto";import{stat as P,copyFile as S}from"fs/promises";var d=null;async function D(){return d||(d=(await import("undici")).request,d)}var F="https://cdn.jsdelivr.net/gh/RealKai42/qwerty-learner@master",x="https://raw.githubusercontent.com/RealKai42/qwerty-learner/master";async function q(t,r=2){let n=await D(),o;for(let e=0;e<=r;e++)try{let a=await n(t,{headersTimeout:1e4,bodyTimeout:3e4});if(a.statusCode>=400)throw new Error(`HTTP ${a.statusCode}`);return await a.body.text()}catch(a){o=a,e<r&&await new Promise(i=>setTimeout(i,500*(e+1)))}throw o instanceof Error?o:new Error(String(o))}function R(t,r){let o=`/public${r.startsWith("/")?r:`/${r}`}`,e=`${F}${o}`,a=`${x}${o}`;return t==="jsdelivr"?[e,a]:[a,e]}async function v(t){let r=await p(t);if(!r)throw new Error(`Unknown dictionary id: ${t}`);let n=await y();await m();let o=R(n.mirror,r.url),e=null,a;for(let l of o)try{e=await q(l);break}catch(b){a=b}if(!e)throw new Error(`Failed to download dictionary ${t} from any mirror: ${a instanceof Error?a.message:a}`);let i;try{i=JSON.parse(e)}catch(l){throw new Error(`Dictionary ${t} is not valid JSON: ${l.message}`)}let c=w.safeParse(i);if(!c.success)throw new Error(`Dictionary ${t} failed schema validation: ${c.error.issues[0]?.message}`);let E=g("sha256").update(e).digest("hex");await u(s.dictFile(t),c.data);let $={sha256:E,size:e.length,fetchedAt:new Date().toISOString(),id:t};return await u(s.dictMeta(t),$),{words:c.data,size:e.length}}async function I(t,r){if(!/^[a-z0-9-]+$/.test(r))throw new Error(`Invalid id "${r}". Must match /^[a-z0-9-]+$/`);if(await m(),!await f(t))throw new Error(`File not found: ${t}`);let n=await h(t),o=w.safeParse(n);if(!o.success)throw new Error(`Import rejected: ${o.error.issues[0]?.message}`);await S(t,s.dictFile(r));let e=(await P(s.dictFile(r))).size,i={sha256:g("sha256").update(JSON.stringify(o.data)).digest("hex"),size:e,fetchedAt:new Date().toISOString(),id:r};return await u(s.dictMeta(r),i),{words:o.data,size:e}}async function z(t){let r=await h(s.dictFile(t));if(!r)return null;let n=w.safeParse(r);if(!n.success)throw new Error(`Local dictionary ${t} corrupt: ${n.error.message}`);return n.data}async function N(t){return f(s.dictFile(t))}async function O(t){let r=await z(t);if(r)return r;let{words:n}=await v(t);return n}async function L(t){let{unlink:r}=await import("fs/promises"),n=!1;for(let o of[s.dictFile(t),s.dictMeta(t)])try{await r(o),n=!0}catch(e){if(e.code!=="ENOENT")throw e}return n}export{v as a,I as b,z as c,N as d,O as e,L as f};
|
|
2
|
+
//# sourceMappingURL=chunk-V3TAMY5K.js.map
|
package/dist/cli.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{Command as b}from"commander";import{Command as u}from"commander";function r(){let o=new u("config").description("Manage CLI configuration");return o.command("list").description("Show the effective merged config").action(async()=>{let{configList:t}=await import("./config.impl-
|
|
1
|
+
import{Command as b}from"commander";import{Command as u}from"commander";function r(){let o=new u("config").description("Manage CLI configuration");return o.command("list").description("Show the effective merged config").action(async()=>{let{configList:t}=await import("./config.impl-G25YTIQY.js");await t()}),o.command("get <key>").description("Get a config value by dotted path (e.g. sounds.keystroke)").action(async t=>{let{configGet:a}=await import("./config.impl-G25YTIQY.js");await a(t)}),o.command("set <key> <value>").description("Set a config value by dotted path").action(async(t,a)=>{let{configSet:n}=await import("./config.impl-G25YTIQY.js");await n(t,a)}),o}import{Command as g}from"commander";function e(){let o=new g("dict").description("Manage dictionaries");return o.command("list").description("List dictionaries (\u2713 = locally available)").option("-c, --category <category>","filter by category").option("--local-only","show only locally downloaded dictionaries").action(async t=>{let{dictList:a}=await import("./dict.impl-7AE6RFW2.js");await a(t)}),o.command("search <keyword>").description("Search the upstream registry by name/description/category/tags").option("-c, --category <category>","restrict to category").option("-l, --language <language>","restrict to language").action(async(t,a)=>{let{dictSearch:n}=await import("./dict.impl-7AE6RFW2.js");await n(t,a)}),o.command("pull <id>").description("Download an upstream dictionary into the local cache").action(async t=>{let{dictPull:a}=await import("./dict.impl-7AE6RFW2.js");await a(t)}),o.command("import <file>").description("Import a local qwerty-native JSON dictionary").requiredOption("--id <id>","dictionary id (lowercase, digits, dashes)").action(async(t,a)=>{let{dictImport:n}=await import("./dict.impl-7AE6RFW2.js");await n(t,a)}),o.command("rm <id>").description("Remove a local dictionary").action(async t=>{let{dictRemove:a}=await import("./dict.impl-7AE6RFW2.js");await a(t)}),o}import{Command as y}from"commander";function c(){return new y("doctor").description("Diagnose audio playback and runtime environment").action(async()=>{let{runDoctor:o}=await import("./doctor.impl-KTNLVHGF.js");await o()})}import{Command as w}from"commander";function m(){return new w("practice").argument("[dictId]","dictionary id; falls back to config.defaultDict").description("Start a typing practice session").option("-c, --chapter <n>","chapter number (1-based)","1").option("-m, --mode <mode>","order | dictation | review | random | loop").option("--stealth","enter stealth mode (minimal UI, no sound)").action(async(o,t)=>{let{runPractice:a}=await import("./practice.impl-KRFKX4AS.js");await a(o,t)})}import{Command as f}from"commander";function d(){return new f("stats").description("Show practice history and trends").option("-d, --days <n>","window size for trend (default 14)","14").option("--top <n>","how many top mistakes to show (default 10)","10").action(async o=>{let{runStats:t}=await import("./stats.impl-3I7BB7X3.js");await t(o)})}import{Command as C}from"commander";function s(){return new C("word").argument("<keyword>").description("Look up a word across local dictionaries").option("--exact","require exact (case-insensitive) match").action(async(o,t)=>{let{runWordLookup:a}=await import("./word.impl-NOC72Y32.js");await a(o,t)})}import{Command as h}from"commander";function p(){return new h("boss").alias("stealth").description("Start practice in stealth mode (minimal UI, looks like plain terminal output)").argument("[dictId]","dictionary id; falls back to config.defaultDict").option("-c, --chapter <n>","chapter number (1-based)","1").option("-m, --mode <mode>","order | dictation | review | random | loop").action(async(o,t)=>{let{runPractice:a}=await import("./practice.impl-KRFKX4AS.js");await a(o,{...t,stealth:!0})})}async function l(){if(!process.stdout.isTTY){console.log("qwerty-cli \u2014 run `qwerty --help` for available commands.");return}let{runMainMenuImpl:o}=await import("./menu.impl-SRQ6MJY3.js");await o()}var i=new b;i.name("qwerty").description("Terminal clone of qwerty-learner \u2014 typing practice for English vocabulary").version("0.0.1-alpha.15");i.addCommand(m());i.addCommand(p());i.addCommand(e());i.addCommand(s());i.addCommand(d());i.addCommand(r());i.addCommand(c());i.action(async()=>{await l()});i.parseAsync(process.argv).catch(o=>{console.error(o instanceof Error?o.message:o),process.exit(1)});
|
|
2
2
|
//# sourceMappingURL=cli.js.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{a as i,b as e,c as s,d as r}from"./chunk-
|
|
2
|
-
//# sourceMappingURL=config.impl-
|
|
1
|
+
import{a as i,b as e,c as s,d as r}from"./chunk-QLTHO2HH.js";import"./chunk-E6BBQALJ.js";import c from"chalk";async function l(){let o=await i();console.log(JSON.stringify(o,null,2))}async function d(o){let t=await i(),n=s(t,o);if(n===void 0){console.error(c.red(`Key not found: ${o}`)),process.exitCode=1;return}console.log(typeof n=="string"?n:JSON.stringify(n))}async function m(o,t){let n=await i(),f=r(n,o,t);await e(f),console.log(c.green(`Set ${o} = ${t}`))}export{d as configGet,l as configList,m as configSet};
|
|
2
|
+
//# sourceMappingURL=config.impl-G25YTIQY.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{a as d,b as m,d as s,f as y}from"./chunk-
|
|
1
|
+
import{a as d,b as m,d as s,f as y}from"./chunk-V3TAMY5K.js";import"./chunk-QLTHO2HH.js";import{b as g,c as l}from"./chunk-NA5UNUVL.js";import"./chunk-E6BBQALJ.js";import i from"chalk";function h(e){return e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:`${(e/1024/1024).toFixed(2)} MB`}function u(e){let t=0,o=e.replace(/\x1b\[[0-9;]*m/g,"");for(let r of o){let a=r.codePointAt(0);t+=a>11904&&a<64256?2:1}return t}async function f(e){let t=await Promise.all(e.map(async n=>({entry:n,local:await s(n.id)}))),o={local:3,id:Math.max(2,...t.map(n=>n.entry.id.length)),name:Math.max(4,...t.map(n=>n.entry.name.length)),category:Math.max(8,...t.map(n=>n.entry.category.length)),length:Math.max(5,...t.map(n=>String(n.entry.length).length))},r=(n,c)=>n+" ".repeat(Math.max(0,c-u(n))),a=[r(" ",o.local),i.bold(r("ID",o.id)),i.bold(r("Name",o.name)),i.bold(r("Category",o.category)),i.bold(r("Words",o.length)),i.bold("Description")].join(" ");console.log(a);for(let{entry:n,local:c}of t){let p=c?i.green(" \u2713"):" ";console.log([r(p,o.local),r(n.id,o.id),r(n.name,o.name),i.dim(r(n.category,o.category)),r(String(n.length),o.length),i.dim(n.description)].join(" "))}}async function $(e){let t=await l(),o=e.category?t.filter(r=>r.category===e.category):t;if(e.localOnly){let r=await Promise.all(o.map(a=>s(a.id)));o=o.filter((a,n)=>r[n])}await f(o),console.log(i.dim(`
|
|
2
2
|
${o.length} dictionaries`))}async function P(e,t){let o=await l(),r=g(o,e,t);if(r.length===0){console.log(i.yellow(`No dictionaries matched "${e}"`));return}await f(r),console.log(i.dim(`
|
|
3
3
|
${r.length} matches`))}async function D(e){try{let{words:t,size:o}=await d(e);console.log(i.green(`Saved ${t.length} words (${h(o)}) \u2192 dict ${i.bold(e)}`))}catch(t){console.error(i.red(t.message)),process.exitCode=1}}async function M(e,t){try{let{words:o,size:r}=await m(e,t.id);console.log(i.green(`Imported ${o.length} words (${h(r)}) as ${i.bold(t.id)}`))}catch(o){console.error(i.red(o.message)),process.exitCode=1}}async function B(e){let t=await y(e);console.log(t?i.green(`Removed ${e}`):i.yellow(`Nothing to remove for ${e}`))}export{M as dictImport,$ as dictList,D as dictPull,B as dictRemove,P as dictSearch};
|
|
4
|
-
//# sourceMappingURL=dict.impl-
|
|
4
|
+
//# sourceMappingURL=dict.impl-7AE6RFW2.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{a as f,g as u,h as d,i as y}from"./chunk-BIBS2Q3E.js";import{a as w}from"./chunk-QLTHO2HH.js";import{c as m}from"./chunk-E6BBQALJ.js";import{spawn as $}from"child_process";import{join as h}from"path";function p(e,o,s=5e3){return new Promise(n=>{let i=Date.now(),l=[],c=!1,a=(r,t)=>{c||(c=!0,n({cmd:e,args:o,exitCode:r,durationMs:Date.now()-i,stderr:Buffer.concat(l).toString("utf8").trim(),spawnError:t}))};try{let r=$(e,o,{stdio:["ignore","pipe","pipe"],windowsHide:!0});r.on("error",t=>a(null,t.message)),r.stdout?.resume(),r.stderr?.on("data",t=>l.push(t)),r.on("exit",t=>a(t)),setTimeout(()=>{try{r.kill()}catch{}a(null,`timeout after ${s}ms`)},s)}catch(r){a(null,r.message)}})}function g(e,o=""){let s=[];if(s.push(`${o}cmd: ${e.cmd} ${e.args.join(" ")}`),s.push(`${o}exit: ${e.exitCode??"null"} duration: ${e.durationMs}ms`),e.spawnError&&s.push(`${o}spawnError: ${e.spawnError}`),e.stderr){let n=e.stderr.length>500?e.stderr.slice(0,500)+"\u2026":e.stderr;s.push(`${o}stderr: ${n.replace(/\n/g,`
|
|
2
|
+
`+o+" ")}`)}return s.join(`
|
|
3
|
+
`)}async function C(){console.log("qwerty doctor \u2014 environment + audio diagnostics"),console.log(""),console.log("Runtime"),console.log(` platform : ${process.platform}`),console.log(` arch : ${process.arch}`),console.log(` node : ${process.version}`);let e=(process.env.PATH??"").slice(0,240);console.log(` PATH(240): ${e}${(process.env.PATH??"").length>240?"\u2026":""}`),console.log(""),console.log("Audio detection"),await f(!1);let o=y();console.log(` wav player : ${o.wav?`${o.wav.kind} (${o.wav.cmd})`:"none"}`),console.log(` mp3 player : ${o.mp3?`${o.mp3.kind} (${o.mp3.cmd})`:"none"}`),console.log(` disabled : ${d()}`),console.log(` warning : ${u()??"(none)"}`);let s=await w();if(console.log(` pron source: ${s.sounds.pronunciationSource}`),console.log(""),process.platform==="win32"){console.log("Windows PowerShell type resolution");let n="Try { [System.Media.SoundPlayer] | Out-Null; 'ok' } Catch { Write-Error $_.Exception.Message; 'fail' }",i=await p("powershell",["-NoProfile","-Command",n]),l=await p("pwsh",["-NoProfile","-Command",n]);console.log(" powershell.exe (5.1):"),console.log(g(i," ")),console.log(" pwsh (7+):"),console.log(g(l," ")),console.log("")}if(console.log("Built-in WAV playback test"),!o.wav)console.log(" no wav player available \u2014 skipping");else{let n=m(),i=["correct.wav","wrong.wav","key-default.wav"];for(let l of i){let c=h(n,"sounds",l),a=o.wav.cmd,r=v(o.wav.kind,c,"wav"),t=await p(a,r,4e3);console.log(` ${l}:`),console.log(g(t," "))}}console.log(""),u()||d()?console.log("Hint: sounds are disabled or degraded. See diagnosis above."):console.log("Audio appears healthy. If you still hear nothing, check OS volume / mute.")}function v(e,o,s){switch(e){case"afplay":return[o];case"ffplay":return["-nodisp","-autoexit","-loglevel","quiet",o];case"mpg123":return["-q",o];case"paplay":return[o];case"aplay":return["-q",o];case"powershell":case"pwsh":{let n=o.replace(/'/g,"''");return s==="wav"?["-NoProfile","-Command",`(New-Object System.Media.SoundPlayer '${n}').PlaySync();`]:["-NoProfile","-ExecutionPolicy","Bypass","-Command",`Add-Type -AssemblyName presentationCore; $p = New-Object System.Windows.Media.MediaPlayer; $p.Open([Uri]::new('${n}')); $p.Play(); Start-Sleep -Milliseconds 3000`]}default:return[o]}}export{C as runDoctor};
|
|
4
|
+
//# sourceMappingURL=doctor.impl-KTNLVHGF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/doctor.impl.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\nimport { join } from 'node:path';\nimport { initAudio, audioWarning, audioDisabled, audioPlayers } from '../infra/audio.js';\nimport { packageAssetsDir } from '../infra/paths.js';\nimport { loadConfig } from '../infra/config-store.js';\n\ntype ProbeResult = {\n cmd: string;\n args: string[];\n exitCode: number | null;\n durationMs: number;\n stderr: string;\n spawnError?: string;\n};\n\nfunction probe(cmd: string, args: string[], timeoutMs = 5000): Promise<ProbeResult> {\n return new Promise((resolve) => {\n const t0 = Date.now();\n const stderrChunks: Buffer[] = [];\n let done = false;\n const finish = (exitCode: number | null, spawnError?: string) => {\n if (done) return;\n done = true;\n resolve({\n cmd,\n args,\n exitCode,\n durationMs: Date.now() - t0,\n stderr: Buffer.concat(stderrChunks).toString('utf8').trim(),\n spawnError,\n });\n };\n try {\n const child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });\n child.on('error', (err) => finish(null, err.message));\n child.stdout?.resume();\n child.stderr?.on('data', (b: Buffer) => stderrChunks.push(b));\n child.on('exit', (code) => finish(code));\n setTimeout(() => {\n try {\n child.kill();\n } catch {\n /* ignore */\n }\n finish(null, `timeout after ${timeoutMs}ms`);\n }, timeoutMs);\n } catch (err) {\n finish(null, (err as Error).message);\n }\n });\n}\n\nfunction fmtResult(r: ProbeResult, prefix = ''): string {\n const lines: string[] = [];\n lines.push(`${prefix}cmd: ${r.cmd} ${r.args.join(' ')}`);\n lines.push(`${prefix}exit: ${r.exitCode ?? 'null'} duration: ${r.durationMs}ms`);\n if (r.spawnError) lines.push(`${prefix}spawnError: ${r.spawnError}`);\n if (r.stderr) {\n const truncated = r.stderr.length > 500 ? r.stderr.slice(0, 500) + '…' : r.stderr;\n lines.push(`${prefix}stderr: ${truncated.replace(/\\n/g, '\\n' + prefix + ' ')}`);\n }\n return lines.join('\\n');\n}\n\nexport async function runDoctor(): Promise<void> {\n console.log('qwerty doctor — environment + audio diagnostics');\n console.log('');\n console.log('Runtime');\n console.log(` platform : ${process.platform}`);\n console.log(` arch : ${process.arch}`);\n console.log(` node : ${process.version}`);\n const PATH = (process.env.PATH ?? '').slice(0, 240);\n console.log(` PATH(240): ${PATH}${(process.env.PATH ?? '').length > 240 ? '…' : ''}`);\n console.log('');\n\n console.log('Audio detection');\n await initAudio(false);\n const players = audioPlayers();\n console.log(` wav player : ${players.wav ? `${players.wav.kind} (${players.wav.cmd})` : 'none'}`);\n console.log(` mp3 player : ${players.mp3 ? `${players.mp3.kind} (${players.mp3.cmd})` : 'none'}`);\n console.log(` disabled : ${audioDisabled()}`);\n console.log(` warning : ${audioWarning() ?? '(none)'}`);\n const cfg = await loadConfig();\n console.log(` pron source: ${cfg.sounds.pronunciationSource}`);\n console.log('');\n\n if (process.platform === 'win32') {\n console.log('Windows PowerShell type resolution');\n const typeProbe = `Try { [System.Media.SoundPlayer] | Out-Null; 'ok' } Catch { Write-Error $_.Exception.Message; 'fail' }`;\n const psResult = await probe('powershell', ['-NoProfile', '-Command', typeProbe]);\n const pwshResult = await probe('pwsh', ['-NoProfile', '-Command', typeProbe]);\n console.log(' powershell.exe (5.1):');\n console.log(fmtResult(psResult, ' '));\n console.log(' pwsh (7+):');\n console.log(fmtResult(pwshResult, ' '));\n console.log('');\n }\n\n console.log('Built-in WAV playback test');\n if (!players.wav) {\n console.log(' no wav player available — skipping');\n } else {\n const assetsDir = packageAssetsDir();\n const samples = ['correct.wav', 'wrong.wav', 'key-default.wav'];\n for (const name of samples) {\n const file = join(assetsDir, 'sounds', name);\n const wavCmd = players.wav.cmd;\n const wavArgs = buildArgsFor(players.wav.kind, file, 'wav');\n const r = await probe(wavCmd, wavArgs, 4000);\n console.log(` ${name}:`);\n console.log(fmtResult(r, ' '));\n }\n }\n console.log('');\n\n if (audioWarning() || audioDisabled()) {\n console.log('Hint: sounds are disabled or degraded. See diagnosis above.');\n } else {\n console.log('Audio appears healthy. If you still hear nothing, check OS volume / mute.');\n }\n}\n\n// Replicates the args builders from audio.ts at the doctor layer so we can\n// invoke a player without going through the runtime. Mirrors CANDIDATES.\nfunction buildArgsFor(kind: string, file: string, fileKind: 'wav' | 'mp3'): string[] {\n switch (kind) {\n case 'afplay':\n return [file];\n case 'ffplay':\n return ['-nodisp', '-autoexit', '-loglevel', 'quiet', file];\n case 'mpg123':\n return ['-q', file];\n case 'paplay':\n return [file];\n case 'aplay':\n return ['-q', file];\n case 'powershell':\n case 'pwsh': {\n const escaped = file.replace(/'/g, \"''\");\n if (fileKind === 'wav') {\n return [\n '-NoProfile',\n '-Command',\n `(New-Object System.Media.SoundPlayer '${escaped}').PlaySync();`,\n ];\n }\n return [\n '-NoProfile',\n '-ExecutionPolicy',\n 'Bypass',\n '-Command',\n `Add-Type -AssemblyName presentationCore; $p = New-Object System.Windows.Media.MediaPlayer; $p.Open([Uri]::new('${escaped}')); $p.Play(); Start-Sleep -Milliseconds 3000`,\n ];\n }\n default:\n return [file];\n }\n}\n"],"mappings":"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","names":["spawn","join","probe","cmd","args","timeoutMs","resolve","t0","stderrChunks","done","finish","exitCode","spawnError","child","spawn","err","b","code","fmtResult","r","prefix","lines","truncated","runDoctor","PATH","initAudio","players","audioPlayers","audioDisabled","audioWarning","cfg","loadConfig","typeProbe","psResult","pwshResult","assetsDir","packageAssetsDir","samples","name","file","join","wavCmd","wavArgs","buildArgsFor","kind","fileKind","escaped"]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{a as o,b as s,c as p,d as c}from"./chunk-K6THKMFC.js";import{d as n,g as a,i as m}from"./chunk-6ROGUGNX.js";import"./chunk-BIBS2Q3E.js";import"./chunk-4GNY4LY7.js";import{a as i}from"./chunk-QLTHO2HH.js";import"./chunk-QG7ZTS2G.js";import{e}from"./chunk-3TQFRACL.js";import"./chunk-NA5UNUVL.js";import"./chunk-E6BBQALJ.js";import{render as g}from"ink";import{createElement as h}from"react";async function y(){o();let r=await i();a();let{waitUntilExit:f}=g(h(s,{initial:{name:"main"},initialCfg:r}),{patchConsole:!1,exitOnCtrlC:!1});await f(),p();let t=n();if(t?.type==="stealth"){let{runPractice:u}=await import("./practice.impl-KRFKX4AS.js");await u(t.dictId,{chapter:t.chapterIndex+1,mode:t.mode,stealth:!0});return}let{lang:l,t:d}=e(r.language);c(m(),d,l)}export{y as runMainMenuImpl};
|
|
2
|
+
//# sourceMappingURL=menu.impl-SRQ6MJY3.js.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{a as c,b as l,c as u,d as g}from"./chunk-K6THKMFC.js";import{f as m,g as f,i as p}from"./chunk-6ROGUGNX.js";import"./chunk-BIBS2Q3E.js";import"./chunk-4GNY4LY7.js";import{a}from"./chunk-QLTHO2HH.js";import"./chunk-QG7ZTS2G.js";import{e as d}from"./chunk-3TQFRACL.js";import"./chunk-NA5UNUVL.js";import"./chunk-E6BBQALJ.js";import n from"chalk";import{render as b}from"ink";import{createElement as v}from"react";var h=["order","dictation","review","random","loop"];function w(t){return h.includes(t)}async function O(t,o){if(!process.stdout.isTTY){console.error(n.red("Practice requires an interactive TTY.")),process.exitCode=1;return}let e=await a(),s=t??e.defaultDict;if(!s){console.error(n.red("No dictionary specified. Pass an id or set config.defaultDict.")),process.exitCode=1;return}let i=o.mode??e.defaultMode;if(!w(i)){console.error(n.red(`Invalid mode "${i}". Valid: ${h.join(", ")}`)),process.exitCode=1;return}let x=Math.max(0,Number(o.chapter??1)-1),r=o.stealth===!0||e.stealth==="default";r||c(),f();let{waitUntilExit:C}=b(v(l,{initial:{name:"practice",params:{dictId:s,chapterIndex:x,mode:i,stealth:r}},initialCfg:e,inline:r}),{patchConsole:!1,exitOnCtrlC:!1});if(await C(),r||u(),m()){process.stdout.write("\x1B[3F\x1B[0J");return}if(r)return;let{lang:M,t:S}=d(e.language);g(p(),S,M)}export{O as runPractice};
|
|
2
|
+
//# sourceMappingURL=practice.impl-KRFKX4AS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/practice.impl.ts"],"sourcesContent":["import chalk from 'chalk';\nimport { render } from 'ink';\nimport { createElement } from 'react';\nimport { loadConfig } from '../infra/config-store.js';\nimport { App } from '../ui/App.js';\nimport { start as startSession, report as sessionReport } from '../infra/session-tracker.js';\nimport { printSessionReport, ensureMainScreen } from '../util/report.js';\nimport { pickStrings } from '../i18n/context.js';\nimport { enterAltScreen } from '../util/altscreen.js';\nimport { consumeSilentExit } from '../util/post-exit-action.js';\nimport type { Mode } from '../domain/chapters.js';\n\nconst MODES: Mode[] = ['order', 'dictation', 'review', 'random', 'loop'];\n\nfunction isMode(v: string): v is Mode {\n return (MODES as string[]).includes(v);\n}\n\nexport async function runPractice(\n dictIdArg: string | undefined,\n options: { chapter?: string | number; mode?: string; stealth?: boolean },\n): Promise<void> {\n if (!process.stdout.isTTY) {\n console.error(chalk.red('Practice requires an interactive TTY.'));\n process.exitCode = 1;\n return;\n }\n const cfg = await loadConfig();\n const dictId = dictIdArg ?? cfg.defaultDict;\n if (!dictId) {\n console.error(chalk.red('No dictionary specified. Pass an id or set config.defaultDict.'));\n process.exitCode = 1;\n return;\n }\n const mode = options.mode ?? cfg.defaultMode;\n if (!isMode(mode)) {\n console.error(chalk.red(`Invalid mode \"${mode}\". Valid: ${MODES.join(', ')}`));\n process.exitCode = 1;\n return;\n }\n const chapterIndex = Math.max(0, Number(options.chapter ?? 1) - 1);\n const stealth = options.stealth === true || cfg.stealth === 'default';\n\n // Enter alt-screen synchronously BEFORE render() so Ink's first frame\n // never lands on the main screen. Without this, when the user exits,\n // the main screen still has the menu/practice content from frame 0\n // showing under the exit report. Stealth uses inline mode, skip.\n if (!stealth) enterAltScreen();\n\n startSession();\n const { waitUntilExit } = render(\n createElement(App, {\n initial: { name: 'practice', params: { dictId, chapterIndex, mode, stealth } },\n initialCfg: cfg,\n inline: stealth,\n }),\n { patchConsole: false, exitOnCtrlC: false },\n );\n await waitUntilExit();\n if (!stealth) {\n ensureMainScreen();\n }\n\n if (consumeSilentExit()) {\n // User hit Ctrl+C on the stealth paused screen — erase the 3 inline rows\n // (paused / Esc back / blank) and skip the session report for a fully\n // clean exit. Cursor goes back to where the paused UI started; the shell\n // prompt then renders there, leaving no trace.\n process.stdout.write('\\x1b[3F\\x1b[0J');\n return;\n }\n if (stealth) {\n // Stealth: StealthSummary already rendered an inline summary row that\n // stays in scrollback. A second ~10-row boxen report would defeat the\n // point of the mode, so skip it.\n return;\n }\n\n const { lang, t } = pickStrings(cfg.language);\n printSessionReport(sessionReport(), t, lang);\n}\n"],"mappings":"0UAAA,OAAOA,MAAW,QAClB,OAAS,UAAAC,MAAc,MACvB,OAAS,iBAAAC,MAAqB,QAU9B,IAAMC,EAAgB,CAAC,QAAS,YAAa,SAAU,SAAU,MAAM,EAEvE,SAASC,EAAOC,EAAsB,CACpC,OAAQF,EAAmB,SAASE,CAAC,CACvC,CAEA,eAAsBC,EACpBC,EACAC,EACe,CACf,GAAI,CAAC,QAAQ,OAAO,MAAO,CACzB,QAAQ,MAAMC,EAAM,IAAI,uCAAuC,CAAC,EAChE,QAAQ,SAAW,EACnB,MACF,CACA,IAAMC,EAAM,MAAMC,EAAW,EACvBC,EAASL,GAAaG,EAAI,YAChC,GAAI,CAACE,EAAQ,CACX,QAAQ,MAAMH,EAAM,IAAI,gEAAgE,CAAC,EACzF,QAAQ,SAAW,EACnB,MACF,CACA,IAAMI,EAAOL,EAAQ,MAAQE,EAAI,YACjC,GAAI,CAACN,EAAOS,CAAI,EAAG,CACjB,QAAQ,MAAMJ,EAAM,IAAI,iBAAiBI,CAAI,aAAaV,EAAM,KAAK,IAAI,CAAC,EAAE,CAAC,EAC7E,QAAQ,SAAW,EACnB,MACF,CACA,IAAMW,EAAe,KAAK,IAAI,EAAG,OAAON,EAAQ,SAAW,CAAC,EAAI,CAAC,EAC3DO,EAAUP,EAAQ,UAAY,IAAQE,EAAI,UAAY,UAMvDK,GAASC,EAAe,EAE7BC,EAAa,EACb,GAAM,CAAE,cAAAC,CAAc,EAAIC,EACxBC,EAAcC,EAAK,CACjB,QAAS,CAAE,KAAM,WAAY,OAAQ,CAAE,OAAAT,EAAQ,aAAAE,EAAc,KAAAD,EAAM,QAAAE,CAAQ,CAAE,EAC7E,WAAYL,EACZ,OAAQK,CACV,CAAC,EACD,CAAE,aAAc,GAAO,YAAa,EAAM,CAC5C,EAMA,GALA,MAAMG,EAAc,EACfH,GACHO,EAAiB,EAGfC,EAAkB,EAAG,CAKvB,QAAQ,OAAO,MAAM,gBAAgB,EACrC,MACF,CACA,GAAIR,EAIF,OAGF,GAAM,CAAE,KAAAS,EAAM,EAAAC,CAAE,EAAIC,EAAYhB,EAAI,QAAQ,EAC5CiB,EAAmBC,EAAc,EAAGH,EAAGD,CAAI,CAC7C","names":["chalk","render","createElement","MODES","isMode","v","runPractice","dictIdArg","options","chalk","cfg","loadConfig","dictId","mode","chapterIndex","stealth","enterAltScreen","start","waitUntilExit","render","createElement","App","ensureMainScreen","consumeSilentExit","lang","t","pickStrings","printSessionReport","report"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import{b as $,c as g,d as u,e as y,f as a,h}from"./chunk-
|
|
1
|
+
import{b as $,c as g,d as u,e as y,f as a,h}from"./chunk-HJIGZU3E.js";import{a as m,d as p}from"./chunk-G3DQB7FI.js";import"./chunk-E6BBQALJ.js";import t from"chalk";async function T(d){let l=Math.max(1,Number(d.days)||14),w=Math.max(1,Number(d.top)||10),r=await $(),M=await m();if(r.length===0){console.log(t.yellow("No practice history yet. Run `qwerty practice <dict>` to get started."));return}let e=h(r,l),k=y(r),n=r.reduce((o,s)=>o+s.wordCount,0),b=r.reduce((o,s)=>o+s.errors,0),i=r.reduce((o,s)=>o+s.durationMs,0),f=r.reduce((o,s)=>o+(s.wordCount-Object.keys(s.perWordErrors).length),0),S=i>0?Math.round(n/(i/6e4)*10)/10:0,v=n===0?1:f/n;console.log(t.bold(`
|
|
2
2
|
Lifetime`)),console.log(` ${t.dim("sessions")} ${r.length} ${t.dim("words")} ${n} ${t.dim("errors")} ${b}`),console.log(` ${t.dim("avg wpm")} ${S} ${t.dim("avg accuracy")} ${Math.round(v*1e3)/10}% ${t.dim("streak")} ${t.bold(k)}d`),console.log(t.bold(`
|
|
3
3
|
Last ${l} days`)),console.log(` ${t.dim("wpm ")} ${a(e.map(o=>o.wpm))} ${t.dim("max")} ${Math.round(Math.max(...e.map(o=>o.wpm)))}`),console.log(` ${t.dim("accuracy")} ${a(e.map(o=>o.accuracy*100))} ${t.dim("range")} ${Math.round(Math.min(...e.map(o=>o.accuracy*100)))}-${Math.round(Math.max(...e.map(o=>o.accuracy*100)))}%`),console.log(` ${t.dim("sessions")} ${a(e.map(o=>o.sessions))}`);let x=r.slice(-5).reverse();console.log(t.bold(`
|
|
4
4
|
Last 5 sessions`));for(let o of x){let s=g(o),E=Math.round(u(o)*1e3)/10;console.log(` ${t.dim(o.ts.replace("T"," ").slice(0,16))} ${t.cyan(o.dictId.padEnd(14))} ch${String(o.chapter+1).padStart(3)} ${o.mode.padEnd(9)} ${String(o.wordCount).padStart(3)}w ${o.errors}err ${s}wpm ${E}%`)}let c=p(M,w);if(c.length>0){console.log(t.bold(`
|
|
5
5
|
Top ${c.length} mistakes`));for(let[o,s]of c)console.log(` ${t.red(String(s.count).padStart(3))} ${t.bold(o.padEnd(20))} ${t.dim(s.dictIds.join(", "))}`)}else console.log(t.bold(`
|
|
6
6
|
Top mistakes`)),console.log(t.dim(" none \u2014 keep going"));console.log()}export{T as runStats};
|
|
7
|
-
//# sourceMappingURL=stats.impl-
|
|
7
|
+
//# sourceMappingURL=stats.impl-3I7BB7X3.js.map
|