qwerty-cli 0.0.1-alpha.18 → 0.0.1-alpha.20
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/dist/ConfigEditor-YYZQ7TUJ.js +2 -0
- package/dist/ConfigEditor-YYZQ7TUJ.js.map +1 -0
- package/dist/{DictBrowser-ZQ773KQV.js → DictBrowser-I6AEYY2I.js} +2 -2
- package/dist/{HelpScreen-Y3TWEEWZ.js → HelpScreen-IKHDZGTG.js} +2 -2
- package/dist/PracticeScreen-4GUFONK5.js +2 -0
- package/dist/PracticeScreen-4GUFONK5.js.map +1 -0
- package/dist/{StatsViewer-XARAMVZW.js → StatsViewer-JIFWFPDL.js} +2 -2
- package/dist/{WordLookup-LKBEPDTB.js → WordLookup-6JFXQYSQ.js} +2 -2
- package/dist/{chunk-DURXS5MX.js → chunk-EBAA2ZKH.js} +2 -2
- package/dist/{chunk-TCYEMBFW.js → chunk-G2SXVAHM.js} +2 -2
- package/dist/{chunk-CQRKGMPU.js → chunk-MFGIEKBU.js} +2 -2
- package/dist/chunk-VIOZNKSK.js +2 -0
- package/dist/chunk-VIOZNKSK.js.map +1 -0
- package/dist/chunk-VTIB5Q36.js +2 -0
- package/dist/chunk-VTIB5Q36.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/{config.impl-3O5SL5QY.js → config.impl-55HI447G.js} +2 -2
- package/dist/{dict.impl-JAQ2GXCS.js → dict.impl-2SWDIDWC.js} +2 -2
- package/dist/{doctor.impl-XVZCEC2O.js → doctor.impl-KOEIKBKP.js} +2 -2
- package/dist/{menu.impl-HBP6CFSW.js → menu.impl-A3NQTDOR.js} +2 -2
- package/dist/{practice.impl-AVTRDL7X.js → practice.impl-FQ4HFJAL.js} +2 -2
- package/dist/{word.impl-CUBY4EZ4.js → word.impl-EJNGBD7U.js} +2 -2
- package/package.json +1 -1
- package/dist/ConfigEditor-ZJ52VG3I.js +0 -2
- package/dist/ConfigEditor-ZJ52VG3I.js.map +0 -1
- package/dist/PracticeScreen-PWXXG33U.js +0 -2
- package/dist/PracticeScreen-PWXXG33U.js.map +0 -1
- package/dist/chunk-7LTZGB7F.js +0 -2
- package/dist/chunk-7LTZGB7F.js.map +0 -1
- package/dist/chunk-FQ2MEK7M.js +0 -2
- package/dist/chunk-FQ2MEK7M.js.map +0 -1
- /package/dist/{DictBrowser-ZQ773KQV.js.map → DictBrowser-I6AEYY2I.js.map} +0 -0
- /package/dist/{HelpScreen-Y3TWEEWZ.js.map → HelpScreen-IKHDZGTG.js.map} +0 -0
- /package/dist/{StatsViewer-XARAMVZW.js.map → StatsViewer-JIFWFPDL.js.map} +0 -0
- /package/dist/{WordLookup-LKBEPDTB.js.map → WordLookup-6JFXQYSQ.js.map} +0 -0
- /package/dist/{chunk-DURXS5MX.js.map → chunk-EBAA2ZKH.js.map} +0 -0
- /package/dist/{chunk-TCYEMBFW.js.map → chunk-G2SXVAHM.js.map} +0 -0
- /package/dist/{chunk-CQRKGMPU.js.map → chunk-MFGIEKBU.js.map} +0 -0
- /package/dist/{config.impl-3O5SL5QY.js.map → config.impl-55HI447G.js.map} +0 -0
- /package/dist/{dict.impl-JAQ2GXCS.js.map → dict.impl-2SWDIDWC.js.map} +0 -0
- /package/dist/{doctor.impl-XVZCEC2O.js.map → doctor.impl-KOEIKBKP.js.map} +0 -0
- /package/dist/{menu.impl-HBP6CFSW.js.map → menu.impl-A3NQTDOR.js.map} +0 -0
- /package/dist/{practice.impl-AVTRDL7X.js.map → practice.impl-FQ4HFJAL.js.map} +0 -0
- /package/dist/{word.impl-CUBY4EZ4.js.map → word.impl-EJNGBD7U.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 soundsPronunciationRate: string;\n soundsPronunciationSource: string;\n language: string;\n stealth: string;\n wordDisplay: string;\n };\n enumValues: {\n stealth: { off: string; menu: string; default: string };\n soundsPronunciationSource: { youdao: string; dictapi: string };\n defaultMode: { order: string; dictation: string; review: string; random: string; loop: string };\n wordDisplay: { auto: string; huge: string; standard: 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 soundsPronunciationRate: 'pronunciation rate',\n soundsPronunciationSource: 'pronunciation source',\n language: 'language',\n stealth: 'stealth mode',\n wordDisplay: 'word display',\n },\n enumValues: {\n stealth: { off: 'off', menu: 'show in menu', default: 'default practice' },\n soundsPronunciationSource: { youdao: 'Youdao', dictapi: 'Wiktionary' },\n defaultMode: { order: 'order', dictation: 'dictation', review: 'review', random: 'random', loop: 'loop' },\n wordDisplay: { auto: 'auto', huge: 'huge (2×2)', standard: 'standard' },\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 soundsPronunciationRate: '发音速率',\n soundsPronunciationSource: '发音音源',\n language: '语言',\n stealth: '神隐模式',\n wordDisplay: '单词显示',\n },\n enumValues: {\n stealth: { off: '关闭', menu: '主菜单显示', default: '默认练习模式' },\n soundsPronunciationSource: { youdao: '有道词典', dictapi: '维基词典' },\n defaultMode: { order: '顺序', dictation: '默写', review: '复习', random: '乱序', loop: '循环' },\n wordDisplay: { auto: '自动', huge: '大号 (2×2)', standard: '标准' },\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"]}
|
|
@@ -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),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,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"),wordDisplay:n.enum(["auto","huge","standard"]).default("auto")}),i=null;function k(){return i||(i=d.parse({}),i)}async function j(){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 x(e){await m(a.config,e)}function $(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 P(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{j as a,x as b,$ as c,P as d};
|
|
2
|
+
//# sourceMappingURL=chunk-VTIB5Q36.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 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 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 wordDisplay: z.enum(['auto', 'huge', 'standard']).default('auto'),\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":"iDAAA,OAAS,KAAAA,MAAS,MAIX,IAAMC,EAAeC,EAAE,OAAO,CACnC,OAAQA,EAAE,KAAK,CAAC,WAAY,QAAQ,CAAC,EAAE,QAAQ,UAAU,EACzD,OAAQA,EAAE,KAAK,CAAC,KAAM,IAAI,CAAC,EAAE,QAAQ,IAAI,EACzC,YAAaA,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,GAAG,EAAE,QAAQ,EAAE,EAC5D,OAAQA,EACL,OAAO,CACN,OAAQA,EAAE,QAAQ,EAAE,QAAQ,EAAI,EAChC,UAAWA,EAAE,QAAQ,EAAE,QAAQ,EAAI,EACnC,SAAUA,EAAE,QAAQ,EAAE,QAAQ,EAAI,EAClC,kBAAmBA,EAChB,OAAO,EACP,OAAQC,GAAM,CAAC,GAAK,IAAM,EAAG,KAAM,GAAG,EAAE,SAASA,CAAC,EAAG,CACpD,QAAS,sDACX,CAAC,EACA,QAAQ,CAAC,EACZ,oBAAqBD,EAAE,KAAK,CAAC,SAAU,SAAS,CAAC,EAAE,QAAQ,QAAQ,CACrE,CAAC,EACA,QAAQ,CACP,OAAQ,GACR,UAAW,GACX,SAAU,GACV,kBAAmB,EACnB,oBAAqB,QACvB,CAAC,EACH,sBAAuBA,EAAE,QAAQ,EAAE,QAAQ,EAAI,EAC/C,YAAaA,EAAE,KAAK,CAAC,QAAS,YAAa,SAAU,SAAU,MAAM,CAAC,EAAE,QAAQ,OAAO,EACvF,YAAaA,EAAE,OAAO,EAAE,SAAS,EACjC,SAAUA,EAAE,KAAK,CAAC,OAAQ,KAAM,IAAI,CAAC,EAAE,QAAQ,MAAM,EACrD,QAASA,EAAE,KAAK,CAAC,MAAO,OAAQ,SAAS,CAAC,EAAE,QAAQ,KAAK,EACzD,YAAaA,EAAE,KAAK,CAAC,OAAQ,OAAQ,UAAU,CAAC,EAAE,QAAQ,MAAM,CAClE,CAAC,EAMGE,EAAgC,KACpC,SAASC,GAAsB,CAC7B,OAAID,IACJA,EAAiBH,EAAa,MAAM,CAAC,CAAC,EAC/BG,EACT,CAEA,eAAsBE,GAA8B,CAClD,IAAMC,EAAM,MAAMC,EAAkBC,EAAM,MAAM,EAChD,GAAI,CAACF,EAAK,OAAOF,EAAY,EAC7B,IAAMK,EAAST,EAAa,UAAUM,CAAG,EACzC,GAAI,CAACG,EAAO,QACV,MAAM,IAAI,MAAM,qBAAqBD,EAAM,MAAM,KAAKC,EAAO,MAAM,OAAO,EAAE,EAE9E,OAAOA,EAAO,IAChB,CAEA,eAAsBC,EAAWC,EAA4B,CAC3D,MAAMC,EAAgBJ,EAAM,OAAQG,CAAG,CACzC,CAEO,SAASE,EAAUF,EAAaG,EAAuB,CAC5D,IAAMC,EAAQD,EAAK,MAAM,GAAG,EACxBE,EAAeL,EACnB,QAAWM,KAAKF,EAAO,CACrB,GAAIC,IAAQ,MAAQ,OAAOA,GAAQ,SAAU,OAC7CA,EAAOA,EAAgCC,CAAC,CAC1C,CACA,OAAOD,CACT,CAEO,SAASE,EAAUP,EAAaG,EAAcK,EAA0B,CAC7E,IAAMJ,EAAQD,EAAK,MAAM,GAAG,EAC5B,GAAIC,EAAM,SAAW,EAAG,MAAM,IAAI,MAAM,kBAAkB,EAC1D,IAAMK,EAAiC,KAAK,MAAM,KAAK,UAAUT,CAAG,CAAC,EACjEK,EAA+BI,EACnC,QAASC,EAAI,EAAGA,EAAIN,EAAM,OAAS,EAAGM,IAAK,CACzC,IAAMC,EAAIP,EAAMM,CAAC,EACXE,EAAOP,EAAIM,CAAC,EAClB,GAAI,OAAOC,GAAS,UAAYA,IAAS,KACvC,MAAM,IAAI,MAAM,cAAcT,CAAI,KAAKC,EAAM,MAAM,EAAGM,EAAI,CAAC,EAAE,KAAK,GAAG,CAAC,mBAAmB,EAE3FL,EAAMO,CACR,CACA,IAAMC,EAAOT,EAAMA,EAAM,OAAS,CAAC,EACnCC,EAAIQ,CAAI,EAAIC,EAAON,CAAQ,EAC3B,IAAMO,EAAY1B,EAAa,UAAUoB,CAAK,EAC9C,GAAI,CAACM,EAAU,QACb,MAAM,IAAI,MAAM,qBAAqBZ,CAAI,KAAKY,EAAU,MAAM,OAAO,CAAC,GAAG,SAAW,SAAS,EAAE,EAEjG,OAAOA,EAAU,IACnB,CAEA,SAASD,EAAOvB,EAAoB,CAClC,OAAIA,IAAM,OAAe,GACrBA,IAAM,QAAgB,GACtBA,IAAM,OAAe,KACrB,UAAU,KAAKA,CAAC,GAChB,eAAe,KAAKA,CAAC,EAAU,OAAOA,CAAC,EACpCA,CACT","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"]}
|
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-55HI447G.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-55HI447G.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-55HI447G.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-2SWDIDWC.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-2SWDIDWC.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-2SWDIDWC.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-2SWDIDWC.js");await n(t,a)}),o.command("rm <id>").description("Remove a local dictionary").action(async t=>{let{dictRemove:a}=await import("./dict.impl-2SWDIDWC.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-KOEIKBKP.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-FQ4HFJAL.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-WX3BFWI3.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-EJNGBD7U.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-FQ4HFJAL.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-A3NQTDOR.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.20");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-VTIB5Q36.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-55HI447G.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-EBAA2ZKH.js";import"./chunk-VTIB5Q36.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-2SWDIDWC.js.map
|
|
@@ -1,4 +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-
|
|
1
|
+
import{a as f,g as u,h as d,i as y}from"./chunk-BIBS2Q3E.js";import{a as w}from"./chunk-VTIB5Q36.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
2
|
`+o+" ")}`)}return s.join(`
|
|
3
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-
|
|
4
|
+
//# sourceMappingURL=doctor.impl-KOEIKBKP.js.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{a as o,b as s,c as p,d as c}from"./chunk-
|
|
2
|
-
//# sourceMappingURL=menu.impl-
|
|
1
|
+
import{a as o,b as s,c as p,d as c}from"./chunk-G2SXVAHM.js";import{d as n,g as a,i as m}from"./chunk-6ROGUGNX.js";import"./chunk-BIBS2Q3E.js";import"./chunk-MFGIEKBU.js";import{a as i}from"./chunk-VTIB5Q36.js";import"./chunk-GULN5HRV.js";import"./chunk-QG7ZTS2G.js";import{e}from"./chunk-VIOZNKSK.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-FQ4HFJAL.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-A3NQTDOR.js.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{a as c,b as g,c as h,d as x}from"./chunk-
|
|
2
|
-
//# sourceMappingURL=practice.impl-
|
|
1
|
+
import{a as c,b as g,c as h,d as x}from"./chunk-G2SXVAHM.js";import{f as m,g as l,i as u}from"./chunk-6ROGUGNX.js";import"./chunk-BIBS2Q3E.js";import"./chunk-MFGIEKBU.js";import{a}from"./chunk-VTIB5Q36.js";import{b as f,e as p}from"./chunk-GULN5HRV.js";import"./chunk-QG7ZTS2G.js";import{e as d}from"./chunk-VIOZNKSK.js";import"./chunk-NA5UNUVL.js";import"./chunk-E6BBQALJ.js";import s from"chalk";import{render as v}from"ink";import{createElement as E}from"react";var C=["order","dictation","review","random","loop"];function P(o){return C.includes(o)}async function Y(o,t){if(!process.stdout.isTTY){console.error(s.red("Practice requires an interactive TTY.")),process.exitCode=1;return}let e=await a(),i=o??e.defaultDict;if(!i){console.error(s.red("No dictionary specified. Pass an id or set config.defaultDict.")),process.exitCode=1;return}let n=t.mode??e.defaultMode;if(!P(n)){console.error(s.red(`Invalid mode "${n}". Valid: ${C.join(", ")}`)),process.exitCode=1;return}let M=t.chapter!==void 0?Math.max(0,Number(t.chapter)-1):p(await f(),i),r=t.stealth===!0||e.stealth==="default";r||c(),l();let{waitUntilExit:S}=v(E(g,{initial:{name:"practice",params:{dictId:i,chapterIndex:M,mode:n,stealth:r}},initialCfg:e,inline:r}),{patchConsole:!1,exitOnCtrlC:!1});if(await S(),r||h(),m()){process.stdout.write("\x1B[3F\x1B[0J");return}if(r)return;let{lang:w,t:b}=d(e.language);x(u(),b,w)}export{Y as runPractice};
|
|
2
|
+
//# sourceMappingURL=practice.impl-FQ4HFJAL.js.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{c as w}from"./chunk-
|
|
2
|
-
//# sourceMappingURL=word.impl-
|
|
1
|
+
import{c as w}from"./chunk-EBAA2ZKH.js";import"./chunk-VTIB5Q36.js";import{a as h}from"./chunk-G3DQB7FI.js";import{d as g}from"./chunk-NA5UNUVL.js";import{a as u}from"./chunk-E6BBQALJ.js";import i from"chalk";import{readdir as k}from"fs/promises";async function j(){try{return(await k(u.dictsDir)).filter(s=>s.endsWith(".json")&&!s.endsWith(".meta.json")).map(s=>s.replace(/\.json$/,""))}catch{return[]}}async function N(c,s){let p=c.toLowerCase(),a=await j();if(a.length===0){console.log(i.yellow("No local dictionaries. Run `qwerty dict pull <id>` first."));return}let l=[];for(let o of a){let t=await w(o);if(t)for(let n of t){let e=n.name.toLowerCase();(s.exact?e===p:e.includes(p))&&l.push({dictId:o,word:n})}}if(l.length===0){console.log(i.yellow(`No matches for "${c}" in ${a.length} local dictionaries`));return}let d=new Map;for(let o of l){let t=d.get(o.word.name)??[];t.push(o),d.set(o.word.name,t)}let y=await h();for(let[o,t]of d){let n=t[0].word;console.log(),console.log(i.bold.white(o));let e=n.usphone?`US /${n.usphone}/`:"",m=n.ukphone?`UK /${n.ukphone}/`:"";(e||m)&&console.log(i.dim(` ${[e,m].filter(Boolean).join(" ")}`));for(let r of n.trans??[])console.log(i.cyan(` \xB7 ${r}`));let $=await Promise.all(t.map(async r=>(await g(r.dictId))?.name??r.dictId));console.log(i.dim(` in: ${$.join(", ")}`));let f=y[o];f&&console.log(i.dim(` mistakes: ${f.count} (last ${f.lastSeen.slice(0,10)})`))}console.log()}export{N as runWordLookup};
|
|
2
|
+
//# sourceMappingURL=word.impl-EJNGBD7U.js.map
|
package/package.json
CHANGED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{b as P}from"./chunk-CQRKGMPU.js";import{d as E}from"./chunk-7LTZGB7F.js";import{b as $,c as B,e as C}from"./chunk-QG7ZTS2G.js";import{b as F,d as M,f as l}from"./chunk-FQ2MEK7M.js";import"./chunk-NA5UNUVL.js";import"./chunk-E6BBQALJ.js";import{useState as k}from"react";import{Box as g,Text as d,useInput as V}from"ink";import{jsx as c,jsxs as S}from"react/jsx-runtime";var f=[{kind:"dictRef",path:"defaultDict",labelKey:"defaultDict"},{kind:"enum",path:"defaultMode",labelKey:"defaultMode",options:["order","dictation","review","random","loop"]},{kind:"enum",path:"accent",labelKey:"accent",options:["us","uk"]},{kind:"enum",path:"language",labelKey:"language",options:["auto","zh","en"]},{kind:"enum",path:"mirror",labelKey:"mirror",options:["jsdelivr","github"]},{kind:"enum",path:"stealth",labelKey:"stealth",options:["off","menu","default"]},{kind:"int",path:"chapterSize",labelKey:"chapterSize",min:1,max:200},{kind:"bool",path:"autoplayPronunciation",labelKey:"autoplayPronunciation"},{kind:"bool",path:"sounds.master",labelKey:"soundsMaster"},{kind:"bool",path:"sounds.keystroke",labelKey:"soundsKeystroke"},{kind:"bool",path:"sounds.feedback",labelKey:"soundsFeedback"},{kind:"enum",path:"sounds.pronunciationRate",labelKey:"soundsPronunciationRate",options:["0.5","0.75","1","1.25","1.5"]},{kind:"enum",path:"sounds.pronunciationSource",labelKey:"soundsPronunciationSource",options:["youdao","dictapi"]}];function N(a,e){return e.split(".").reduce((o,s)=>{if(o&&typeof o=="object")return o[s]},a)}function U(){let a=F(),{cfg:e,setCfg:o}=P(),s=M(),x=$(e.defaultDict),[y,K]=k(0),[m,h]=k(!1),[w,p]=k(""),[v,u]=k(null),r=f[y],T=N(e,r.path),b=async i=>{try{let n=E(e,r.path,i);await o(n),h(!1),u(null)}catch(n){u(n.message)}};V((i,n)=>{if(m&&r.kind==="string"){if(n.escape){h(!1),u(null);return}if(n.return){b(w);return}if(n.backspace||n.delete){p(t=>t.slice(0,-1));return}i&&!n.ctrl&&!n.meta&&p(t=>t+i);return}if(m&&r.kind==="int"){if(n.escape){h(!1),u(null);return}if(n.return){b(w);return}if(n.backspace||n.delete){p(t=>t.slice(0,-1));return}/^[0-9]$/.test(i)&&p(t=>t+i);return}if(n.escape){a.back();return}if(n.upArrow){K(t=>(t-1+f.length)%f.length),u(null);return}if(n.downArrow){K(t=>(t+1)%f.length),u(null);return}if(r.kind==="bool"&&(i===" "||n.return)){b(T?"false":"true");return}if(r.kind==="enum"&&(n.leftArrow||n.rightArrow)){let t=r.options.indexOf(String(T)),D=n.rightArrow?1:-1,R=r.options[(t+D+r.options.length)%r.options.length];b(R);return}if(r.kind==="dictRef"&&n.return){a.navigate({name:"dict",params:{pickerMode:"set-default"}});return}(r.kind==="string"||r.kind==="int")&&n.return&&(p(String(T??"")),h(!0),u(null))});let z=Math.max(...f.map(i=>B(s.config.fields[i.labelKey])))+4;return S(g,{flexDirection:"column",paddingX:2,paddingY:1,width:"100%",height:"100%",children:[c(d,{bold:!0,color:l.accent,children:s.config.title}),c(g,{marginTop:1,flexDirection:"column",flexGrow:1,children:f.map((i,n)=>{let t=n===y,D=N(e,i.path),R=j(i,D,t&&m?w:null,s,i.path==="defaultDict"?x:""),A=s.config.fields[i.labelKey],I=" ".repeat(Math.max(0,z-B(A)));return S(g,{children:[c(d,{color:t?l.accent:l.muted,children:t?"\u258C ":" "}),S(d,{bold:t,color:t?l.text:l.muted,children:[A,I]}),c(d,{color:t?l.accent:l.muted,children:R})]},i.path)})}),v&&c(g,{marginTop:1,children:S(d,{color:l.error,children:["! ",v]})}),c(g,{marginTop:1,children:c(d,{color:l.muted,children:L(r,m,s)})})]})}function j(a,e,o,s,x){return o!==null?`${o}_`:a.kind==="bool"?e?`\u2713 ${s.common.on}`:`\u2717 ${s.common.off}`:a.kind==="dictRef"?e?C(x||String(e),24):"\u2014":a.kind==="enum"?`< ${s.config.enumValues[a.labelKey]?.[String(e)]??String(e)} >`:String(e??"")}function L(a,e,o){return e?o.config.hints.editing:a.kind==="bool"?o.config.hints.bool:a.kind==="enum"?o.config.hints.enum:a.kind==="dictRef"?o.config.hints.dictRef:o.config.hints.stringOrInt}export{U as ConfigEditor};
|
|
2
|
-
//# sourceMappingURL=ConfigEditor-ZJ52VG3I.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/ui/screens/ConfigEditor.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { Box, Text, useInput } from 'ink';\nimport { useNav } from '../nav.js';\nimport { useAppState } from '../app-state.js';\nimport { useStrings } from '../../i18n/context.js';\nimport { useDictName } from '../registry-context.js';\nimport { setByPath, type Config } from '../../infra/config-store.js';\nimport { PALETTE } from '../components/BigWord.js';\nimport { visibleWidth } from '../../util/text.js';\nimport { truncateName } from '../../util/dict-name.js';\nimport type { Strings } from '../../i18n/strings.js';\n\ntype FieldSpec =\n | { kind: 'enum'; path: string; labelKey: keyof Strings['config']['fields']; options: string[] }\n | { kind: 'bool'; path: string; labelKey: keyof Strings['config']['fields'] }\n | { kind: 'int'; path: string; labelKey: keyof Strings['config']['fields']; min: number; max: number }\n | { kind: 'string'; path: string; labelKey: keyof Strings['config']['fields'] }\n | { kind: 'dictRef'; path: 'defaultDict'; labelKey: keyof Strings['config']['fields'] };\n\nconst FIELDS: FieldSpec[] = [\n { kind: 'dictRef', path: 'defaultDict', labelKey: 'defaultDict' },\n { kind: 'enum', path: 'defaultMode', labelKey: 'defaultMode', options: ['order', 'dictation', 'review', 'random', 'loop'] },\n { kind: 'enum', path: 'accent', labelKey: 'accent', options: ['us', 'uk'] },\n { kind: 'enum', path: 'language', labelKey: 'language', options: ['auto', 'zh', 'en'] },\n { kind: 'enum', path: 'mirror', labelKey: 'mirror', options: ['jsdelivr', 'github'] },\n { kind: 'enum', path: 'stealth', labelKey: 'stealth', options: ['off', 'menu', 'default'] },\n { kind: 'int', path: 'chapterSize', labelKey: 'chapterSize', min: 1, max: 200 },\n { kind: 'bool', path: 'autoplayPronunciation', labelKey: 'autoplayPronunciation' },\n { kind: 'bool', path: 'sounds.master', labelKey: 'soundsMaster' },\n { kind: 'bool', path: 'sounds.keystroke', labelKey: 'soundsKeystroke' },\n { kind: 'bool', path: 'sounds.feedback', labelKey: 'soundsFeedback' },\n {\n kind: 'enum',\n path: 'sounds.pronunciationRate',\n labelKey: 'soundsPronunciationRate',\n options: ['0.5', '0.75', '1', '1.25', '1.5'],\n },\n {\n kind: 'enum',\n path: 'sounds.pronunciationSource',\n labelKey: 'soundsPronunciationSource',\n options: ['youdao', 'dictapi'],\n },\n];\n\nfunction getByPath(cfg: Config, path: string): unknown {\n return path.split('.').reduce<unknown>((acc, k) => {\n if (acc && typeof acc === 'object') return (acc as Record<string, unknown>)[k];\n return undefined;\n }, cfg);\n}\n\nexport function ConfigEditor() {\n const nav = useNav();\n const { cfg, setCfg } = useAppState();\n const t = useStrings();\n const defaultDictName = useDictName(cfg.defaultDict);\n const [selected, setSelected] = useState(0);\n const [editing, setEditing] = useState(false);\n const [draft, setDraft] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const field = FIELDS[selected]!;\n const currentValue = getByPath(cfg, field.path);\n\n const commit = async (raw: string) => {\n try {\n const next = setByPath(cfg, field.path, raw);\n await setCfg(next);\n setEditing(false);\n setError(null);\n } catch (err) {\n setError((err as Error).message);\n }\n };\n\n useInput((input, key) => {\n if (editing && field.kind === 'string') {\n if (key.escape) {\n setEditing(false);\n setError(null);\n return;\n }\n if (key.return) {\n void commit(draft);\n return;\n }\n if (key.backspace || key.delete) {\n setDraft((d) => d.slice(0, -1));\n return;\n }\n if (input && !key.ctrl && !key.meta) setDraft((d) => d + input);\n return;\n }\n if (editing && field.kind === 'int') {\n if (key.escape) {\n setEditing(false);\n setError(null);\n return;\n }\n if (key.return) {\n void commit(draft);\n return;\n }\n if (key.backspace || key.delete) {\n setDraft((d) => d.slice(0, -1));\n return;\n }\n if (/^[0-9]$/.test(input)) setDraft((d) => d + input);\n return;\n }\n\n if (key.escape) {\n nav.back();\n return;\n }\n if (key.upArrow) {\n setSelected((i) => (i - 1 + FIELDS.length) % FIELDS.length);\n setError(null);\n return;\n }\n if (key.downArrow) {\n setSelected((i) => (i + 1) % FIELDS.length);\n setError(null);\n return;\n }\n if (field.kind === 'bool' && (input === ' ' || key.return)) {\n void commit(currentValue ? 'false' : 'true');\n return;\n }\n if (field.kind === 'enum' && (key.leftArrow || key.rightArrow)) {\n const idx = field.options.indexOf(String(currentValue));\n const delta = key.rightArrow ? 1 : -1;\n const next = field.options[(idx + delta + field.options.length) % field.options.length]!;\n void commit(next);\n return;\n }\n if (field.kind === 'dictRef' && key.return) {\n nav.navigate({ name: 'dict', params: { pickerMode: 'set-default' } });\n return;\n }\n if ((field.kind === 'string' || field.kind === 'int') && key.return) {\n setDraft(String(currentValue ?? ''));\n setEditing(true);\n setError(null);\n }\n });\n\n const labelW = Math.max(...FIELDS.map((f) => visibleWidth(t.config.fields[f.labelKey]))) + 4;\n\n return (\n <Box flexDirection=\"column\" paddingX={2} paddingY={1} width=\"100%\" height=\"100%\">\n <Text bold color={PALETTE.accent}>\n {t.config.title}\n </Text>\n\n <Box marginTop={1} flexDirection=\"column\" flexGrow={1}>\n {FIELDS.map((f, i) => {\n const active = i === selected;\n const value = getByPath(cfg, f.path);\n const display = renderValue(\n f,\n value,\n active && editing ? draft : null,\n t,\n f.path === 'defaultDict' ? defaultDictName : '',\n );\n const label = t.config.fields[f.labelKey];\n const pad = ' '.repeat(Math.max(0, labelW - visibleWidth(label)));\n return (\n <Box key={f.path}>\n <Text color={active ? PALETTE.accent : PALETTE.muted}>{active ? '▌ ' : ' '}</Text>\n <Text bold={active} color={active ? PALETTE.text : PALETTE.muted}>\n {label}\n {pad}\n </Text>\n <Text color={active ? PALETTE.accent : PALETTE.muted}>{display}</Text>\n </Box>\n );\n })}\n </Box>\n\n {error && (\n <Box marginTop={1}>\n <Text color={PALETTE.error}>! {error}</Text>\n </Box>\n )}\n\n <Box marginTop={1}>\n <Text color={PALETTE.muted}>{hintFor(field, editing, t)}</Text>\n </Box>\n </Box>\n );\n}\n\nfunction renderValue(\n field: FieldSpec,\n value: unknown,\n draft: string | null,\n t: Strings,\n dictDisplayName: string,\n): string {\n if (draft !== null) return `${draft}_`;\n if (field.kind === 'bool') return value ? `✓ ${t.common.on}` : `✗ ${t.common.off}`;\n if (field.kind === 'dictRef') {\n if (!value) return '—';\n return truncateName(dictDisplayName || String(value), 24);\n }\n if (field.kind === 'enum') {\n const map = (t.config.enumValues as Record<string, Record<string, string> | undefined>)[\n field.labelKey\n ];\n const label = map?.[String(value)] ?? String(value);\n return `< ${label} >`;\n }\n return String(value ?? '');\n}\n\nfunction hintFor(field: FieldSpec, editing: boolean, t: Strings): string {\n if (editing) return t.config.hints.editing;\n if (field.kind === 'bool') return t.config.hints.bool;\n if (field.kind === 'enum') return t.config.hints.enum;\n if (field.kind === 'dictRef') return t.config.hints.dictRef;\n return t.config.hints.stringOrInt;\n}\n"],"mappings":"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","names":["useState","Box","Text","useInput","jsx","jsxs","FIELDS","getByPath","cfg","path","acc","k","ConfigEditor","nav","useNav","setCfg","useAppState","t","useStrings","defaultDictName","useDictName","selected","setSelected","useState","editing","setEditing","draft","setDraft","error","setError","field","currentValue","commit","raw","next","setByPath","err","useInput","input","key","d","i","idx","delta","labelW","f","visibleWidth","Box","Text","PALETTE","active","value","display","renderValue","label","pad","hintFor","dictDisplayName","truncateName"]}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{b as ht,e as bt,h as Tt}from"./chunk-6ROGUGNX.js";import{a as ut,b as lt,c as dt,d as mt,e as ft,f as pt}from"./chunk-BIBS2Q3E.js";import{e as st}from"./chunk-DURXS5MX.js";import{b as Y}from"./chunk-CQRKGMPU.js";import"./chunk-7LTZGB7F.js";import{a as V,b as wt,c as St}from"./chunk-G3DQB7FI.js";import{a as yt}from"./chunk-GULN5HRV.js";import{b as gt,e as L}from"./chunk-QG7ZTS2G.js";import{b as X,d as W,f as i,g as xt}from"./chunk-FQ2MEK7M.js";import"./chunk-NA5UNUVL.js";import"./chunk-E6BBQALJ.js";import{useState as j,useEffect as H,useRef as tt}from"react";import{Box as f,Text as g,useApp as de,useInput as O}from"ink";function It(t,e=Math.random){let r=[...t];for(let n=r.length-1;n>0;n--){let c=Math.floor(e()*(n+1)),d=r[n];r[n]=r[c],r[c]=d}return r}function kt(t){let e=t>>>0;return()=>{e=e+1831565813>>>0;let r=Math.imul(e^e>>>15,1|e);return r=r+Math.imul(r^r>>>7,61|r)^r,((r^r>>>14)>>>0)/4294967296}}function Ct(t,e){if(e<=0)throw new Error("chapterSize must be positive");let r=[];for(let n=0;n<t.length;n+=e)r.push(t.slice(n,n+e));return r}function Mt(t,e,r){if(e==="random"){let n=r===void 0?Math.random:kt(r);return It(t,n)}return t}function _(t){return{target:t,typed:"",errorsThisWord:0}}function Wt(t,e){switch(e.type){case"reset":return{state:{...t,typed:""},effect:"none"};case"backspace":return t.typed.length===0?{state:t,effect:"none"}:{state:{...t,typed:t.typed.slice(0,-1)},effect:"none"};case"char":{let r=t.typed+e.ch,n=[...t.target].slice(0,[...r].length).join("");return r===n?r.length===t.target.length?{state:{...t,typed:r},effect:"correct"}:{state:{...t,typed:r},effect:"progress"}:{state:{...t,typed:"",errorsThisWord:t.errorsThisWord+1},effect:"wrong"}}}}function J(t,e=Date.now()){return t.length===0?{startedAt:e,results:[],current:null,finishedAt:e,playlist:t}:{startedAt:e,results:[],current:{wordIndex:0,wordStartedAt:e,input:_(t[0].name)},finishedAt:null,playlist:t}}function Q(t,e,r=Date.now()){if(!t.current)return{session:t,effect:"none"};let{state:n,effect:c}=Wt(t.current.input,e);if(c==="correct"){let d={word:n.target,errors:n.errorsThisWord,durationMs:r-t.current.wordStartedAt},a=t.current.wordIndex+1,s=[...t.results,d];return a>=t.playlist.length?{session:{...t,results:s,current:null,finishedAt:r},effect:c}:{session:{...t,results:s,current:{wordIndex:a,wordStartedAt:r,input:_(t.playlist[a].name)}},effect:c}}return{session:{...t,current:{...t.current,input:n}},effect:c}}function vt(t,e=Date.now()){if(!t.current)return{session:t,effect:"none"};let r={word:t.current.input.target,errors:0,durationMs:e-t.current.wordStartedAt,skipped:!0},n=t.current.wordIndex+1,c=[...t.results,r];return n>=t.playlist.length?{session:{...t,results:c,current:null,finishedAt:e},effect:"skipped"}:{session:{...t,results:c,current:{wordIndex:n,wordStartedAt:e,input:_(t.playlist[n].name)}},effect:"skipped"}}function Z(t){let e=t.results.reduce((c,d)=>c+d.errors,0),r=(t.finishedAt??Date.now())-t.startedAt,n={};for(let c of t.results)c.errors>0&&(n[c.word]=(n[c.word]??0)+c.errors);return{wordCount:t.results.length,errors:e,durationMs:r,perWordErrors:n}}import{useEffect as Et,useReducer as Qt,useRef as Zt,useState as te}from"react";import{useInput as ee,useApp as re}from"ink";function ne(t,e){if(e.type==="start")return{session:J(e.playlist,e.now),lastEffect:null,effectSeq:0};if(e.type==="skip"){let r=vt(t.session,e.now);return{session:r.session,lastEffect:r.effect,effectSeq:t.effectSeq+1}}if(e.type==="event"){if(e.key.backspace||e.key.delete){let c=Q(t.session,{type:"backspace"},e.now);return{session:c.session,lastEffect:c.effect,effectSeq:t.effectSeq+1}}if(e.input.length===0)return t;let r=t.session,n=t.lastEffect;for(let c of e.input){let d=Q(r,{type:"char",ch:c},e.now);if(r=d.session,n=d.effect,r.finishedAt!==null)break}return{session:r,lastEffect:n,effectSeq:t.effectSeq+1}}return t}function oe(t){let e=[...t];if(e.some(n=>n.codePointAt(0)>=128))return{kind:"ime",cleaned:""};let r=e.filter(n=>{let c=n.codePointAt(0);return c>=32&&c<=126}).join("");return{kind:r.length>0?"valid":"noise",cleaned:r}}function Bt({playlist:t,onComplete:e,onTab:r,onEscape:n,onSkip:c,onImeBlock:d,onValidInput:a,enabled:s=!0}){let[l,y]=Qt(ne,void 0,()=>({session:J(t,Date.now()),lastEffect:null,effectSeq:0})),M=Zt(!1),[T,I]=te(0),{exit:w}=re();return ee((p,x)=>{if(x.ctrl&&p==="c"){w();return}if(x.ctrl&&p==="n"){c?.(),y({type:"skip",now:Date.now()});return}if(x.escape){n?.();return}if(x.tab){r?.();return}if(x.upArrow||x.downArrow||x.leftArrow||x.rightArrow||x.return||x.ctrl||x.meta)return;let{kind:v,cleaned:N}=oe(p);if(v==="ime"){d?.();return}v!=="noise"&&(a?.(),y({type:"event",input:N,key:x,now:Date.now()}))},{isActive:s}),Et(()=>{l.session.finishedAt!==null&&!M.current&&(M.current=!0,e(l.session))},[l.session,e]),Et(()=>{if(l.session.finishedAt!==null)return;let p=setInterval(()=>I(x=>x+1),1e3);return()=>clearInterval(p)},[l.session.finishedAt]),{session:l.session,lastEffect:l.lastEffect,effectSeq:l.effectSeq,tick:T}}import{useEffect as ce,useRef as ie}from"react";function At(t){let e=ie(!1);return ce(()=>{e.current||(e.current=!0,ut(!t.enabled).catch(()=>{}))},[t.enabled]),{keystroke:()=>t.enabled&<(),correct:()=>t.enabled&&dt(),wrong:()=>t.enabled&&mt(),pronounce:r=>{t.enabled&&t.autoplayPronunciation&&ft(r,t.accent,t.pronunciationRate,t.pronunciationSource)},prefetch:r=>{t.enabled&&pt(r,t.accent,t.pronunciationSource)}}}import{useCallback as ae}from"react";function Pt(t){return ae(async e=>{let r={ts:new Date().toISOString(),dictId:t.dictId,chapter:t.chapterIndex,mode:t.mode,wordCount:e.wordCount,errors:e.errors,durationMs:e.durationMs,perWordErrors:e.perWordErrors};await yt(r),Tt({dictId:t.dictId,chapterIndex:t.chapterIndex,mode:t.mode,wordCount:e.wordCount,errors:e.errors,durationMs:e.durationMs,perWordErrors:e.perWordErrors});let n=Object.entries(e.perWordErrors).filter(([,d])=>d>0);if(n.length===0)return;let c=await V();for(let[d,a]of n)c=St(c,d,t.dictId,a);await wt(c)},[t.dictId,t.chapterIndex,t.mode])}import{Box as R,Text as m,useStdout as se}from"ink";import{Fragment as le,jsx as u,jsxs as B}from"react/jsx-runtime";var $t=28;function Rt(t){let e=Math.floor(t/1e3),r=Math.floor(e/60),n=e%60;return`${r}:${String(n).padStart(2,"0")}`}function ue(){let{stdout:t}=se(),e=t?.columns??80;return Math.max(20,e-$t)}function E({left:t,right:e}){let r=ue();return B(R,{children:[u(R,{width:r,children:t}),u(R,{width:$t,justifyContent:"flex-end",children:e})]})}function Nt(t){let e=W(),r=[...t.target],n=[...t.typed],c=B(R,{children:[r.map((I,w)=>{let p=w<n.length,x=t.hideTarget&&!p?"_":p?n[w]:I,v=t.error?i.error:p?i.accent:i.muted;return u(m,{bold:!0,color:v,children:x},w)}),t.phonetic&&B(le,{children:[u(m,{children:" "}),u(m,{italic:!0,dimColor:!0,color:i.muted,children:t.phonetic})]})]}),d=t.translation.length>0?u(m,{color:i.primary,children:t.translation[0]}):u(m,{children:" "}),a=t.info,s=Number.isInteger(a.accPct)?`${a.accPct}`:a.accPct.toFixed(1),l=!t.imeBlocked&&t.audioWarning!==null,y=t.imeBlocked?u(m,{color:i.warning,children:e.practice.imeWarningShort}):l?u(m,{color:i.warning,children:e.practice.audioWarningShort}):a.visible?u(m,{color:i.muted,children:`${a.dictName} \xB7 ${a.chapterLabel}`}):u(m,{children:" "}),M=t.imeBlocked||l?u(m,{children:" "}):a.visible?u(m,{color:i.muted,children:`${a.completed}/${a.total} \xB7 ${a.wpm}wpm \xB7 ${s}%`}):u(m,{children:" "}),T=t.imeBlocked||l?u(m,{children:" "}):a.visible?u(m,{color:i.muted,children:Rt(a.elapsedMs)}):u(m,{children:" "});return B(R,{flexDirection:"column",children:[u(E,{left:c,right:y}),u(E,{left:u(m,{children:" "}),right:M}),u(E,{left:d,right:T})]})}function Dt(){let t=W();return B(R,{flexDirection:"column",children:[u(E,{left:u(m,{color:i.warning,children:t.stealth.paused}),right:u(m,{color:i.muted,children:t.stealth.pausedHintRight})}),u(E,{left:u(m,{children:" "}),right:B(m,{color:i.muted,children:["Esc ",t.common.back]})}),u(E,{left:u(m,{children:" "}),right:u(m,{children:" "})})]})}function Lt(t){let e=W(),r=Number.isInteger(t.accPct)?`${t.accPct}`:t.accPct.toFixed(1),n=`${e.stealth.chapterDone} \xB7 ${t.wordCount}w \xB7 ${t.wpm}wpm \xB7 ${r}% \xB7 ${Rt(t.durationMs)}`;return B(R,{flexDirection:"column",children:[u(E,{left:u(m,{color:i.success,children:n}),right:u(m,{color:i.muted,children:e.stealth.nextHintRight})}),u(E,{left:u(m,{children:" "}),right:B(m,{color:i.muted,children:["Esc ",e.common.back]})}),u(E,{left:u(m,{children:" "}),right:u(m,{children:" "})})]})}import{jsx as o,jsxs as C}from"react/jsx-runtime";function ar({params:t}){let{dictId:e,chapterIndex:r,mode:n}=t,{cfg:c}=Y(),d=W(),[a,s]=j("loading"),[l,y]=j(null),[M,T]=j(null);return H(()=>{let I=!1;return s("loading"),y(null),T(null),(async()=>{try{let w=await st(e);if(I)return;if(n==="review"){let N=await V();if(I)return;let q=w.filter(F=>N[F.name]?.count).slice(0,c.chapterSize);if(q.length===0){T(d.practice.errors.noMistakes),s("error");return}y({playlist:q,totalChapters:1}),s("typing");return}let p=Ct(w,c.chapterSize);if(p.length===0){T(d.practice.errors.dictEmpty(e)),s("error");return}let x=Math.max(0,Math.min(p.length-1,r)),v=Mt(p[x],n);y({playlist:v,totalChapters:p.length}),s("typing")}catch(w){if(I)return;T(w.message),s("error")}})(),()=>{I=!0}},[e,r,n,c.chapterSize,d]),a==="loading"?o(be,{text:d.practice.loading,color:i.muted}):a==="error"?o(ge,{msg:M??d.practice.errors.unknown}):l?o(me,{params:t,loaded:l,phase:a,setPhase:s},`${e}-${r}-${n}-${t.stealth?"s":"n"}`):null}function me({params:t,loaded:e,phase:r,setPhase:n}){let{dictId:c,chapterIndex:d,mode:a}=t,s=t.stealth===!0,{cfg:l}=Y(),y=X(),{exit:M}=de(),T=()=>y.stack.length>1?y.back():M(),I=Pt({dictId:c,chapterIndex:d,mode:a}),w=gt(c),p=At({enabled:!s&&l.sounds.master,accent:l.accent,autoplayPronunciation:!s&&l.autoplayPronunciation,pronunciationRate:l.sounds.pronunciationRate,pronunciationSource:l.sounds.pronunciationSource}),x=ht(),v=l.sounds.master?x.warning:null,N=tt(!1),q=tt(0),F=tt(-1),[Ot,rt]=j(!1),[nt,Ft]=j(null),[ot,ct]=j(!1);H(()=>{if(nt===null)return;let h=setTimeout(()=>rt(!1),2e3);return()=>clearTimeout(h)},[nt]);let{session:S,lastEffect:A,effectSeq:K,tick:Vt}=Bt({playlist:e.playlist,enabled:r==="typing",onComplete:h=>{N.current||(N.current=!0,n("summary"),Promise.resolve(I(Z(h))).catch(b=>{console.error("Failed to persist session:",b)}))},onEscape:()=>n(r==="paused"?"typing":"paused"),onTab:s?void 0:()=>{let h=S.current?e.playlist[S.current.wordIndex]:void 0;h&&p.pronounce(h.name)},onImeBlock:()=>ct(!0),onValidInput:()=>ct(!1)});H(()=>{s||K!==q.current&&(q.current=K,A!==null&&(A==="wrong"&&l.sounds.feedback&&p.wrong(),A==="progress"&&l.sounds.keystroke&&p.keystroke(),A==="correct"&&(l.sounds.feedback&&p.correct(),l.sounds.keystroke&&p.keystroke())))},[s,K,A,p,l.sounds.feedback,l.sounds.keystroke]),H(()=>{if(s)return;let h=S.current?.wordIndex??-1;if(h===-1||h===F.current)return;F.current=h;let b=e.playlist[h],$=e.playlist[h+1];b&&l.autoplayPronunciation&&p.pronounce(b.name),$&&p.prefetch($.name)},[s,S.current?.wordIndex,p,l.autoplayPronunciation,e.playlist]),O((h,b)=>{if(b.tab){rt(!0),Ft(Date.now());return}},{isActive:s&&r==="typing"}),O((h,b)=>{if(b.return){n("typing");return}if(b.escape){T();return}},{isActive:r==="paused"}),O((h,b)=>{b.ctrl&&h==="c"&&(bt(!0),M())},{isActive:s&&r==="paused"}),O((h,b)=>{if(b.escape){T();return}if(b.return){let $=d+1;a==="loop"?y.replace({name:"practice",params:{dictId:c,chapterIndex:d,mode:a,stealth:t.stealth}}):a==="review"||$>=e.totalChapters?T():y.replace({name:"practice",params:{dictId:c,chapterIndex:$,mode:a,stealth:t.stealth}});return}if(h==="m"){y.replace({name:"practice",params:{dictId:c,chapterIndex:0,mode:"review",stealth:t.stealth}});return}},{isActive:r==="summary"});let P=S.results.length,_t=S.results.reduce((h,b)=>h+b.errors,0),z=Date.now()-S.startedAt,it=z/6e4,at=it>0?Math.round(P/it*10)/10:0,k=r==="summary"?Z(S):null;if(s){if(r==="paused")return o(Dt,{});if(r==="summary"&&k){let D=k.durationMs/6e4,Ut=D>0?Math.round(k.wordCount/D*10)/10:0,Xt=Object.keys(k.perWordErrors).length,Yt=k.wordCount===0?1:Math.max(0,(k.wordCount-Xt)/k.wordCount),Jt=Math.round(Yt*1e3)/10;return o(Lt,{wordCount:k.wordCount,errors:k.errors,durationMs:k.durationMs,wpm:Ut,accPct:Jt})}let h=S.current?e.playlist[S.current.wordIndex]:e.playlist[e.playlist.length-1],b=S.current?.input??{target:"",typed:"",errorsThisWord:0},$=new Set(S.results.filter(D=>D.errors>0).map(D=>D.word)).size,Ht=P===0?1:Math.max(0,(P-$)/P),Kt=Math.round(Ht*1e3)/10,zt=a==="review"?"review":`ch ${d+1}/${e.totalChapters}`;return o(Nt,{target:h?.name??"",typed:b.typed,hideTarget:a==="dictation",phonetic:jt(h,l.accent),translation:h?.trans??[],error:A==="wrong",imeBlocked:ot,audioWarning:v,info:{visible:Ot,dictName:L(w,24),chapterLabel:zt,completed:P,total:e.playlist.length,wpm:at,accPct:Kt,elapsedMs:z}})}if(r==="paused")return o(he,{dictName:w,chapterIndex:d,totalChapters:e.totalChapters,mode:a,completed:P,total:e.playlist.length});if(r==="summary"&&k)return o(ye,{dictName:w,chapterIndex:d,totalChapters:e.totalChapters,mode:a,summary:k});let U=S.current?e.playlist[S.current.wordIndex]:e.playlist[e.playlist.length-1],Gt=S.current?.input??{target:"",typed:"",errorsThisWord:0};return o(fe,{dictName:w,chapterIndex:d,totalChapters:e.totalChapters,mode:a,accent:l.accent,completed:P,total:e.playlist.length,errors:_t,wpm:at,elapsedMs:z,target:U?.name??"",typed:Gt.typed,flashError:A==="wrong",hideTarget:a==="dictation",phonetic:jt(U,l.accent),translation:U?.trans??[],imeBlocked:ot,audioWarning:v})}function jt(t,e){return t?(e==="us"?t.usphone:t.ukphone)??null:null}function et(t){let e=Math.floor(t/1e3),r=Math.floor(e/60),n=e%60;return`${r}:${String(n).padStart(2,"0")}`}function fe(t){let e=W(),r=t.total===0?0:t.completed/t.total;return C(f,{flexDirection:"column",paddingX:2,paddingY:1,width:"100%",height:"100%",children:[o(pe,{dictName:t.dictName,chapterIndex:t.chapterIndex,totalChapters:t.totalChapters,mode:t.mode,accent:t.accent,completed:t.completed,total:t.total,elapsedMs:t.elapsedMs}),C(f,{flexGrow:1,flexDirection:"column",alignItems:"center",justifyContent:"center",children:[o(xt,{target:t.target,typed:t.typed,error:t.flashError,hideTarget:t.hideTarget}),t.phonetic&&o(f,{marginTop:1,children:o(g,{italic:!0,dimColor:!0,color:i.muted,children:t.phonetic})}),t.translation.length>0&&o(f,{marginTop:1,flexDirection:"column",alignItems:"center",children:t.translation.slice(0,2).map((n,c)=>o(g,{color:i.primary,children:n},c))}),t.imeBlocked&&o(f,{marginTop:1,children:o(g,{color:i.warning,children:e.practice.imeWarning})}),!t.imeBlocked&&t.audioWarning&&o(f,{marginTop:1,children:o(g,{color:i.warning,children:t.audioWarning})})]}),C(f,{flexDirection:"column",children:[o(qt,{frac:r}),o(f,{justifyContent:"center",marginTop:1,children:C(g,{color:i.muted,children:[t.completed,"/",t.total," \xB7 ",et(t.elapsedMs)," \xB7 ",t.wpm," ",e.practice.statCards.wpm," \xB7 ",t.errors," ",e.practice.statCards.errors]})}),o(f,{justifyContent:"center",marginTop:1,children:o(g,{color:i.muted,children:e.practice.footers.typing})})]})]})}function pe(t){let e=W(),r=e.practice.modes[t.mode],n=e.practice.accents[t.accent],c=L(t.dictName,20),d=t.mode==="review"?`${c} \xB7 ${e.practice.reviewLabel} \xB7 ${n}`:`${c} \xB7 ${e.practice.chapterLabel(t.chapterIndex+1,t.totalChapters)} \xB7 ${r} \xB7 ${n}`,a=`${t.completed}/${t.total} \xB7 ${et(t.elapsedMs)}`;return C(f,{children:[o(g,{color:i.muted,children:d}),o(f,{flexGrow:1}),o(g,{color:i.muted,children:a})]})}function qt({frac:t}){let e=process.stdout.columns??80,r=Math.max(20,Math.min(72,e-16)),n=Math.round(r*Math.max(0,Math.min(1,t))),c=r-n;return C(f,{justifyContent:"center",children:[o(g,{color:i.accent,children:"\u2501".repeat(n)}),o(g,{color:i.muted,children:"\u2500".repeat(c)})]})}function he(t){let e=W(),r=t.total===0?0:t.completed/t.total,n=t.mode==="review"?`${L(t.dictName,20)} \xB7 ${e.practice.reviewLabel}`:`${L(t.dictName,20)} \xB7 ${e.practice.pause.chapter(t.chapterIndex+1,t.totalChapters)}`;return C(f,{flexDirection:"column",alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:[o(g,{bold:!0,color:i.warning,children:e.practice.pause.title}),o(f,{marginTop:1,children:o(g,{color:i.muted,children:n})}),o(f,{marginTop:2,children:o(qt,{frac:r})}),o(f,{marginTop:1,children:o(g,{color:i.muted,children:e.practice.pause.progress(t.completed,t.total)})}),o(f,{marginTop:2,children:o(g,{color:i.muted,children:e.practice.pause.hint})})]})}function ge({msg:t}){let e=W();return C(f,{flexDirection:"column",alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:[o(g,{color:i.error,children:t}),o(f,{marginTop:2,children:C(g,{color:i.muted,children:["Esc ",e.common.back]})}),o(xe,{})]})}function xe(){let t=X();return O((e,r)=>{r.escape&&t.back()}),null}function be({text:t,color:e}){return o(f,{alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:o(g,{color:e,children:t})})}function ye(t){let{summary:e}=t,r=e.durationMs/6e4,n=r>0?Math.round(e.wordCount/r*10)/10:0,c=Object.keys(e.perWordErrors).length,d=e.wordCount===0?1:Math.max(0,(e.wordCount-c)/e.wordCount),a=Math.round(d*1e3)/10,s=W(),l=s.practice.modes[t.mode],y=L(t.dictName,20),M=t.mode==="review"?`${y} \xB7 ${s.practice.reviewLabel}`:`${y} \xB7 ${s.practice.chapterLabel(t.chapterIndex+1,t.totalChapters)} \xB7 ${l}`,I=`Enter ${t.mode==="loop"?s.practice.summary.loopAgain:t.mode==="review"||t.chapterIndex+1>=t.totalChapters?s.practice.summary.backMenu:s.practice.summary.nextChapter} \xB7 m ${s.practice.summary.reviewMistakes} \xB7 Esc ${s.practice.summary.backMenu}`;return C(f,{flexDirection:"column",alignItems:"center",justifyContent:"center",paddingY:1,width:"100%",height:"100%",children:[o(g,{bold:!0,color:i.success,children:s.practice.chapterComplete}),o(f,{marginTop:1,children:o(g,{color:i.muted,children:M})}),C(f,{marginTop:3,flexDirection:"row",justifyContent:"center",children:[o(G,{label:s.practice.statCards.words,value:String(e.wordCount),color:i.text}),o(G,{label:s.practice.statCards.errors,value:String(e.errors),color:e.errors>0?i.error:i.muted}),o(G,{label:s.practice.statCards.wpm,value:String(n),color:i.accent}),o(G,{label:s.practice.statCards.accuracy,value:`${a}%`,color:i.accent})]}),o(f,{marginTop:2,children:o(g,{color:i.muted,children:s.practice.statCards.elapsed(et(e.durationMs))})}),o(f,{flexGrow:1}),o(f,{marginTop:2,children:o(g,{color:i.muted,children:I})})]})}function G({label:t,value:e,color:r}){return C(f,{flexDirection:"column",alignItems:"center",marginX:3,children:[o(g,{bold:!0,color:r,children:e}),o(g,{color:i.muted,children:t})]})}export{ar as PracticeScreen};
|
|
2
|
-
//# sourceMappingURL=PracticeScreen-PWXXG33U.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/ui/screens/PracticeScreen.tsx","../src/util/shuffle.ts","../src/domain/chapters.ts","../src/domain/input-buffer.ts","../src/domain/session.ts","../src/ui/hooks/useWordLoop.ts","../src/ui/hooks/useAudio.ts","../src/ui/hooks/useSessionPersistence.ts","../src/ui/screens/StealthPracticeLayout.tsx"],"sourcesContent":["import { useState, useEffect, useRef } from 'react';\nimport { Box, Text, useApp, useInput } from 'ink';\nimport type { Word } from '../../domain/dictionary.js';\nimport type { Mode } from '../../domain/chapters.js';\nimport { chunkChapters, buildPlaylist } from '../../domain/chapters.js';\nimport { sessionSummary } from '../../domain/session.js';\nimport { loadMistakes } from '../../domain/mistakes.js';\nimport { ensureDictionary } from '../../infra/dict-downloader.js';\nimport { useWordLoop } from '../hooks/useWordLoop.js';\nimport { useAudio } from '../hooks/useAudio.js';\nimport { useSessionPersistence } from '../hooks/useSessionPersistence.js';\nimport { useNav, type PracticeParams } from '../nav.js';\nimport { useAppState } from '../app-state.js';\nimport { useAudioStatus } from '../audio-context.js';\nimport { useStrings } from '../../i18n/context.js';\nimport { useDictName } from '../registry-context.js';\nimport { BigWord, PALETTE } from '../components/BigWord.js';\nimport { truncateName } from '../../util/dict-name.js';\nimport { StealthTyping, StealthPaused, StealthSummary } from './StealthPracticeLayout.js';\nimport { setSilentExit } from '../../util/post-exit-action.js';\n\ntype Phase = 'loading' | 'typing' | 'paused' | 'summary' | 'error';\n\ntype Loaded = {\n playlist: Word[];\n totalChapters: number;\n};\n\nexport function PracticeScreen({ params }: { params: PracticeParams }) {\n const { dictId, chapterIndex, mode } = params;\n const { cfg } = useAppState();\n const t = useStrings();\n\n const [phase, setPhase] = useState<Phase>('loading');\n const [loaded, setLoaded] = useState<Loaded | null>(null);\n const [errorMsg, setErrorMsg] = useState<string | null>(null);\n\n useEffect(() => {\n let cancelled = false;\n setPhase('loading');\n setLoaded(null);\n setErrorMsg(null);\n (async () => {\n try {\n const words = await ensureDictionary(dictId);\n if (cancelled) return;\n if (mode === 'review') {\n const book = await loadMistakes();\n if (cancelled) return;\n const reviewWords = words.filter((w) => book[w.name]?.count).slice(0, cfg.chapterSize);\n if (reviewWords.length === 0) {\n setErrorMsg(t.practice.errors.noMistakes);\n setPhase('error');\n return;\n }\n setLoaded({ playlist: reviewWords, totalChapters: 1 });\n setPhase('typing');\n return;\n }\n const chapters = chunkChapters(words, cfg.chapterSize);\n if (chapters.length === 0) {\n setErrorMsg(t.practice.errors.dictEmpty(dictId));\n setPhase('error');\n return;\n }\n const idx = Math.max(0, Math.min(chapters.length - 1, chapterIndex));\n const playlist = buildPlaylist(chapters[idx]!, mode);\n setLoaded({ playlist, totalChapters: chapters.length });\n setPhase('typing');\n } catch (err) {\n if (cancelled) return;\n setErrorMsg((err as Error).message);\n setPhase('error');\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [dictId, chapterIndex, mode, cfg.chapterSize, t]);\n\n if (phase === 'loading') {\n return <Centered text={t.practice.loading} color={PALETTE.muted} />;\n }\n if (phase === 'error') {\n return <ErrorView msg={errorMsg ?? t.practice.errors.unknown} />;\n }\n if (!loaded) return null;\n\n return (\n <PracticeRunner\n key={`${dictId}-${chapterIndex}-${mode}-${params.stealth ? 's' : 'n'}`}\n params={params}\n loaded={loaded}\n phase={phase}\n setPhase={setPhase}\n />\n );\n}\n\nfunction PracticeRunner({\n params,\n loaded,\n phase,\n setPhase,\n}: {\n params: PracticeParams;\n loaded: Loaded;\n phase: Phase;\n setPhase: (p: Phase) => void;\n}) {\n const { dictId, chapterIndex, mode } = params;\n const stealth = params.stealth === true;\n const { cfg } = useAppState();\n const nav = useNav();\n const { exit } = useApp();\n const goBack = () => (nav.stack.length > 1 ? nav.back() : exit());\n const persist = useSessionPersistence({ dictId, chapterIndex, mode });\n const dictName = useDictName(dictId);\n\n const audio = useAudio({\n enabled: !stealth && cfg.sounds.master,\n accent: cfg.accent,\n autoplayPronunciation: !stealth && cfg.autoplayPronunciation,\n pronunciationRate: cfg.sounds.pronunciationRate,\n pronunciationSource: cfg.sounds.pronunciationSource,\n });\n const audioStatus = useAudioStatus();\n // Only surface the warning when the user opted in to sounds — if they\n // disabled sounds via config, \"no player found\" isn't a problem to flag.\n const audioWarn = cfg.sounds.master ? audioStatus.warning : null;\n\n const finishedRef = useRef(false);\n const lastEffectSeqRef = useRef<number>(0);\n const lastIndexRef = useRef<number>(-1);\n const [infoVisible, setInfoVisible] = useState(false);\n const [infoShownAt, setInfoShownAt] = useState<number | null>(null);\n const [imeBlocked, setImeBlocked] = useState(false);\n\n useEffect(() => {\n if (infoShownAt === null) return;\n const id = setTimeout(() => setInfoVisible(false), 2000);\n return () => clearTimeout(id);\n }, [infoShownAt]);\n\n const { session, lastEffect, effectSeq, tick } = useWordLoop({\n playlist: loaded.playlist,\n enabled: phase === 'typing',\n onComplete: (s) => {\n if (finishedRef.current) return;\n finishedRef.current = true;\n setPhase('summary');\n Promise.resolve(persist(sessionSummary(s))).catch((err) => {\n console.error('Failed to persist session:', err);\n });\n },\n onEscape: () => setPhase(phase === 'paused' ? 'typing' : 'paused'),\n onTab: stealth\n ? undefined\n : () => {\n const cur = session.current ? loaded.playlist[session.current.wordIndex] : undefined;\n if (cur) void audio.pronounce(cur.name);\n },\n onImeBlock: () => setImeBlocked(true),\n onValidInput: () => setImeBlocked(false),\n });\n\n useEffect(() => {\n if (stealth) return;\n if (effectSeq === lastEffectSeqRef.current) return;\n lastEffectSeqRef.current = effectSeq;\n if (lastEffect === null) return;\n if (lastEffect === 'wrong' && cfg.sounds.feedback) audio.wrong();\n if (lastEffect === 'progress' && cfg.sounds.keystroke) audio.keystroke();\n if (lastEffect === 'correct') {\n if (cfg.sounds.feedback) audio.correct();\n if (cfg.sounds.keystroke) audio.keystroke();\n }\n }, [stealth, effectSeq, lastEffect, audio, cfg.sounds.feedback, cfg.sounds.keystroke]);\n\n useEffect(() => {\n if (stealth) return;\n const idx = session.current?.wordIndex ?? -1;\n if (idx === -1) return;\n if (idx === lastIndexRef.current) return;\n lastIndexRef.current = idx;\n const cur = loaded.playlist[idx];\n const next = loaded.playlist[idx + 1];\n if (cur && cfg.autoplayPronunciation) audio.pronounce(cur.name);\n if (next) audio.prefetch(next.name);\n }, [stealth, session.current?.wordIndex, audio, cfg.autoplayPronunciation, loaded.playlist]);\n\n void tick;\n\n useInput(\n (_input, key) => {\n // Node's readline normalizes byte 0x09 (Ctrl+I) to {name:'tab', ctrl:false},\n // so key.ctrl && input === 'i' would never match. Tab and Ctrl+I both arrive\n // here as key.tab — bind to that. In stealth mode Tab has no other use\n // (onTab is disabled below), so this is non-conflicting.\n if (key.tab) {\n setInfoVisible(true);\n setInfoShownAt(Date.now());\n return;\n }\n },\n { isActive: stealth && phase === 'typing' },\n );\n\n useInput(\n (_input, key) => {\n if (key.return) {\n setPhase('typing');\n return;\n }\n if (key.escape) {\n goBack();\n return;\n }\n },\n { isActive: phase === 'paused' },\n );\n\n // Stealth + paused only: Ctrl+C exits silently — erase the 3 inline rows\n // (handled in practice.impl.ts after waitUntilExit) and skip the session\n // report. Normal Ctrl+C from typing keeps the 3 rows in scrollback.\n useInput(\n (input, key) => {\n if (key.ctrl && input === 'c') {\n setSilentExit(true);\n exit();\n }\n },\n { isActive: stealth && phase === 'paused' },\n );\n\n useInput(\n (input, key) => {\n if (key.escape) {\n goBack();\n return;\n }\n if (key.return) {\n const nextIdx = chapterIndex + 1;\n if (mode === 'loop') {\n nav.replace({\n name: 'practice',\n params: { dictId, chapterIndex, mode, stealth: params.stealth },\n });\n } else if (mode === 'review' || nextIdx >= loaded.totalChapters) {\n goBack();\n } else {\n nav.replace({\n name: 'practice',\n params: { dictId, chapterIndex: nextIdx, mode, stealth: params.stealth },\n });\n }\n return;\n }\n if (input === 'm') {\n nav.replace({\n name: 'practice',\n params: { dictId, chapterIndex: 0, mode: 'review', stealth: params.stealth },\n });\n return;\n }\n },\n { isActive: phase === 'summary' },\n );\n\n const completed = session.results.length;\n const errors = session.results.reduce((a, r) => a + r.errors, 0);\n const elapsedMs = Date.now() - session.startedAt;\n const minutes = elapsedMs / 60000;\n const wpm = minutes > 0 ? Math.round((completed / minutes) * 10) / 10 : 0;\n\n const summary = phase === 'summary' ? sessionSummary(session) : null;\n\n if (stealth) {\n if (phase === 'paused') return <StealthPaused />;\n if (phase === 'summary' && summary) {\n const sMinutes = summary.durationMs / 60000;\n const sWpm = sMinutes > 0 ? Math.round((summary.wordCount / sMinutes) * 10) / 10 : 0;\n const sErrWords = Object.keys(summary.perWordErrors).length;\n const sAcc =\n summary.wordCount === 0 ? 1 : Math.max(0, (summary.wordCount - sErrWords) / summary.wordCount);\n const sAccPct = Math.round(sAcc * 1000) / 10;\n return (\n <StealthSummary\n wordCount={summary.wordCount}\n errors={summary.errors}\n durationMs={summary.durationMs}\n wpm={sWpm}\n accPct={sAccPct}\n />\n );\n }\n const currentWord = session.current\n ? loaded.playlist[session.current.wordIndex]\n : loaded.playlist[loaded.playlist.length - 1];\n const inputState = session.current?.input ?? { target: '', typed: '', errorsThisWord: 0 };\n const errWords = new Set(\n session.results.filter((r) => r.errors > 0).map((r) => r.word),\n ).size;\n const accFrac =\n completed === 0 ? 1 : Math.max(0, (completed - errWords) / completed);\n const accPct = Math.round(accFrac * 1000) / 10;\n const chapterLabel =\n mode === 'review'\n ? 'review'\n : `ch ${chapterIndex + 1}/${loaded.totalChapters}`;\n return (\n <StealthTyping\n target={currentWord?.name ?? ''}\n typed={inputState.typed}\n hideTarget={mode === 'dictation'}\n phonetic={pickPhonetic(currentWord, cfg.accent)}\n translation={currentWord?.trans ?? []}\n error={lastEffect === 'wrong'}\n imeBlocked={imeBlocked}\n audioWarning={audioWarn}\n info={{\n visible: infoVisible,\n dictName: truncateName(dictName, 24),\n chapterLabel,\n completed,\n total: loaded.playlist.length,\n wpm,\n accPct,\n elapsedMs,\n }}\n />\n );\n }\n\n if (phase === 'paused') {\n return (\n <PausedView\n dictName={dictName}\n chapterIndex={chapterIndex}\n totalChapters={loaded.totalChapters}\n mode={mode}\n completed={completed}\n total={loaded.playlist.length}\n />\n );\n }\n\n if (phase === 'summary' && summary) {\n return (\n <SummaryView\n dictName={dictName}\n chapterIndex={chapterIndex}\n totalChapters={loaded.totalChapters}\n mode={mode}\n summary={summary}\n />\n );\n }\n\n const currentWord = session.current\n ? loaded.playlist[session.current.wordIndex]\n : loaded.playlist[loaded.playlist.length - 1];\n const inputState = session.current?.input ?? { target: '', typed: '', errorsThisWord: 0 };\n\n return (\n <TypingLayout\n dictName={dictName}\n chapterIndex={chapterIndex}\n totalChapters={loaded.totalChapters}\n mode={mode}\n accent={cfg.accent}\n completed={completed}\n total={loaded.playlist.length}\n errors={errors}\n wpm={wpm}\n elapsedMs={elapsedMs}\n target={currentWord?.name ?? ''}\n typed={inputState.typed}\n flashError={lastEffect === 'wrong'}\n hideTarget={mode === 'dictation'}\n phonetic={pickPhonetic(currentWord, cfg.accent)}\n translation={currentWord?.trans ?? []}\n imeBlocked={imeBlocked}\n audioWarning={audioWarn}\n />\n );\n}\n\nfunction pickPhonetic(word: Word | undefined, accent: 'us' | 'uk'): string | null {\n if (!word) return null;\n const p = accent === 'us' ? word.usphone : word.ukphone;\n return p ?? null;\n}\n\nfunction fmtTime(ms: number): string {\n const total = Math.floor(ms / 1000);\n const m = Math.floor(total / 60);\n const s = total % 60;\n return `${m}:${String(s).padStart(2, '0')}`;\n}\n\nfunction TypingLayout(props: {\n dictName: string;\n chapterIndex: number;\n totalChapters: number;\n mode: Mode;\n accent: 'us' | 'uk';\n completed: number;\n total: number;\n errors: number;\n wpm: number;\n elapsedMs: number;\n target: string;\n typed: string;\n flashError: boolean;\n hideTarget: boolean;\n phonetic: string | null;\n translation: string[];\n imeBlocked: boolean;\n audioWarning: string | null;\n}) {\n const t = useStrings();\n const progressFrac = props.total === 0 ? 0 : props.completed / props.total;\n return (\n <Box flexDirection=\"column\" paddingX={2} paddingY={1} width=\"100%\" height=\"100%\">\n <StatusBar\n dictName={props.dictName}\n chapterIndex={props.chapterIndex}\n totalChapters={props.totalChapters}\n mode={props.mode}\n accent={props.accent}\n completed={props.completed}\n total={props.total}\n elapsedMs={props.elapsedMs}\n />\n\n <Box flexGrow={1} flexDirection=\"column\" alignItems=\"center\" justifyContent=\"center\">\n <BigWord\n target={props.target}\n typed={props.typed}\n error={props.flashError}\n hideTarget={props.hideTarget}\n />\n\n {props.phonetic && (\n <Box marginTop={1}>\n <Text italic dimColor color={PALETTE.muted}>\n {props.phonetic}\n </Text>\n </Box>\n )}\n\n {props.translation.length > 0 && (\n <Box marginTop={1} flexDirection=\"column\" alignItems=\"center\">\n {props.translation.slice(0, 2).map((tr, i) => (\n <Text key={i} color={PALETTE.primary}>\n {tr}\n </Text>\n ))}\n </Box>\n )}\n\n {props.imeBlocked && (\n <Box marginTop={1}>\n <Text color={PALETTE.warning}>{t.practice.imeWarning}</Text>\n </Box>\n )}\n\n {!props.imeBlocked && props.audioWarning && (\n <Box marginTop={1}>\n <Text color={PALETTE.warning}>{props.audioWarning}</Text>\n </Box>\n )}\n </Box>\n\n <Box flexDirection=\"column\">\n <ProgressBar frac={progressFrac} />\n <Box justifyContent=\"center\" marginTop={1}>\n <Text color={PALETTE.muted}>\n {props.completed}/{props.total} · {fmtTime(props.elapsedMs)} · {props.wpm} {t.practice.statCards.wpm} · {props.errors} {t.practice.statCards.errors}\n </Text>\n </Box>\n <Box justifyContent=\"center\" marginTop={1}>\n <Text color={PALETTE.muted}>{t.practice.footers.typing}</Text>\n </Box>\n </Box>\n </Box>\n );\n}\n\nfunction StatusBar(props: {\n dictName: string;\n chapterIndex: number;\n totalChapters: number;\n mode: Mode;\n accent: 'us' | 'uk';\n completed: number;\n total: number;\n elapsedMs: number;\n}) {\n const t = useStrings();\n const modeName = t.practice.modes[props.mode];\n const accentName = t.practice.accents[props.accent];\n const name = truncateName(props.dictName, 20);\n const left =\n props.mode === 'review'\n ? `${name} · ${t.practice.reviewLabel} · ${accentName}`\n : `${name} · ${t.practice.chapterLabel(props.chapterIndex + 1, props.totalChapters)} · ${modeName} · ${accentName}`;\n const right = `${props.completed}/${props.total} · ${fmtTime(props.elapsedMs)}`;\n return (\n <Box>\n <Text color={PALETTE.muted}>{left}</Text>\n <Box flexGrow={1} />\n <Text color={PALETTE.muted}>{right}</Text>\n </Box>\n );\n}\n\nfunction ProgressBar({ frac }: { frac: number }) {\n const cols = process.stdout.columns ?? 80;\n const width = Math.max(20, Math.min(72, cols - 16));\n const filled = Math.round(width * Math.max(0, Math.min(1, frac)));\n const empty = width - filled;\n return (\n <Box justifyContent=\"center\">\n <Text color={PALETTE.accent}>{'━'.repeat(filled)}</Text>\n <Text color={PALETTE.muted}>{'─'.repeat(empty)}</Text>\n </Box>\n );\n}\n\nfunction PausedView(props: {\n dictName: string;\n chapterIndex: number;\n totalChapters: number;\n mode: Mode;\n completed: number;\n total: number;\n}) {\n const t = useStrings();\n const frac = props.total === 0 ? 0 : props.completed / props.total;\n const subtitle =\n props.mode === 'review'\n ? `${truncateName(props.dictName, 20)} · ${t.practice.reviewLabel}`\n : `${truncateName(props.dictName, 20)} · ${t.practice.pause.chapter(props.chapterIndex + 1, props.totalChapters)}`;\n return (\n <Box flexDirection=\"column\" alignItems=\"center\" justifyContent=\"center\" width=\"100%\" height=\"100%\">\n <Text bold color={PALETTE.warning}>\n {t.practice.pause.title}\n </Text>\n <Box marginTop={1}>\n <Text color={PALETTE.muted}>{subtitle}</Text>\n </Box>\n <Box marginTop={2}>\n <ProgressBar frac={frac} />\n </Box>\n <Box marginTop={1}>\n <Text color={PALETTE.muted}>{t.practice.pause.progress(props.completed, props.total)}</Text>\n </Box>\n <Box marginTop={2}>\n <Text color={PALETTE.muted}>{t.practice.pause.hint}</Text>\n </Box>\n </Box>\n );\n}\n\nfunction ErrorView({ msg }: { msg: string }) {\n const t = useStrings();\n return (\n <Box flexDirection=\"column\" alignItems=\"center\" justifyContent=\"center\" width=\"100%\" height=\"100%\">\n <Text color={PALETTE.error}>{msg}</Text>\n <Box marginTop={2}>\n <Text color={PALETTE.muted}>Esc {t.common.back}</Text>\n </Box>\n <BackKey />\n </Box>\n );\n}\n\nfunction BackKey() {\n const nav = useNav();\n useInput((_input, key) => {\n if (key.escape) nav.back();\n });\n return null;\n}\n\nfunction Centered({ text, color }: { text: string; color: string }) {\n return (\n <Box alignItems=\"center\" justifyContent=\"center\" width=\"100%\" height=\"100%\">\n <Text color={color}>{text}</Text>\n </Box>\n );\n}\n\nfunction SummaryView(props: {\n dictName: string;\n chapterIndex: number;\n totalChapters: number;\n mode: Mode;\n summary: { wordCount: number; errors: number; durationMs: number; perWordErrors: Record<string, number> };\n}) {\n const { summary } = props;\n const minutes = summary.durationMs / 60000;\n const wpm = minutes > 0 ? Math.round((summary.wordCount / minutes) * 10) / 10 : 0;\n const errorWords = Object.keys(summary.perWordErrors).length;\n const acc = summary.wordCount === 0 ? 1 : Math.max(0, (summary.wordCount - errorWords) / summary.wordCount);\n const accPct = Math.round(acc * 1000) / 10;\n\n const t = useStrings();\n const modeName = t.practice.modes[props.mode];\n const name = truncateName(props.dictName, 20);\n const subtitle =\n props.mode === 'review'\n ? `${name} · ${t.practice.reviewLabel}`\n : `${name} · ${t.practice.chapterLabel(props.chapterIndex + 1, props.totalChapters)} · ${modeName}`;\n\n const nextLabel =\n props.mode === 'loop'\n ? t.practice.summary.loopAgain\n : props.mode === 'review' || props.chapterIndex + 1 >= props.totalChapters\n ? t.practice.summary.backMenu\n : t.practice.summary.nextChapter;\n const footer = `Enter ${nextLabel} · m ${t.practice.summary.reviewMistakes} · Esc ${t.practice.summary.backMenu}`;\n\n return (\n <Box flexDirection=\"column\" alignItems=\"center\" justifyContent=\"center\" paddingY={1} width=\"100%\" height=\"100%\">\n <Text bold color={PALETTE.success}>\n {t.practice.chapterComplete}\n </Text>\n <Box marginTop={1}>\n <Text color={PALETTE.muted}>{subtitle}</Text>\n </Box>\n\n <Box marginTop={3} flexDirection=\"row\" justifyContent=\"center\">\n <StatCard label={t.practice.statCards.words} value={String(summary.wordCount)} color={PALETTE.text} />\n <StatCard\n label={t.practice.statCards.errors}\n value={String(summary.errors)}\n color={summary.errors > 0 ? PALETTE.error : PALETTE.muted}\n />\n <StatCard label={t.practice.statCards.wpm} value={String(wpm)} color={PALETTE.accent} />\n <StatCard label={t.practice.statCards.accuracy} value={`${accPct}%`} color={PALETTE.accent} />\n </Box>\n\n <Box marginTop={2}>\n <Text color={PALETTE.muted}>{t.practice.statCards.elapsed(fmtTime(summary.durationMs))}</Text>\n </Box>\n\n <Box flexGrow={1} />\n\n <Box marginTop={2}>\n <Text color={PALETTE.muted}>{footer}</Text>\n </Box>\n </Box>\n );\n}\n\nfunction StatCard({ label, value, color }: { label: string; value: string; color: string }) {\n return (\n <Box flexDirection=\"column\" alignItems=\"center\" marginX={3}>\n <Text bold color={color}>{value}</Text>\n <Text color={PALETTE.muted}>{label}</Text>\n </Box>\n );\n}\n","export function shuffle<T>(arr: readonly T[], rng: () => number = Math.random): T[] {\n const out = [...arr];\n for (let i = out.length - 1; i > 0; i--) {\n const j = Math.floor(rng() * (i + 1));\n const tmp = out[i]!;\n out[i] = out[j]!;\n out[j] = tmp;\n }\n return out;\n}\n\nexport function mulberry32(seed: number): () => number {\n let t = seed >>> 0;\n return () => {\n t = (t + 0x6d2b79f5) >>> 0;\n let r = Math.imul(t ^ (t >>> 15), 1 | t);\n r = (r + Math.imul(r ^ (r >>> 7), 61 | r)) ^ r;\n return ((r ^ (r >>> 14)) >>> 0) / 4294967296;\n };\n}\n","import type { Word } from './dictionary.js';\nimport { shuffle, mulberry32 } from '../util/shuffle.js';\n\nexport type Mode = 'order' | 'dictation' | 'review' | 'random' | 'loop';\n\nexport function chunkChapters(words: Word[], chapterSize: number): Word[][] {\n if (chapterSize <= 0) throw new Error('chapterSize must be positive');\n const chunks: Word[][] = [];\n for (let i = 0; i < words.length; i += chapterSize) {\n chunks.push(words.slice(i, i + chapterSize));\n }\n return chunks;\n}\n\nexport function chapterCount(totalWords: number, chapterSize: number): number {\n return Math.ceil(totalWords / chapterSize);\n}\n\n/**\n * Build a play list for a chapter, applying the given mode.\n * - order: return as-is\n * - dictation: same as order (hide-the-word behavior is UI-only)\n * - random: single shuffled pass\n * - loop: return as-is; the practice screen drives the repeat\n * - review: caller passes the mistake-book words, we just chunk those\n */\nexport function buildPlaylist(chapter: Word[], mode: Mode, seed?: number): Word[] {\n if (mode === 'random') {\n const rng = seed === undefined ? Math.random : mulberry32(seed);\n return shuffle(chapter, rng);\n }\n return chapter;\n}\n","export type InputState = {\n target: string;\n typed: string;\n errorsThisWord: number;\n};\n\nexport type InputEvent =\n | { type: 'char'; ch: string }\n | { type: 'backspace' }\n | { type: 'reset' };\n\nexport type InputEffect = 'none' | 'progress' | 'wrong' | 'correct' | 'skipped';\n\nexport function initialState(target: string): InputState {\n return { target, typed: '', errorsThisWord: 0 };\n}\n\nexport function reduce(state: InputState, ev: InputEvent): { state: InputState; effect: InputEffect } {\n switch (ev.type) {\n case 'reset':\n return { state: { ...state, typed: '' }, effect: 'none' };\n case 'backspace': {\n if (state.typed.length === 0) return { state, effect: 'none' };\n return { state: { ...state, typed: state.typed.slice(0, -1) }, effect: 'none' };\n }\n case 'char': {\n const candidate = state.typed + ev.ch;\n // Compare by code-point index, not byte length, to handle unicode safely.\n const targetUpToCandidate = [...state.target].slice(0, [...candidate].length).join('');\n if (candidate === targetUpToCandidate) {\n if (candidate.length === state.target.length) {\n return { state: { ...state, typed: candidate }, effect: 'correct' };\n }\n return { state: { ...state, typed: candidate }, effect: 'progress' };\n }\n return {\n state: { ...state, typed: '', errorsThisWord: state.errorsThisWord + 1 },\n effect: 'wrong',\n };\n }\n }\n}\n","import type { Word } from './dictionary.js';\nimport { initialState, reduce, type InputEvent, type InputState, type InputEffect } from './input-buffer.js';\n\nexport type SessionWordResult = { word: string; errors: number; durationMs: number; skipped?: boolean };\n\nexport type Session = {\n startedAt: number;\n results: SessionWordResult[];\n current: { wordIndex: number; wordStartedAt: number; input: InputState } | null;\n finishedAt: number | null;\n playlist: Word[];\n};\n\nexport function startSession(playlist: Word[], now = Date.now()): Session {\n if (playlist.length === 0) {\n return { startedAt: now, results: [], current: null, finishedAt: now, playlist };\n }\n return {\n startedAt: now,\n results: [],\n current: { wordIndex: 0, wordStartedAt: now, input: initialState(playlist[0]!.name) },\n finishedAt: null,\n playlist,\n };\n}\n\nexport function feedSession(session: Session, ev: InputEvent, now = Date.now()): { session: Session; effect: InputEffect } {\n if (!session.current) return { session, effect: 'none' };\n const { state, effect } = reduce(session.current.input, ev);\n if (effect === 'correct') {\n const finished: SessionWordResult = {\n word: state.target,\n errors: state.errorsThisWord,\n durationMs: now - session.current.wordStartedAt,\n };\n const nextIndex = session.current.wordIndex + 1;\n const results = [...session.results, finished];\n if (nextIndex >= session.playlist.length) {\n return {\n session: { ...session, results, current: null, finishedAt: now },\n effect,\n };\n }\n return {\n session: {\n ...session,\n results,\n current: {\n wordIndex: nextIndex,\n wordStartedAt: now,\n input: initialState(session.playlist[nextIndex]!.name),\n },\n },\n effect,\n };\n }\n return {\n session: {\n ...session,\n current: { ...session.current, input: state },\n },\n effect,\n };\n}\n\nexport function skipSession(session: Session, now = Date.now()): { session: Session; effect: InputEffect } {\n if (!session.current) return { session, effect: 'none' };\n const result: SessionWordResult = {\n word: session.current.input.target,\n errors: 0,\n durationMs: now - session.current.wordStartedAt,\n skipped: true,\n };\n const nextIndex = session.current.wordIndex + 1;\n const results = [...session.results, result];\n if (nextIndex >= session.playlist.length) {\n return {\n session: { ...session, results, current: null, finishedAt: now },\n effect: 'skipped',\n };\n }\n return {\n session: {\n ...session,\n results,\n current: {\n wordIndex: nextIndex,\n wordStartedAt: now,\n input: initialState(session.playlist[nextIndex]!.name),\n },\n },\n effect: 'skipped',\n };\n}\n\nexport function sessionSummary(session: Session): {\n wordCount: number;\n errors: number;\n durationMs: number;\n perWordErrors: Record<string, number>;\n} {\n const errors = session.results.reduce((a, r) => a + r.errors, 0);\n const durationMs =\n (session.finishedAt ?? Date.now()) - session.startedAt;\n const perWordErrors: Record<string, number> = {};\n for (const r of session.results) {\n if (r.errors > 0) perWordErrors[r.word] = (perWordErrors[r.word] ?? 0) + r.errors;\n }\n return { wordCount: session.results.length, errors, durationMs, perWordErrors };\n}\n","import { useEffect, useReducer, useRef, useState } from 'react';\nimport { useInput, useApp } from 'ink';\nimport type { Word } from '../../domain/dictionary.js';\nimport { startSession, feedSession, skipSession, type Session } from '../../domain/session.js';\nimport type { InputEffect } from '../../domain/input-buffer.js';\n\ntype Action =\n | { type: 'event'; input: string; key: { backspace?: boolean; delete?: boolean; tab?: boolean; escape?: boolean; return?: boolean; ctrl?: boolean }; now: number }\n | { type: 'start'; playlist: Word[]; now: number }\n | { type: 'skip'; now: number };\n\n// effectSeq is a monotonic counter bumped on every dispatch that resolves into\n// a new lastEffect value. Consumers (PracticeScreen audio effect) depend on it\n// instead of lastEffect alone because React's Object.is comparison treats two\n// consecutive 'progress' strings as equal — without a counter, useEffect would\n// fire on only the first char of a word, dropping the keystroke sound on every\n// subsequent character.\ntype LoopState = { session: Session; lastEffect: InputEffect | null; effectSeq: number };\n\nfunction reducer(state: LoopState, action: Action): LoopState {\n if (action.type === 'start') {\n return { session: startSession(action.playlist, action.now), lastEffect: null, effectSeq: 0 };\n }\n if (action.type === 'skip') {\n const r = skipSession(state.session, action.now);\n return { session: r.session, lastEffect: r.effect, effectSeq: state.effectSeq + 1 };\n }\n if (action.type === 'event') {\n if (action.key.backspace || action.key.delete) {\n const r = feedSession(state.session, { type: 'backspace' }, action.now);\n return { session: r.session, lastEffect: r.effect, effectSeq: state.effectSeq + 1 };\n }\n if (action.input.length === 0) return state;\n let session = state.session;\n let lastEffect: InputEffect | null = state.lastEffect;\n for (const c of action.input) {\n const r = feedSession(session, { type: 'char', ch: c }, action.now);\n session = r.session;\n lastEffect = r.effect;\n if (session.finishedAt !== null) break;\n }\n return { session, lastEffect, effectSeq: state.effectSeq + 1 };\n }\n return state;\n}\n\n// Classify a typed batch from Ink's useInput. Single source of truth so the\n// IME-detection logic stays unit-testable without rendering Ink.\n// kind='ime' → contains at least one non-ASCII codepoint (>= 0x80, e.g.\n// CJK / Latin-1 supplement); reject the batch and show hint.\n// kind='valid' → all-ASCII printable; `cleaned` has the dispatchable subset.\n// kind='noise' → ASCII control (DEL, NUL, etc.) or empty; ignore silently.\nexport type InputBatchKind = 'ime' | 'valid' | 'noise';\nexport function classifyInputBatch(input: string): { kind: InputBatchKind; cleaned: string } {\n const chars = [...input];\n if (chars.some((c) => c.codePointAt(0)! >= 0x80)) {\n return { kind: 'ime', cleaned: '' };\n }\n const cleaned = chars\n .filter((c) => {\n const cp = c.codePointAt(0)!;\n return cp >= 0x20 && cp <= 0x7e;\n })\n .join('');\n return { kind: cleaned.length > 0 ? 'valid' : 'noise', cleaned };\n}\n\nexport type UseWordLoopOpts = {\n playlist: Word[];\n onComplete: (session: Session) => void;\n onTab?: () => void;\n onEscape?: () => void;\n onSkip?: () => void;\n // Fires when the typed batch contains any codepoint > 0x7E — almost always\n // means the user's IME is active (CJK input method). The batch is rejected\n // entirely so the wrong characters aren't fed to the session.\n onImeBlock?: () => void;\n // Fires when a non-empty all-ASCII batch is about to dispatch. Used to\n // clear a previously-set IME warning so it auto-dismisses on the first\n // successful keystroke.\n onValidInput?: () => void;\n enabled?: boolean;\n};\n\nexport function useWordLoop({\n playlist,\n onComplete,\n onTab,\n onEscape,\n onSkip,\n onImeBlock,\n onValidInput,\n enabled = true,\n}: UseWordLoopOpts) {\n const [state, dispatch] = useReducer(reducer, undefined, () => ({\n session: startSession(playlist, Date.now()),\n lastEffect: null as InputEffect | null,\n effectSeq: 0,\n }));\n const completedRef = useRef(false);\n const [tick, setTick] = useState(0);\n const { exit } = useApp();\n\n useInput(\n (input, key) => {\n if (key.ctrl && input === 'c') {\n exit();\n return;\n }\n if (key.ctrl && input === 'n') {\n onSkip?.();\n dispatch({ type: 'skip', now: Date.now() });\n return;\n }\n if (key.escape) {\n onEscape?.();\n return;\n }\n if (key.tab) {\n onTab?.();\n return;\n }\n if (key.upArrow || key.downArrow || key.leftArrow || key.rightArrow || key.return) return;\n if (key.ctrl || key.meta) return;\n const { kind, cleaned } = classifyInputBatch(input);\n if (kind === 'ime') {\n onImeBlock?.();\n return;\n }\n if (kind === 'noise') return;\n onValidInput?.();\n dispatch({ type: 'event', input: cleaned, key, now: Date.now() });\n },\n { isActive: enabled },\n );\n\n useEffect(() => {\n if (state.session.finishedAt !== null && !completedRef.current) {\n completedRef.current = true;\n onComplete(state.session);\n }\n }, [state.session, onComplete]);\n\n useEffect(() => {\n if (state.session.finishedAt !== null) return;\n const id = setInterval(() => setTick((t) => t + 1), 1000);\n return () => clearInterval(id);\n }, [state.session.finishedAt]);\n\n return { session: state.session, lastEffect: state.lastEffect, effectSeq: state.effectSeq, tick };\n}\n\n// Exposed for unit tests only — verifies effectSeq monotonic increment and\n// effect dispatch behavior without mounting a full Ink tree.\nexport const __test = { reducer };\n","import { useEffect, useRef } from 'react';\nimport {\n initAudio,\n playCorrect,\n playWrong,\n playKeystroke,\n playPronunciation,\n prefetchPronunciation,\n} from '../../infra/audio.js';\n\ntype Opts = {\n enabled: boolean;\n accent: 'us' | 'uk';\n autoplayPronunciation: boolean;\n pronunciationRate: number;\n pronunciationSource: 'youdao' | 'dictapi';\n};\n\nexport type AudioControls = {\n keystroke: () => void;\n correct: () => void;\n wrong: () => void;\n pronounce: (word: string) => void;\n prefetch: (word: string) => void;\n};\n\nexport function useAudio(opts: Opts): AudioControls {\n const initedRef = useRef(false);\n useEffect(() => {\n if (initedRef.current) return;\n initedRef.current = true;\n initAudio(!opts.enabled).catch(() => undefined);\n }, [opts.enabled]);\n\n return {\n keystroke: () => opts.enabled && playKeystroke(),\n correct: () => opts.enabled && playCorrect(),\n wrong: () => opts.enabled && playWrong(),\n pronounce: (word) => {\n if (!opts.enabled) return;\n if (opts.autoplayPronunciation)\n void playPronunciation(word, opts.accent, opts.pronunciationRate, opts.pronunciationSource);\n },\n prefetch: (word) => {\n if (!opts.enabled) return;\n void prefetchPronunciation(word, opts.accent, opts.pronunciationSource);\n },\n };\n}\n","import { useCallback } from 'react';\nimport { appendSession, type SessionRecord } from '../../domain/stats.js';\nimport { loadMistakes, saveMistakes, bump } from '../../domain/mistakes.js';\nimport { addChapter as trackChapter } from '../../infra/session-tracker.js';\nimport type { Mode } from '../../domain/chapters.js';\n\ntype Summary = {\n wordCount: number;\n errors: number;\n durationMs: number;\n perWordErrors: Record<string, number>;\n};\n\nexport function useSessionPersistence(meta: { dictId: string; chapterIndex: number; mode: Mode }) {\n return useCallback(\n async (summary: Summary): Promise<void> => {\n const rec: SessionRecord = {\n ts: new Date().toISOString(),\n dictId: meta.dictId,\n chapter: meta.chapterIndex,\n mode: meta.mode,\n wordCount: summary.wordCount,\n errors: summary.errors,\n durationMs: summary.durationMs,\n perWordErrors: summary.perWordErrors,\n };\n await appendSession(rec);\n trackChapter({\n dictId: meta.dictId,\n chapterIndex: meta.chapterIndex,\n mode: meta.mode,\n wordCount: summary.wordCount,\n errors: summary.errors,\n durationMs: summary.durationMs,\n perWordErrors: summary.perWordErrors,\n });\n const dirty = Object.entries(summary.perWordErrors).filter(([, n]) => n > 0);\n if (dirty.length === 0) return;\n let book = await loadMistakes();\n for (const [word, n] of dirty) book = bump(book, word, meta.dictId, n);\n await saveMistakes(book);\n },\n [meta.dictId, meta.chapterIndex, meta.mode],\n );\n}\n","import type { ReactNode } from 'react';\nimport { Box, Text, useStdout } from 'ink';\nimport { PALETTE } from '../components/BigWord.js';\nimport { useStrings } from '../../i18n/context.js';\n\nconst RIGHT_WIDTH = 28;\n\nfunction fmtTime(ms: number): string {\n const total = Math.floor(ms / 1000);\n const m = Math.floor(total / 60);\n const s = total % 60;\n return `${m}:${String(s).padStart(2, '0')}`;\n}\n\nfunction useLeftWidth(): number {\n const { stdout } = useStdout();\n const cols = stdout?.columns ?? 80;\n return Math.max(20, cols - RIGHT_WIDTH);\n}\n\nfunction Row({ left, right }: { left: ReactNode; right: ReactNode }) {\n const leftWidth = useLeftWidth();\n return (\n <Box>\n <Box width={leftWidth}>{left}</Box>\n <Box width={RIGHT_WIDTH} justifyContent=\"flex-end\">\n {right}\n </Box>\n </Box>\n );\n}\n\nexport function StealthTyping(props: {\n target: string;\n typed: string;\n hideTarget: boolean;\n phonetic: string | null;\n translation: string[];\n error: boolean;\n imeBlocked: boolean;\n audioWarning: string | null;\n info: {\n visible: boolean;\n dictName: string;\n chapterLabel: string;\n completed: number;\n total: number;\n wpm: number;\n accPct: number;\n elapsedMs: number;\n };\n}) {\n const t = useStrings();\n const target = [...props.target];\n const typed = [...props.typed];\n\n // Row 1: word chars + inline phonetic (two-space gap, italic + dim muted).\n // error=true flashes the entire word red; phonetic remains dim regardless.\n const wordCell = (\n <Box>\n {target.map((ch, i) => {\n const isTyped = i < typed.length;\n const display = props.hideTarget && !isTyped ? '_' : isTyped ? typed[i]! : ch;\n const color = props.error ? PALETTE.error : isTyped ? PALETTE.accent : PALETTE.muted;\n return (\n <Text key={i} bold color={color}>\n {display}\n </Text>\n );\n })}\n {props.phonetic && (\n <>\n <Text> </Text>\n <Text italic dimColor color={PALETTE.muted}>\n {props.phonetic}\n </Text>\n </>\n )}\n </Box>\n );\n\n // Row 3: first translation in primary cyan (matches TypingLayout translation color)\n const translationCell = props.translation.length > 0 ? (\n <Text color={PALETTE.primary}>{props.translation[0]!}</Text>\n ) : (\n <Text> </Text>\n );\n\n const info = props.info;\n const accFmt = Number.isInteger(info.accPct) ? `${info.accPct}` : info.accPct.toFixed(1);\n\n // Right column priority: imeBlocked > audioWarning > info > idle.\n // imeBlocked: warning indicator on row 1, rows 2/3 blank.\n // audioWarning (and no IME issue): `! audio` short marker on row 1, rows 2/3 blank.\n // info.visible: dict / progress / time on rows 1/2/3.\n // idle: all three rows blank.\n const showAudio = !props.imeBlocked && props.audioWarning !== null;\n const right1 = props.imeBlocked ? (\n <Text color={PALETTE.warning}>{t.practice.imeWarningShort}</Text>\n ) : showAudio ? (\n <Text color={PALETTE.warning}>{t.practice.audioWarningShort}</Text>\n ) : info.visible ? (\n <Text color={PALETTE.muted}>{`${info.dictName} · ${info.chapterLabel}`}</Text>\n ) : (\n <Text> </Text>\n );\n const right2 = props.imeBlocked || showAudio ? (\n <Text> </Text>\n ) : info.visible ? (\n <Text color={PALETTE.muted}>{`${info.completed}/${info.total} · ${info.wpm}wpm · ${accFmt}%`}</Text>\n ) : (\n <Text> </Text>\n );\n const right3 = props.imeBlocked || showAudio ? (\n <Text> </Text>\n ) : info.visible ? (\n <Text color={PALETTE.muted}>{fmtTime(info.elapsedMs)}</Text>\n ) : (\n <Text> </Text>\n );\n\n // Layout: 3 rendered rows. Middle row's left is blank → acts as a 1-line\n // visual gap between word+phonetic and translation. Right column keeps all\n // three info slots (dict / progress / time) when info.visible is true.\n return (\n <Box flexDirection=\"column\">\n <Row left={wordCell} right={right1} />\n <Row left={<Text> </Text>} right={right2} />\n <Row left={translationCell} right={right3} />\n </Box>\n );\n}\n\nexport function StealthPaused() {\n const t = useStrings();\n return (\n <Box flexDirection=\"column\">\n <Row\n left={<Text color={PALETTE.warning}>{t.stealth.paused}</Text>}\n right={<Text color={PALETTE.muted}>{t.stealth.pausedHintRight}</Text>}\n />\n <Row left={<Text> </Text>} right={<Text color={PALETTE.muted}>Esc {t.common.back}</Text>} />\n <Row left={<Text> </Text>} right={<Text> </Text>} />\n </Box>\n );\n}\n\nexport function StealthSummary(props: {\n wordCount: number;\n errors: number;\n durationMs: number;\n wpm: number;\n accPct: number;\n}) {\n const t = useStrings();\n const accFmt = Number.isInteger(props.accPct) ? `${props.accPct}` : props.accPct.toFixed(1);\n const line = `${t.stealth.chapterDone} · ${props.wordCount}w · ${props.wpm}wpm · ${accFmt}% · ${fmtTime(props.durationMs)}`;\n return (\n <Box flexDirection=\"column\">\n <Row\n left={<Text color={PALETTE.success}>{line}</Text>}\n right={<Text color={PALETTE.muted}>{t.stealth.nextHintRight}</Text>}\n />\n <Row left={<Text> </Text>} right={<Text color={PALETTE.muted}>Esc {t.common.back}</Text>} />\n <Row left={<Text> </Text>} right={<Text> </Text>} />\n </Box>\n );\n}\n"],"mappings":"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","names":["useState","useEffect","useRef","Box","Text","useApp","useInput","shuffle","arr","rng","out","i","j","tmp","mulberry32","seed","t","chunkChapters","words","chapterSize","chunks","i","buildPlaylist","chapter","mode","seed","rng","mulberry32","shuffle","initialState","target","reduce","state","ev","candidate","targetUpToCandidate","startSession","playlist","now","initialState","feedSession","session","ev","state","effect","reduce","finished","nextIndex","results","skipSession","result","sessionSummary","errors","a","r","durationMs","perWordErrors","useEffect","useReducer","useRef","useState","useInput","useApp","reducer","state","action","startSession","skipSession","r","feedSession","session","lastEffect","classifyInputBatch","input","chars","c","cleaned","cp","useWordLoop","playlist","onComplete","onTab","onEscape","onSkip","onImeBlock","onValidInput","enabled","dispatch","useReducer","completedRef","useRef","tick","setTick","useState","exit","useApp","useInput","key","kind","useEffect","id","t","useEffect","useRef","useAudio","opts","initedRef","useRef","useEffect","initAudio","playKeystroke","playCorrect","playWrong","word","playPronunciation","prefetchPronunciation","useCallback","useSessionPersistence","meta","useCallback","summary","rec","appendSession","addChapter","dirty","n","book","loadMistakes","word","bump","saveMistakes","Box","Text","useStdout","Fragment","jsx","jsxs","RIGHT_WIDTH","fmtTime","ms","total","m","s","useLeftWidth","stdout","useStdout","cols","Row","left","right","leftWidth","Box","StealthTyping","props","t","useStrings","target","typed","wordCell","ch","i","isTyped","display","color","PALETTE","Text","translationCell","info","accFmt","showAudio","right1","right2","right3","StealthPaused","StealthSummary","line","jsx","jsxs","PracticeScreen","params","dictId","chapterIndex","mode","cfg","useAppState","t","useStrings","phase","setPhase","useState","loaded","setLoaded","errorMsg","setErrorMsg","useEffect","cancelled","words","ensureDictionary","book","loadMistakes","reviewWords","w","chapters","chunkChapters","idx","playlist","buildPlaylist","err","Centered","PALETTE","ErrorView","PracticeRunner","stealth","nav","useNav","exit","useApp","goBack","persist","useSessionPersistence","dictName","useDictName","audio","useAudio","audioStatus","useAudioStatus","audioWarn","finishedRef","useRef","lastEffectSeqRef","lastIndexRef","infoVisible","setInfoVisible","infoShownAt","setInfoShownAt","imeBlocked","setImeBlocked","id","session","lastEffect","effectSeq","tick","useWordLoop","s","sessionSummary","cur","next","useInput","_input","key","input","setSilentExit","nextIdx","completed","errors","a","r","elapsedMs","minutes","wpm","summary","StealthPaused","sMinutes","sWpm","sErrWords","sAcc","sAccPct","StealthSummary","currentWord","inputState","errWords","accFrac","accPct","chapterLabel","StealthTyping","pickPhonetic","truncateName","PausedView","SummaryView","TypingLayout","word","accent","fmtTime","ms","total","m","props","progressFrac","Box","StatusBar","BigWord","Text","tr","i","ProgressBar","modeName","accentName","name","left","right","frac","cols","width","filled","empty","subtitle","msg","BackKey","text","color","errorWords","acc","footer","StatCard","label","value"]}
|
package/dist/chunk-7LTZGB7F.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{a as i,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),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,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")}),a=null;function k(){return a||(a=d.parse({}),a)}async function j(){let e=await g(i.config);if(!e)return k();let o=d.safeParse(e);if(!o.success)throw new Error(`Invalid config at ${i.config}: ${o.error.message}`);return o.data}async function x(e){await m(i.config,e)}function $(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 P(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]=b(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 b(e){return e==="true"?!0:e==="false"?!1:e==="null"?null:/^-?\d+$/.test(e)||/^-?\d+\.\d+$/.test(e)?Number(e):e}export{j as a,x as b,$ as c,P as d};
|
|
2
|
-
//# sourceMappingURL=chunk-7LTZGB7F.js.map
|
|
@@ -1 +0,0 @@
|
|
|
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 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 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"]}
|