qwerty-cli 0.0.1-alpha.19 → 0.0.1-alpha.21

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.
Files changed (36) hide show
  1. package/dist/ConfigEditor-YG5K2WT2.js +2 -0
  2. package/dist/{ConfigEditor-YYZQ7TUJ.js.map → ConfigEditor-YG5K2WT2.js.map} +1 -1
  3. package/dist/PracticeScreen-BRPEUQJ6.js +2 -0
  4. package/dist/PracticeScreen-BRPEUQJ6.js.map +1 -0
  5. package/dist/StatsViewer-43ATH3OG.js +2 -0
  6. package/dist/{StatsViewer-JIFWFPDL.js.map → StatsViewer-43ATH3OG.js.map} +1 -1
  7. package/dist/WordLookup-LQMQVORJ.js +2 -0
  8. package/dist/WordLookup-LQMQVORJ.js.map +1 -0
  9. package/dist/chunk-MHLVBOJU.js +3 -0
  10. package/dist/chunk-MHLVBOJU.js.map +1 -0
  11. package/dist/chunk-UEJCQKZ2.js +2 -0
  12. package/dist/chunk-UEJCQKZ2.js.map +1 -0
  13. package/dist/chunk-WRO5XX35.js +2 -0
  14. package/dist/chunk-WRO5XX35.js.map +1 -0
  15. package/dist/cli.js +1 -1
  16. package/dist/menu.impl-6DPHV36B.js +2 -0
  17. package/dist/{menu.impl-ZUAMXKFD.js.map → menu.impl-6DPHV36B.js.map} +1 -1
  18. package/dist/practice.impl-HILWI6GI.js +2 -0
  19. package/dist/{practice.impl-2PPCBBK3.js.map → practice.impl-HILWI6GI.js.map} +1 -1
  20. package/dist/word.impl-VVSIGCKS.js +2 -0
  21. package/dist/word.impl-VVSIGCKS.js.map +1 -0
  22. package/package.json +1 -1
  23. package/dist/ConfigEditor-YYZQ7TUJ.js +0 -2
  24. package/dist/PracticeScreen-PXPBME6H.js +0 -2
  25. package/dist/PracticeScreen-PXPBME6H.js.map +0 -1
  26. package/dist/StatsViewer-JIFWFPDL.js +0 -2
  27. package/dist/WordLookup-6JFXQYSQ.js +0 -2
  28. package/dist/WordLookup-6JFXQYSQ.js.map +0 -1
  29. package/dist/chunk-QG7ZTS2G.js +0 -2
  30. package/dist/chunk-QG7ZTS2G.js.map +0 -1
  31. package/dist/chunk-RR77GWZ3.js +0 -3
  32. package/dist/chunk-RR77GWZ3.js.map +0 -1
  33. package/dist/menu.impl-ZUAMXKFD.js +0 -2
  34. package/dist/practice.impl-2PPCBBK3.js +0 -2
  35. package/dist/word.impl-EJNGBD7U.js +0 -2
  36. package/dist/word.impl-EJNGBD7U.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/util/altscreen.ts","../src/ui/App.tsx","../src/ui/Fullscreen.tsx","../src/ui/screens/MainMenu.tsx","../src/util/report.ts"],"sourcesContent":["// Shared alt-screen state. Both the command entry point (which writes\n// ENTER before render() to avoid Ink's first frame leaking onto the main\n// screen) and Fullscreen.tsx (which used to write ENTER inside useEffect)\n// route through this module so enter/leave are idempotent.\n\nconst ENTER = '\\x1b[?1049h\\x1b[?25l\\x1b[2J\\x1b[H';\nconst LEAVE = '\\x1b[?25h\\x1b[?1049l';\n\nlet active = false;\nlet signalsRegistered = false;\n\nfunction ensureSignals(): void {\n if (signalsRegistered) return;\n signalsRegistered = true;\n const cleanup = () => {\n if (!active) return;\n try {\n process.stdout.write(LEAVE);\n } catch {\n // stream may be closed\n }\n active = false;\n };\n process.once('exit', cleanup);\n process.once('SIGINT', () => {\n cleanup();\n process.exit(130);\n });\n process.once('SIGTERM', () => {\n cleanup();\n process.exit(143);\n });\n}\n\nexport function enterAltScreen(): void {\n if (active) return;\n if (!process.stdout.isTTY) return;\n if (process.env.QWERTY_NO_ALTSCREEN === '1') return;\n ensureSignals();\n process.stdout.write(ENTER);\n active = true;\n}\n\nexport function leaveAltScreen(): void {\n if (!active) return;\n try {\n process.stdout.write(LEAVE);\n } catch {\n // stream may be closed\n }\n active = false;\n}\n\nexport function isAltScreenActive(): boolean {\n return active;\n}\n","import { Suspense, lazy, useRef, type ReactNode } from 'react';\nimport { Box, Text, useApp, useInput } from 'ink';\nimport { NavProvider, useNav, type ScreenFrame } from './nav.js';\nimport { Fullscreen } from './Fullscreen.js';\nimport { AudioStatusProvider } from './audio-context.js';\nimport { AppStateProvider, useAppState } from './app-state.js';\nimport { StringsProvider } from '../i18n/context.js';\nimport { RegistryProvider } from './registry-context.js';\nimport { MainMenu } from './screens/MainMenu.js';\nimport { PALETTE } from './components/BigWord.js';\nimport type { Config } from '../infra/config-store.js';\n\n// MainMenu stays eager — it's the initial screen for `qwerty` (no args) and\n// must render instantly. All other screens are lazy so their module graphs\n// (DictBrowser pulls registry helpers, StatsViewer pulls stats domain, etc.)\n// only load when the user navigates to them.\nconst PracticeScreen = lazy(() =>\n import('./screens/PracticeScreen.js').then((m) => ({ default: m.PracticeScreen })),\n);\nconst DictBrowser = lazy(() =>\n import('./screens/DictBrowser.js').then((m) => ({ default: m.DictBrowser })),\n);\nconst ConfigEditor = lazy(() =>\n import('./screens/ConfigEditor.js').then((m) => ({ default: m.ConfigEditor })),\n);\nconst StatsViewer = lazy(() =>\n import('./screens/StatsViewer.js').then((m) => ({ default: m.StatsViewer })),\n);\nconst WordLookup = lazy(() =>\n import('./screens/WordLookup.js').then((m) => ({ default: m.WordLookup })),\n);\nconst HelpScreen = lazy(() =>\n import('./screens/HelpScreen.js').then((m) => ({ default: m.HelpScreen })),\n);\n\nfunction LazyFallback() {\n return (\n <Box alignItems=\"center\" justifyContent=\"center\" width=\"100%\" height=\"100%\">\n <Text color={PALETTE.muted}>…</Text>\n </Box>\n );\n}\n\nexport function App({\n initial,\n initialCfg,\n inline = false,\n}: {\n initial: ScreenFrame;\n initialCfg: Config;\n inline?: boolean;\n}) {\n return (\n <AppStateProvider initialCfg={initialCfg}>\n <LangBridge>\n <RegistryProvider>\n <AudioStatusProvider disabled={!initialCfg.sounds.master}>\n <NavProvider initial={initial}>\n {inline ? <Router inline /> : <Fullscreen><Router /></Fullscreen>}\n </NavProvider>\n </AudioStatusProvider>\n </RegistryProvider>\n </LangBridge>\n </AppStateProvider>\n );\n}\n\nfunction LangBridge({ children }: { children: ReactNode }) {\n const { cfg } = useAppState();\n return <StringsProvider pref={cfg.language}>{children}</StringsProvider>;\n}\n\nfunction screenKey(frame: ScreenFrame): string {\n if (frame.name === 'practice') {\n const p = frame.params;\n return `practice:${p.dictId}:${p.chapterIndex}:${p.mode}:${p.stealth ? 's' : 'n'}`;\n }\n return frame.name;\n}\n\nfunction Router({ inline = false }: { inline?: boolean }) {\n const nav = useNav();\n const { cfg } = useAppState();\n const { exit } = useApp();\n const lastKeyRef = useRef<string | null>(null);\n\n useInput((input, key) => {\n if (key.ctrl && input === 'c') exit();\n });\n\n const frame = nav.current;\n const key = screenKey(frame);\n if (lastKeyRef.current !== key) {\n if (!inline && process.stdout.isTTY) process.stdout.write('\\x1b[2J\\x1b[H');\n lastKeyRef.current = key;\n }\n\n const screen = (() => {\n switch (frame.name) {\n case 'main':\n return <MainMenu cfg={cfg} />;\n case 'practice':\n return <PracticeScreen params={frame.params} />;\n case 'dict':\n return <DictBrowser params={frame.params} />;\n case 'config':\n return <ConfigEditor />;\n case 'stats':\n return <StatsViewer />;\n case 'word':\n return <WordLookup />;\n case 'help':\n return <HelpScreen />;\n }\n })();\n\n return <Suspense fallback={<LazyFallback />}>{screen}</Suspense>;\n}\n","import { useEffect, useState, type ReactNode } from 'react';\nimport { Box, useStdout } from 'ink';\nimport { enterAltScreen, leaveAltScreen } from '../util/altscreen.js';\n\nexport function Fullscreen({ children }: { children: ReactNode }) {\n const { stdout } = useStdout();\n const [size, setSize] = useState(() => ({\n rows: stdout?.rows ?? 24,\n cols: stdout?.columns ?? 80,\n }));\n\n useEffect(() => {\n // enterAltScreen is idempotent — if the command entry already wrote the\n // ENTER sequence pre-render (the normal path for menu / non-stealth\n // practice), this is a no-op. Acts as a safety net for any code path\n // that mounts the App without pre-render alt-screen setup.\n enterAltScreen();\n\n const onResize = () => {\n setSize({ rows: process.stdout.rows ?? 24, cols: process.stdout.columns ?? 80 });\n };\n process.stdout.on('resize', onResize);\n\n return () => {\n process.stdout.off('resize', onResize);\n leaveAltScreen();\n };\n }, []);\n\n return (\n <Box width={size.cols} height={size.rows} flexDirection=\"column\">\n {children}\n </Box>\n );\n}\n","import { useState } from 'react';\nimport { Box, Text, useApp, useInput } from 'ink';\nimport { useNav } from '../nav.js';\nimport { useAudioStatus } from '../audio-context.js';\nimport { useStrings } from '../../i18n/context.js';\nimport { useDictName } from '../registry-context.js';\nimport { PALETTE } from '../components/BigWord.js';\nimport { visibleWidth } from '../../util/text.js';\nimport { truncateName } from '../../util/dict-name.js';\nimport { setPostExitAction } from '../../util/post-exit-action.js';\nimport { loadSessions, resumeChapterFor } from '../../domain/stats.js';\nimport type { Config } from '../../infra/config-store.js';\n\ntype Item = { key: string; label: string; hint: string; run: () => void };\n\nexport function MainMenu({ cfg }: { cfg: Config }) {\n const [selected, setSelected] = useState(0);\n const { exit } = useApp();\n const nav = useNav();\n const audio = useAudioStatus();\n const t = useStrings();\n const defaultDictName = useDictName(cfg.defaultDict);\n\n const m = t.mainMenu.items;\n const startPractice = async (stealth: boolean) => {\n if (!cfg.defaultDict) {\n nav.navigate({ name: 'dict', params: { pickerMode: 'choose-then-practice' } });\n return;\n }\n const chapterIndex = resumeChapterFor(await loadSessions(), cfg.defaultDict);\n // Stealth needs Ink rendered in inline mode (no alt-screen), but the menu\n // is currently running inside an alt-screen render(). Hand the dict +\n // mode off via a single-shot store and exit Ink; the CLI entry point\n // consumes the action and re-renders inline.\n if (stealth) {\n setPostExitAction({\n type: 'stealth',\n dictId: cfg.defaultDict,\n chapterIndex,\n mode: cfg.defaultMode,\n });\n exit();\n return;\n }\n nav.navigate({\n name: 'practice',\n params: {\n dictId: cfg.defaultDict,\n chapterIndex,\n mode: cfg.defaultMode,\n stealth,\n },\n });\n };\n\n const items: Item[] = [\n {\n key: 'p',\n label: m.practiceLabel,\n hint: cfg.defaultDict\n ? m.practiceHintWith(truncateName(defaultDictName, 24))\n : m.practiceHintNone,\n run: () => void startPractice(cfg.stealth === 'default'),\n },\n ];\n\n if (cfg.stealth === 'menu' || cfg.stealth === 'default') {\n items.push({\n key: 'b',\n label: m.stealthLabel,\n hint: m.stealthHint,\n run: () => void startPractice(true),\n });\n }\n\n items.push(\n { key: 'd', label: m.dictLabel, hint: m.dictHint, run: () => nav.navigate({ name: 'dict' }) },\n { key: 'w', label: m.wordLabel, hint: m.wordHint, run: () => nav.navigate({ name: 'word' }) },\n { key: 's', label: m.statsLabel, hint: m.statsHint, run: () => nav.navigate({ name: 'stats' }) },\n { key: 'c', label: m.configLabel, hint: m.configHint, run: () => nav.navigate({ name: 'config' }) },\n { key: 'q', label: m.quitLabel, hint: m.quitHint, run: () => exit() },\n );\n\n const labelW = Math.max(...items.map((it) => visibleWidth(it.label))) + 4;\n\n useInput((input, key) => {\n if (key.escape) {\n exit();\n return;\n }\n if (key.upArrow) setSelected((i) => (i - 1 + items.length) % items.length);\n if (key.downArrow) setSelected((i) => (i + 1) % items.length);\n if (key.return) {\n items[selected]!.run();\n return;\n }\n if (input === '?') {\n nav.navigate({ name: 'help' });\n return;\n }\n for (const it of items) {\n if (input === it.key) {\n it.run();\n return;\n }\n }\n });\n\n return (\n <Box flexDirection=\"column\" paddingX={2} paddingY={1} width=\"100%\">\n {cfg.stealth === 'off' && (\n <Box>\n <Text bold color={PALETTE.accent}>\n {t.app.title}\n </Text>\n <Text color={PALETTE.muted}> · {t.app.subtitle}</Text>\n </Box>\n )}\n\n <Box marginTop={cfg.stealth === 'off' ? 2 : 0} flexDirection=\"column\">\n {items.map((it, i) => {\n const active = i === selected;\n const pad = ' '.repeat(Math.max(0, labelW - visibleWidth(it.label)));\n return (\n <Box key={it.key}>\n <Text color={active ? PALETTE.accent : PALETTE.muted}>{active ? '▌ ' : ' '}</Text>\n <Text color={active ? PALETTE.accent : PALETTE.muted}>[{it.key}]</Text>\n <Text>{' '}</Text>\n <Text bold={active} color={active ? PALETTE.text : PALETTE.muted}>\n {it.label}\n {pad}\n </Text>\n <Text color={PALETTE.muted}>{it.hint}</Text>\n </Box>\n );\n })}\n </Box>\n\n <Box marginTop={2}>\n <Text color={PALETTE.muted}>\n {t.mainMenu.hint}\n {' · '}\n {t.mainMenu.helpHint}\n </Text>\n </Box>\n\n {audio.warning && (\n <Box marginTop={1}>\n <Text color={PALETTE.warning}>{t.audio.noPlayer}</Text>\n </Box>\n )}\n </Box>\n );\n}\n","import chalk from 'chalk';\nimport boxen from 'boxen';\nimport type { SessionReport } from '../infra/session-tracker.js';\nimport type { Strings } from '../i18n/strings.js';\nimport { leaveAltScreen } from './altscreen.js';\nimport { visibleWidth } from './text.js';\n\nexport function ensureMainScreen(): void {\n leaveAltScreen();\n}\n\nfunction fmtDuration(ms: number, lang: 'zh' | 'en'): string {\n const total = Math.floor(ms / 1000);\n const m = Math.floor(total / 60);\n const s = total % 60;\n if (lang === 'zh') {\n if (m === 0) return `${s} 秒`;\n return `${m} 分 ${s} 秒`;\n }\n if (m === 0) return `${s}s`;\n return `${m}m ${s}s`;\n}\n\nfunction accColor(pct: number): (s: string) => string {\n if (pct >= 90) return chalk.green;\n if (pct < 75) return chalk.dim;\n return (s) => s;\n}\n\nfunction buildLines(r: SessionReport, t: Strings, lang: 'zh' | 'en'): string[] {\n const lines: string[] = [];\n\n // Align labels in a single visual column. Use the longest label's\n // visible width as the target (CJK counted at 2 cols by visibleWidth).\n const allLabels: string[] = [t.report.duration];\n if (r.chaptersCompleted === 0) {\n allLabels.push(t.report.notPracticed);\n } else {\n allLabels.push(\n t.report.practiced,\n t.report.chapters,\n t.report.words,\n t.report.accuracy,\n t.report.wpm,\n );\n if (r.newMistakeWords > 0) allLabels.push(t.report.newMistakes);\n }\n const labelW = Math.max(...allLabels.map(visibleWidth));\n const padLabel = (s: string) =>\n s + ' '.repeat(Math.max(0, labelW - visibleWidth(s)));\n const row = (label: string, value: string) =>\n `${chalk.dim(padLabel(label))} ${value}`;\n\n lines.push(row(t.report.duration, fmtDuration(r.totalDurationMs, lang)));\n\n if (r.chaptersCompleted === 0) {\n lines.push(chalk.dim(t.report.notPracticed));\n } else {\n lines.push(row(t.report.practiced, fmtDuration(r.practiceMs, lang)));\n lines.push(row(t.report.chapters, String(r.chaptersCompleted)));\n lines.push(row(t.report.words, String(r.wordCount)));\n const accPct = Math.round(r.accuracy * 1000) / 10;\n lines.push(row(t.report.accuracy, accColor(accPct)(`${accPct}%`)));\n lines.push(row(t.report.wpm, String(r.wpm)));\n if (r.newMistakeWords > 0) {\n lines.push(row(t.report.newMistakes, chalk.red(String(r.newMistakeWords))));\n }\n }\n\n lines.push('');\n lines.push(chalk.dim.italic(t.report.farewell));\n return lines;\n}\n\nexport function printSessionReport(r: SessionReport, t: Strings, lang: 'zh' | 'en'): void {\n if (r.startedAt === null && r.chaptersCompleted === 0) return;\n const content = buildLines(r, t, lang).join('\\n');\n console.log(\n boxen(content, {\n title: chalk.bold.cyan(t.report.title),\n titleAlignment: 'left',\n borderStyle: 'round',\n borderColor: 'gray',\n padding: { top: 0, bottom: 0, left: 3, right: 3 },\n margin: { top: 1, bottom: 1, left: 2, right: 0 },\n }),\n );\n}\n"],"mappings":"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","names":["ENTER","LEAVE","active","signalsRegistered","ensureSignals","cleanup","enterAltScreen","leaveAltScreen","Suspense","lazy","useRef","Box","Text","useApp","useInput","useEffect","useState","Box","useStdout","jsx","Fullscreen","children","stdout","useStdout","size","setSize","useState","useEffect","enterAltScreen","onResize","leaveAltScreen","Box","useState","Box","Text","useApp","useInput","jsx","jsxs","MainMenu","cfg","selected","setSelected","useState","exit","useApp","nav","useNav","audio","useAudioStatus","t","useStrings","defaultDictName","useDictName","m","startPractice","stealth","chapterIndex","resumeChapterFor","loadSessions","setPostExitAction","items","truncateName","labelW","it","visibleWidth","useInput","input","key","i","Box","Text","PALETTE","active","pad","jsx","PracticeScreen","lazy","m","DictBrowser","ConfigEditor","StatsViewer","WordLookup","HelpScreen","LazyFallback","Box","Text","PALETTE","App","initial","initialCfg","inline","AppStateProvider","LangBridge","RegistryProvider","AudioStatusProvider","NavProvider","Router","Fullscreen","children","cfg","useAppState","StringsProvider","screenKey","frame","p","nav","useNav","exit","useApp","lastKeyRef","useRef","useInput","input","key","screen","MainMenu","Suspense","chalk","boxen","ensureMainScreen","leaveAltScreen","fmtDuration","ms","lang","total","m","s","accColor","pct","chalk","buildLines","r","lines","allLabels","labelW","visibleWidth","padLabel","row","label","value","accPct","printSessionReport","content","boxen"]}
@@ -0,0 +1,2 @@
1
+ import{a}from"./chunk-WRO5XX35.js";import{c as u}from"./chunk-NA5UNUVL.js";import{createContext as l,useContext as d,useEffect as p,useState as g}from"react";import{jsx as x}from"react/jsx-runtime";var f=l(null);function w({children:t}){let[e,r]=g(null),[s,i]=g(new Map);return p(()=>{let n=!1;return(async()=>{try{let o=await u();if(n)return;let c=new Map;for(let y of o)c.set(y.id,y);r(o),i(c)}catch{n||(r([]),i(new Map))}})(),()=>{n=!0}},[]),x(f.Provider,{value:{registry:e,byId:s},children:t})}function R(){let t=d(f);if(!t)throw new Error("useRegistry must be used inside RegistryProvider");return t}function C(t){let{byId:e}=R();return t?e.get(t)?.name??t:""}function D(t,e){if(a(t)<=e)return t;let r="",s=0;for(let i of t){let n=i.codePointAt(0),o=n>11904&&n<64256?2:1;if(s+o>e-1)break;r+=i,s+=o}return r+"\u2026"}export{w as a,C as b,D as c};
2
+ //# sourceMappingURL=chunk-UEJCQKZ2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/ui/registry-context.tsx","../src/util/dict-name.ts"],"sourcesContent":["import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';\nimport { loadRegistry } from '../infra/registry-store.js';\nimport type { Registry, DictionaryEntry } from '../domain/dictionary.js';\n\ntype RegistryContextValue = {\n registry: Registry | null;\n byId: Map<string, DictionaryEntry>;\n};\n\nconst RegistryContext = createContext<RegistryContextValue | null>(null);\n\nexport function RegistryProvider({ children }: { children: ReactNode }) {\n const [registry, setRegistry] = useState<Registry | null>(null);\n const [byId, setById] = useState<Map<string, DictionaryEntry>>(new Map());\n\n useEffect(() => {\n let cancelled = false;\n (async () => {\n try {\n const reg = await loadRegistry();\n if (cancelled) return;\n const map = new Map<string, DictionaryEntry>();\n for (const e of reg) map.set(e.id, e);\n setRegistry(reg);\n setById(map);\n } catch {\n if (!cancelled) {\n setRegistry([]);\n setById(new Map());\n }\n }\n })();\n return () => {\n cancelled = true;\n };\n }, []);\n\n return (\n <RegistryContext.Provider value={{ registry, byId }}>\n {children}\n </RegistryContext.Provider>\n );\n}\n\nexport function useRegistry(): RegistryContextValue {\n const ctx = useContext(RegistryContext);\n if (!ctx) throw new Error('useRegistry must be used inside RegistryProvider');\n return ctx;\n}\n\nexport function useDictName(id: string | undefined): string {\n const { byId } = useRegistry();\n if (!id) return '';\n const entry = byId.get(id);\n return entry?.name ?? id;\n}\n","import { visibleWidth } from './text.js';\n\nexport function truncateName(name: string, max: number): string {\n if (visibleWidth(name) <= max) return name;\n let out = '';\n let w = 0;\n for (const ch of name) {\n const code = ch.codePointAt(0)!;\n const cw = code > 0x2e80 && code < 0xfb00 ? 2 : 1;\n if (w + cw > max - 1) break;\n out += ch;\n w += cw;\n }\n return out + '…';\n}\n"],"mappings":"2EAAA,OAAS,iBAAAA,EAAe,cAAAC,EAAY,aAAAC,EAAW,YAAAC,MAAgC,QAsC3E,cAAAC,MAAA,oBA7BJ,IAAMC,EAAkBC,EAA2C,IAAI,EAEhE,SAASC,EAAiB,CAAE,SAAAC,CAAS,EAA4B,CACtE,GAAM,CAACC,EAAUC,CAAW,EAAIC,EAA0B,IAAI,EACxD,CAACC,EAAMC,CAAO,EAAIF,EAAuC,IAAI,GAAK,EAExE,OAAAG,EAAU,IAAM,CACd,IAAIC,EAAY,GAChB,OAAC,SAAY,CACX,GAAI,CACF,IAAMC,EAAM,MAAMC,EAAa,EAC/B,GAAIF,EAAW,OACf,IAAMG,EAAM,IAAI,IAChB,QAAWC,KAAKH,EAAKE,EAAI,IAAIC,EAAE,GAAIA,CAAC,EACpCT,EAAYM,CAAG,EACfH,EAAQK,CAAG,CACb,MAAQ,CACDH,IACHL,EAAY,CAAC,CAAC,EACdG,EAAQ,IAAI,GAAK,EAErB,CACF,GAAG,EACI,IAAM,CACXE,EAAY,EACd,CACF,EAAG,CAAC,CAAC,EAGHX,EAACC,EAAgB,SAAhB,CAAyB,MAAO,CAAE,SAAAI,EAAU,KAAAG,CAAK,EAC/C,SAAAJ,EACH,CAEJ,CAEO,SAASY,GAAoC,CAClD,IAAMC,EAAMC,EAAWjB,CAAe,EACtC,GAAI,CAACgB,EAAK,MAAM,IAAI,MAAM,kDAAkD,EAC5E,OAAOA,CACT,CAEO,SAASE,EAAYC,EAAgC,CAC1D,GAAM,CAAE,KAAAZ,CAAK,EAAIQ,EAAY,EAC7B,OAAKI,EACSZ,EAAK,IAAIY,CAAE,GACX,MAAQA,EAFN,EAGlB,CCrDO,SAASC,EAAaC,EAAcC,EAAqB,CAC9D,GAAIC,EAAaF,CAAI,GAAKC,EAAK,OAAOD,EACtC,IAAIG,EAAM,GACNC,EAAI,EACR,QAAWC,KAAML,EAAM,CACrB,IAAMM,EAAOD,EAAG,YAAY,CAAC,EACvBE,EAAKD,EAAO,OAAUA,EAAO,MAAS,EAAI,EAChD,GAAIF,EAAIG,EAAKN,EAAM,EAAG,MACtBE,GAAOE,EACPD,GAAKG,CACP,CACA,OAAOJ,EAAM,QACf","names":["createContext","useContext","useEffect","useState","jsx","RegistryContext","createContext","RegistryProvider","children","registry","setRegistry","useState","byId","setById","useEffect","cancelled","reg","loadRegistry","map","e","useRegistry","ctx","useContext","useDictName","id","truncateName","name","max","visibleWidth","out","w","ch","code","cw"]}
@@ -0,0 +1,2 @@
1
+ var i=/\x1b\[[0-9;]*m/g;function c(t){return t.replace(i,"")}function s(t){let n=c(t),r=0;for(let o of n){let e=o.codePointAt(0);r+=e>11904&&e<64256?2:1}return r}function u(t,n){return t+" ".repeat(Math.max(0,n-s(t)))}function f(t){return`/${t}/`}export{s as a,u as b,f as c};
2
+ //# sourceMappingURL=chunk-WRO5XX35.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/util/text.ts"],"sourcesContent":["const ANSI_RE = /\\x1b\\[[0-9;]*m/g;\n\nexport function stripAnsi(s: string): string {\n return s.replace(ANSI_RE, '');\n}\n\nexport function visibleWidth(s: string): number {\n const plain = stripAnsi(s);\n let w = 0;\n for (const ch of plain) {\n const code = ch.codePointAt(0)!;\n w += code > 0x2e80 && code < 0xfb00 ? 2 : 1;\n }\n return w;\n}\n\nexport function pad(s: string, n: number): string {\n return s + ' '.repeat(Math.max(0, n - visibleWidth(s)));\n}\n\nexport function truncate(s: string, max: number): string {\n if (visibleWidth(s) <= max) return s;\n const plain = stripAnsi(s);\n let out = '';\n let w = 0;\n for (const ch of plain) {\n const code = ch.codePointAt(0)!;\n const cw = code > 0x2e80 && code < 0xfb00 ? 2 : 1;\n if (w + cw > max - 1) break;\n out += ch;\n w += cw;\n }\n return out + '…';\n}\n\n// Wrap a raw phonetic string in slashes (IPA phonemic convention, as used by\n// Oxford / Cambridge / Longman). Centralized so the notation stays consistent\n// across the practice screen, stealth, word lookup, and the `word` command.\nexport function wrapPhonetic(raw: string): string {\n return `/${raw}/`;\n}\n"],"mappings":"AAAA,IAAMA,EAAU,kBAET,SAASC,EAAUC,EAAmB,CAC3C,OAAOA,EAAE,QAAQF,EAAS,EAAE,CAC9B,CAEO,SAASG,EAAaD,EAAmB,CAC9C,IAAME,EAAQH,EAAUC,CAAC,EACrBG,EAAI,EACR,QAAWC,KAAMF,EAAO,CACtB,IAAMG,EAAOD,EAAG,YAAY,CAAC,EAC7BD,GAAKE,EAAO,OAAUA,EAAO,MAAS,EAAI,CAC5C,CACA,OAAOF,CACT,CAEO,SAASG,EAAIN,EAAW,EAAmB,CAChD,OAAOA,EAAI,IAAI,OAAO,KAAK,IAAI,EAAG,EAAIC,EAAaD,CAAC,CAAC,CAAC,CACxD,CAoBO,SAASO,EAAaC,EAAqB,CAChD,MAAO,IAAIA,CAAG,GAChB","names":["ANSI_RE","stripAnsi","s","visibleWidth","plain","w","ch","code","pad","wrapPhonetic","raw"]}
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-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-2PPCBBK3.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-2PPCBBK3.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-ZUAMXKFD.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.19");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)});
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-HILWI6GI.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-VVSIGCKS.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-HILWI6GI.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-6DPHV36B.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.21");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
@@ -0,0 +1,2 @@
1
+ import{a as o,b as s,c as p,d as c}from"./chunk-MHLVBOJU.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-UEJCQKZ2.js";import"./chunk-WRO5XX35.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-HILWI6GI.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-6DPHV36B.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/commands/menu.impl.ts"],"sourcesContent":["import { render } from 'ink';\nimport { createElement } from 'react';\nimport { loadConfig } from '../infra/config-store.js';\nimport { App } from '../ui/App.js';\nimport { start as startSession, report as sessionReport } from '../infra/session-tracker.js';\nimport { printSessionReport, ensureMainScreen } from '../util/report.js';\nimport { pickStrings } from '../i18n/context.js';\nimport { enterAltScreen } from '../util/altscreen.js';\nimport { consumePostExitAction } from '../util/post-exit-action.js';\n\nexport async function runMainMenuImpl(): Promise<void> {\n // Enter alt-screen synchronously BEFORE render() so Ink's first frame\n // never lands on the main screen. Without this, the menu leaks under\n // the exit report when the user quits.\n enterAltScreen();\n\n const cfg = await loadConfig();\n startSession();\n const { waitUntilExit } = render(\n createElement(App, { initial: { name: 'main' }, initialCfg: cfg }),\n { patchConsole: false, exitOnCtrlC: false },\n );\n await waitUntilExit();\n ensureMainScreen();\n\n // Handoff: if the menu requested stealth practice, leave alt-screen (done)\n // and re-enter via runPractice() in inline mode so the 3 rows render under\n // the real shell scrollback. runPractice prints its own report at the end.\n const pending = consumePostExitAction();\n if (pending?.type === 'stealth') {\n const { runPractice } = await import('./practice.impl.js');\n await runPractice(pending.dictId, {\n chapter: pending.chapterIndex + 1,\n mode: pending.mode,\n stealth: true,\n });\n return;\n }\n\n const { lang, t } = pickStrings(cfg.language);\n printSessionReport(sessionReport(), t, lang);\n}\n"],"mappings":"sWAAA,OAAS,UAAAA,MAAc,MACvB,OAAS,iBAAAC,MAAqB,QAS9B,eAAsBC,GAAiC,CAIrDC,EAAe,EAEf,IAAMC,EAAM,MAAMC,EAAW,EAC7BC,EAAa,EACb,GAAM,CAAE,cAAAC,CAAc,EAAIC,EACxBC,EAAcC,EAAK,CAAE,QAAS,CAAE,KAAM,MAAO,EAAG,WAAYN,CAAI,CAAC,EACjE,CAAE,aAAc,GAAO,YAAa,EAAM,CAC5C,EACA,MAAMG,EAAc,EACpBI,EAAiB,EAKjB,IAAMC,EAAUC,EAAsB,EACtC,GAAID,GAAS,OAAS,UAAW,CAC/B,GAAM,CAAE,YAAAE,CAAY,EAAI,KAAM,QAAO,6BAAoB,EACzD,MAAMA,EAAYF,EAAQ,OAAQ,CAChC,QAASA,EAAQ,aAAe,EAChC,KAAMA,EAAQ,KACd,QAAS,EACX,CAAC,EACD,MACF,CAEA,GAAM,CAAE,KAAAG,EAAM,EAAAC,CAAE,EAAIC,EAAYb,EAAI,QAAQ,EAC5Cc,EAAmBC,EAAc,EAAGH,EAAGD,CAAI,CAC7C","names":["render","createElement","runMainMenuImpl","enterAltScreen","cfg","loadConfig","start","waitUntilExit","render","createElement","App","ensureMainScreen","pending","consumePostExitAction","runPractice","lang","t","pickStrings","printSessionReport","report"]}
1
+ {"version":3,"sources":["../src/commands/menu.impl.ts"],"sourcesContent":["import { render } from 'ink';\nimport { createElement } from 'react';\nimport { loadConfig } from '../infra/config-store.js';\nimport { App } from '../ui/App.js';\nimport { start as startSession, report as sessionReport } from '../infra/session-tracker.js';\nimport { printSessionReport, ensureMainScreen } from '../util/report.js';\nimport { pickStrings } from '../i18n/context.js';\nimport { enterAltScreen } from '../util/altscreen.js';\nimport { consumePostExitAction } from '../util/post-exit-action.js';\n\nexport async function runMainMenuImpl(): Promise<void> {\n // Enter alt-screen synchronously BEFORE render() so Ink's first frame\n // never lands on the main screen. Without this, the menu leaks under\n // the exit report when the user quits.\n enterAltScreen();\n\n const cfg = await loadConfig();\n startSession();\n const { waitUntilExit } = render(\n createElement(App, { initial: { name: 'main' }, initialCfg: cfg }),\n { patchConsole: false, exitOnCtrlC: false },\n );\n await waitUntilExit();\n ensureMainScreen();\n\n // Handoff: if the menu requested stealth practice, leave alt-screen (done)\n // and re-enter via runPractice() in inline mode so the 3 rows render under\n // the real shell scrollback. runPractice prints its own report at the end.\n const pending = consumePostExitAction();\n if (pending?.type === 'stealth') {\n const { runPractice } = await import('./practice.impl.js');\n await runPractice(pending.dictId, {\n chapter: pending.chapterIndex + 1,\n mode: pending.mode,\n stealth: true,\n });\n return;\n }\n\n const { lang, t } = pickStrings(cfg.language);\n printSessionReport(sessionReport(), t, lang);\n}\n"],"mappings":"kYAAA,OAAS,UAAAA,MAAc,MACvB,OAAS,iBAAAC,MAAqB,QAS9B,eAAsBC,GAAiC,CAIrDC,EAAe,EAEf,IAAMC,EAAM,MAAMC,EAAW,EAC7BC,EAAa,EACb,GAAM,CAAE,cAAAC,CAAc,EAAIC,EACxBC,EAAcC,EAAK,CAAE,QAAS,CAAE,KAAM,MAAO,EAAG,WAAYN,CAAI,CAAC,EACjE,CAAE,aAAc,GAAO,YAAa,EAAM,CAC5C,EACA,MAAMG,EAAc,EACpBI,EAAiB,EAKjB,IAAMC,EAAUC,EAAsB,EACtC,GAAID,GAAS,OAAS,UAAW,CAC/B,GAAM,CAAE,YAAAE,CAAY,EAAI,KAAM,QAAO,6BAAoB,EACzD,MAAMA,EAAYF,EAAQ,OAAQ,CAChC,QAASA,EAAQ,aAAe,EAChC,KAAMA,EAAQ,KACd,QAAS,EACX,CAAC,EACD,MACF,CAEA,GAAM,CAAE,KAAAG,EAAM,EAAAC,CAAE,EAAIC,EAAYb,EAAI,QAAQ,EAC5Cc,EAAmBC,EAAc,EAAGH,EAAGD,CAAI,CAC7C","names":["render","createElement","runMainMenuImpl","enterAltScreen","cfg","loadConfig","start","waitUntilExit","render","createElement","App","ensureMainScreen","pending","consumePostExitAction","runPractice","lang","t","pickStrings","printSessionReport","report"]}
@@ -0,0 +1,2 @@
1
+ import{a as c,b as g,c as h,d as x}from"./chunk-MHLVBOJU.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-UEJCQKZ2.js";import"./chunk-WRO5XX35.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-HILWI6GI.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/commands/practice.impl.ts"],"sourcesContent":["import chalk from 'chalk';\nimport { render } from 'ink';\nimport { createElement } from 'react';\nimport { loadConfig } from '../infra/config-store.js';\nimport { App } from '../ui/App.js';\nimport { start as startSession, report as sessionReport } from '../infra/session-tracker.js';\nimport { printSessionReport, ensureMainScreen } from '../util/report.js';\nimport { pickStrings } from '../i18n/context.js';\nimport { enterAltScreen } from '../util/altscreen.js';\nimport { consumeSilentExit } from '../util/post-exit-action.js';\nimport { loadSessions, resumeChapterFor } from '../domain/stats.js';\nimport type { Mode } from '../domain/chapters.js';\n\nconst MODES: Mode[] = ['order', 'dictation', 'review', 'random', 'loop'];\n\nfunction isMode(v: string): v is Mode {\n return (MODES as string[]).includes(v);\n}\n\nexport async function runPractice(\n dictIdArg: string | undefined,\n options: { chapter?: string | number; mode?: string; stealth?: boolean },\n): Promise<void> {\n if (!process.stdout.isTTY) {\n console.error(chalk.red('Practice requires an interactive TTY.'));\n process.exitCode = 1;\n return;\n }\n const cfg = await loadConfig();\n const dictId = dictIdArg ?? cfg.defaultDict;\n if (!dictId) {\n console.error(chalk.red('No dictionary specified. Pass an id or set config.defaultDict.'));\n process.exitCode = 1;\n return;\n }\n const mode = options.mode ?? cfg.defaultMode;\n if (!isMode(mode)) {\n console.error(chalk.red(`Invalid mode \"${mode}\". Valid: ${MODES.join(', ')}`));\n process.exitCode = 1;\n return;\n }\n const chapterIndex =\n options.chapter !== undefined\n ? Math.max(0, Number(options.chapter) - 1)\n : resumeChapterFor(await loadSessions(), dictId);\n const stealth = options.stealth === true || cfg.stealth === 'default';\n\n // Enter alt-screen synchronously BEFORE render() so Ink's first frame\n // never lands on the main screen. Without this, when the user exits,\n // the main screen still has the menu/practice content from frame 0\n // showing under the exit report. Stealth uses inline mode, skip.\n if (!stealth) enterAltScreen();\n\n startSession();\n const { waitUntilExit } = render(\n createElement(App, {\n initial: { name: 'practice', params: { dictId, chapterIndex, mode, stealth } },\n initialCfg: cfg,\n inline: stealth,\n }),\n { patchConsole: false, exitOnCtrlC: false },\n );\n await waitUntilExit();\n if (!stealth) {\n ensureMainScreen();\n }\n\n if (consumeSilentExit()) {\n // User hit Ctrl+C on the stealth paused screen — erase the 3 inline rows\n // (paused / Esc back / blank) and skip the session report for a fully\n // clean exit. Cursor goes back to where the paused UI started; the shell\n // prompt then renders there, leaving no trace.\n process.stdout.write('\\x1b[3F\\x1b[0J');\n return;\n }\n if (stealth) {\n // Stealth: StealthSummary already rendered an inline summary row that\n // stays in scrollback. A second ~10-row boxen report would defeat the\n // point of the mode, so skip it.\n return;\n }\n\n const { lang, t } = pickStrings(cfg.language);\n printSessionReport(sessionReport(), t, lang);\n}\n"],"mappings":"yXAAA,OAAOA,MAAW,QAClB,OAAS,UAAAC,MAAc,MACvB,OAAS,iBAAAC,MAAqB,QAW9B,IAAMC,EAAgB,CAAC,QAAS,YAAa,SAAU,SAAU,MAAM,EAEvE,SAASC,EAAOC,EAAsB,CACpC,OAAQF,EAAmB,SAASE,CAAC,CACvC,CAEA,eAAsBC,EACpBC,EACAC,EACe,CACf,GAAI,CAAC,QAAQ,OAAO,MAAO,CACzB,QAAQ,MAAMC,EAAM,IAAI,uCAAuC,CAAC,EAChE,QAAQ,SAAW,EACnB,MACF,CACA,IAAMC,EAAM,MAAMC,EAAW,EACvBC,EAASL,GAAaG,EAAI,YAChC,GAAI,CAACE,EAAQ,CACX,QAAQ,MAAMH,EAAM,IAAI,gEAAgE,CAAC,EACzF,QAAQ,SAAW,EACnB,MACF,CACA,IAAMI,EAAOL,EAAQ,MAAQE,EAAI,YACjC,GAAI,CAACN,EAAOS,CAAI,EAAG,CACjB,QAAQ,MAAMJ,EAAM,IAAI,iBAAiBI,CAAI,aAAaV,EAAM,KAAK,IAAI,CAAC,EAAE,CAAC,EAC7E,QAAQ,SAAW,EACnB,MACF,CACA,IAAMW,EACJN,EAAQ,UAAY,OAChB,KAAK,IAAI,EAAG,OAAOA,EAAQ,OAAO,EAAI,CAAC,EACvCO,EAAiB,MAAMC,EAAa,EAAGJ,CAAM,EAC7CK,EAAUT,EAAQ,UAAY,IAAQE,EAAI,UAAY,UAMvDO,GAASC,EAAe,EAE7BC,EAAa,EACb,GAAM,CAAE,cAAAC,CAAc,EAAIC,EACxBC,EAAcC,EAAK,CACjB,QAAS,CAAE,KAAM,WAAY,OAAQ,CAAE,OAAAX,EAAQ,aAAAE,EAAc,KAAAD,EAAM,QAAAI,CAAQ,CAAE,EAC7E,WAAYP,EACZ,OAAQO,CACV,CAAC,EACD,CAAE,aAAc,GAAO,YAAa,EAAM,CAC5C,EAMA,GALA,MAAMG,EAAc,EACfH,GACHO,EAAiB,EAGfC,EAAkB,EAAG,CAKvB,QAAQ,OAAO,MAAM,gBAAgB,EACrC,MACF,CACA,GAAIR,EAIF,OAGF,GAAM,CAAE,KAAAS,EAAM,EAAAC,CAAE,EAAIC,EAAYlB,EAAI,QAAQ,EAC5CmB,EAAmBC,EAAc,EAAGH,EAAGD,CAAI,CAC7C","names":["chalk","render","createElement","MODES","isMode","v","runPractice","dictIdArg","options","chalk","cfg","loadConfig","dictId","mode","chapterIndex","resumeChapterFor","loadSessions","stealth","enterAltScreen","start","waitUntilExit","render","createElement","App","ensureMainScreen","consumeSilentExit","lang","t","pickStrings","printSessionReport","report"]}
1
+ {"version":3,"sources":["../src/commands/practice.impl.ts"],"sourcesContent":["import chalk from 'chalk';\nimport { render } from 'ink';\nimport { createElement } from 'react';\nimport { loadConfig } from '../infra/config-store.js';\nimport { App } from '../ui/App.js';\nimport { start as startSession, report as sessionReport } from '../infra/session-tracker.js';\nimport { printSessionReport, ensureMainScreen } from '../util/report.js';\nimport { pickStrings } from '../i18n/context.js';\nimport { enterAltScreen } from '../util/altscreen.js';\nimport { consumeSilentExit } from '../util/post-exit-action.js';\nimport { loadSessions, resumeChapterFor } from '../domain/stats.js';\nimport type { Mode } from '../domain/chapters.js';\n\nconst MODES: Mode[] = ['order', 'dictation', 'review', 'random', 'loop'];\n\nfunction isMode(v: string): v is Mode {\n return (MODES as string[]).includes(v);\n}\n\nexport async function runPractice(\n dictIdArg: string | undefined,\n options: { chapter?: string | number; mode?: string; stealth?: boolean },\n): Promise<void> {\n if (!process.stdout.isTTY) {\n console.error(chalk.red('Practice requires an interactive TTY.'));\n process.exitCode = 1;\n return;\n }\n const cfg = await loadConfig();\n const dictId = dictIdArg ?? cfg.defaultDict;\n if (!dictId) {\n console.error(chalk.red('No dictionary specified. Pass an id or set config.defaultDict.'));\n process.exitCode = 1;\n return;\n }\n const mode = options.mode ?? cfg.defaultMode;\n if (!isMode(mode)) {\n console.error(chalk.red(`Invalid mode \"${mode}\". Valid: ${MODES.join(', ')}`));\n process.exitCode = 1;\n return;\n }\n const chapterIndex =\n options.chapter !== undefined\n ? Math.max(0, Number(options.chapter) - 1)\n : resumeChapterFor(await loadSessions(), dictId);\n const stealth = options.stealth === true || cfg.stealth === 'default';\n\n // Enter alt-screen synchronously BEFORE render() so Ink's first frame\n // never lands on the main screen. Without this, when the user exits,\n // the main screen still has the menu/practice content from frame 0\n // showing under the exit report. Stealth uses inline mode, skip.\n if (!stealth) enterAltScreen();\n\n startSession();\n const { waitUntilExit } = render(\n createElement(App, {\n initial: { name: 'practice', params: { dictId, chapterIndex, mode, stealth } },\n initialCfg: cfg,\n inline: stealth,\n }),\n { patchConsole: false, exitOnCtrlC: false },\n );\n await waitUntilExit();\n if (!stealth) {\n ensureMainScreen();\n }\n\n if (consumeSilentExit()) {\n // User hit Ctrl+C on the stealth paused screen — erase the 3 inline rows\n // (paused / Esc back / blank) and skip the session report for a fully\n // clean exit. Cursor goes back to where the paused UI started; the shell\n // prompt then renders there, leaving no trace.\n process.stdout.write('\\x1b[3F\\x1b[0J');\n return;\n }\n if (stealth) {\n // Stealth: StealthSummary already rendered an inline summary row that\n // stays in scrollback. A second ~10-row boxen report would defeat the\n // point of the mode, so skip it.\n return;\n }\n\n const { lang, t } = pickStrings(cfg.language);\n printSessionReport(sessionReport(), t, lang);\n}\n"],"mappings":"qZAAA,OAAOA,MAAW,QAClB,OAAS,UAAAC,MAAc,MACvB,OAAS,iBAAAC,MAAqB,QAW9B,IAAMC,EAAgB,CAAC,QAAS,YAAa,SAAU,SAAU,MAAM,EAEvE,SAASC,EAAOC,EAAsB,CACpC,OAAQF,EAAmB,SAASE,CAAC,CACvC,CAEA,eAAsBC,EACpBC,EACAC,EACe,CACf,GAAI,CAAC,QAAQ,OAAO,MAAO,CACzB,QAAQ,MAAMC,EAAM,IAAI,uCAAuC,CAAC,EAChE,QAAQ,SAAW,EACnB,MACF,CACA,IAAMC,EAAM,MAAMC,EAAW,EACvBC,EAASL,GAAaG,EAAI,YAChC,GAAI,CAACE,EAAQ,CACX,QAAQ,MAAMH,EAAM,IAAI,gEAAgE,CAAC,EACzF,QAAQ,SAAW,EACnB,MACF,CACA,IAAMI,EAAOL,EAAQ,MAAQE,EAAI,YACjC,GAAI,CAACN,EAAOS,CAAI,EAAG,CACjB,QAAQ,MAAMJ,EAAM,IAAI,iBAAiBI,CAAI,aAAaV,EAAM,KAAK,IAAI,CAAC,EAAE,CAAC,EAC7E,QAAQ,SAAW,EACnB,MACF,CACA,IAAMW,EACJN,EAAQ,UAAY,OAChB,KAAK,IAAI,EAAG,OAAOA,EAAQ,OAAO,EAAI,CAAC,EACvCO,EAAiB,MAAMC,EAAa,EAAGJ,CAAM,EAC7CK,EAAUT,EAAQ,UAAY,IAAQE,EAAI,UAAY,UAMvDO,GAASC,EAAe,EAE7BC,EAAa,EACb,GAAM,CAAE,cAAAC,CAAc,EAAIC,EACxBC,EAAcC,EAAK,CACjB,QAAS,CAAE,KAAM,WAAY,OAAQ,CAAE,OAAAX,EAAQ,aAAAE,EAAc,KAAAD,EAAM,QAAAI,CAAQ,CAAE,EAC7E,WAAYP,EACZ,OAAQO,CACV,CAAC,EACD,CAAE,aAAc,GAAO,YAAa,EAAM,CAC5C,EAMA,GALA,MAAMG,EAAc,EACfH,GACHO,EAAiB,EAGfC,EAAkB,EAAG,CAKvB,QAAQ,OAAO,MAAM,gBAAgB,EACrC,MACF,CACA,GAAIR,EAIF,OAGF,GAAM,CAAE,KAAAS,EAAM,EAAAC,CAAE,EAAIC,EAAYlB,EAAI,QAAQ,EAC5CmB,EAAmBC,EAAc,EAAGH,EAAGD,CAAI,CAC7C","names":["chalk","render","createElement","MODES","isMode","v","runPractice","dictIdArg","options","chalk","cfg","loadConfig","dictId","mode","chapterIndex","resumeChapterFor","loadSessions","stealth","enterAltScreen","start","waitUntilExit","render","createElement","App","ensureMainScreen","consumeSilentExit","lang","t","pickStrings","printSessionReport","report"]}
@@ -0,0 +1,2 @@
1
+ import{c as h}from"./chunk-EBAA2ZKH.js";import"./chunk-VTIB5Q36.js";import{a as y}from"./chunk-G3DQB7FI.js";import{c as p}from"./chunk-WRO5XX35.js";import{d as g}from"./chunk-NA5UNUVL.js";import{a as w}from"./chunk-E6BBQALJ.js";import i from"chalk";import{readdir as j}from"fs/promises";async function I(){try{return(await j(w.dictsDir)).filter(s=>s.endsWith(".json")&&!s.endsWith(".meta.json")).map(s=>s.replace(/\.json$/,""))}catch{return[]}}async function q(c,s){let u=c.toLowerCase(),a=await I();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 h(o);if(t)for(let n of t){let r=n.name.toLowerCase();(s.exact?r===u:r.includes(u))&&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 $=await y();for(let[o,t]of d){let n=t[0].word;console.log(),console.log(i.bold.white(o));let r=n.usphone?`US ${p(n.usphone)}`:"",m=n.ukphone?`UK ${p(n.ukphone)}`:"";(r||m)&&console.log(i.dim(` ${[r,m].filter(Boolean).join(" ")}`));for(let e of n.trans??[])console.log(i.cyan(` \xB7 ${e}`));let k=await Promise.all(t.map(async e=>(await g(e.dictId))?.name??e.dictId));console.log(i.dim(` in: ${k.join(", ")}`));let f=$[o];f&&console.log(i.dim(` mistakes: ${f.count} (last ${f.lastSeen.slice(0,10)})`))}console.log()}export{q as runWordLookup};
2
+ //# sourceMappingURL=word.impl-VVSIGCKS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/word.impl.ts"],"sourcesContent":["import chalk from 'chalk';\nimport { readdir } from 'node:fs/promises';\nimport { paths } from '../infra/paths.js';\nimport { loadLocalDictionary } from '../infra/dict-downloader.js';\nimport { loadMistakes } from '../domain/mistakes.js';\nimport { findEntry } from '../infra/registry-store.js';\nimport { wrapPhonetic } from '../util/text.js';\nimport type { Word } from '../domain/dictionary.js';\n\nasync function listLocalDictIds(): Promise<string[]> {\n try {\n const files = await readdir(paths.dictsDir);\n return files\n .filter((f) => f.endsWith('.json') && !f.endsWith('.meta.json'))\n .map((f) => f.replace(/\\.json$/, ''));\n } catch {\n return [];\n }\n}\n\nexport async function runWordLookup(keyword: string, opts: { exact?: boolean }): Promise<void> {\n const q = keyword.toLowerCase();\n const ids = await listLocalDictIds();\n if (ids.length === 0) {\n console.log(chalk.yellow('No local dictionaries. Run `qwerty dict pull <id>` first.'));\n return;\n }\n\n type Hit = { dictId: string; word: Word };\n const hits: Hit[] = [];\n for (const id of ids) {\n const words = await loadLocalDictionary(id);\n if (!words) continue;\n for (const w of words) {\n const wl = w.name.toLowerCase();\n const match = opts.exact ? wl === q : wl.includes(q);\n if (match) hits.push({ dictId: id, word: w });\n }\n }\n\n if (hits.length === 0) {\n console.log(\n chalk.yellow(`No matches for \"${keyword}\" in ${ids.length} local dictionaries`),\n );\n return;\n }\n\n // Group hits by word name; one block per distinct word.\n const byName = new Map<string, Hit[]>();\n for (const h of hits) {\n const arr = byName.get(h.word.name) ?? [];\n arr.push(h);\n byName.set(h.word.name, arr);\n }\n\n const book = await loadMistakes();\n for (const [name, group] of byName) {\n const first = group[0]!.word;\n console.log();\n console.log(chalk.bold.white(name));\n const us = first.usphone ? `US ${wrapPhonetic(first.usphone)}` : '';\n const uk = first.ukphone ? `UK ${wrapPhonetic(first.ukphone)}` : '';\n if (us || uk) console.log(chalk.dim(` ${[us, uk].filter(Boolean).join(' ')}`));\n for (const t of first.trans ?? []) console.log(chalk.cyan(` · ${t}`));\n const sources = await Promise.all(\n group.map(async (h) => {\n const reg = await findEntry(h.dictId);\n return reg?.name ?? h.dictId;\n }),\n );\n console.log(chalk.dim(` in: ${sources.join(', ')}`));\n const mistake = book[name];\n if (mistake) {\n console.log(\n chalk.dim(` mistakes: ${mistake.count} (last ${mistake.lastSeen.slice(0, 10)})`),\n );\n }\n }\n console.log();\n}\n"],"mappings":"oOAAA,OAAOA,MAAW,QAClB,OAAS,WAAAC,MAAe,cAQxB,eAAeC,GAAsC,CACnD,GAAI,CAEF,OADc,MAAMC,EAAQC,EAAM,QAAQ,GAEvC,OAAQC,GAAMA,EAAE,SAAS,OAAO,GAAK,CAACA,EAAE,SAAS,YAAY,CAAC,EAC9D,IAAKA,GAAMA,EAAE,QAAQ,UAAW,EAAE,CAAC,CACxC,MAAQ,CACN,MAAO,CAAC,CACV,CACF,CAEA,eAAsBC,EAAcC,EAAiBC,EAA0C,CAC7F,IAAMC,EAAIF,EAAQ,YAAY,EACxBG,EAAM,MAAMR,EAAiB,EACnC,GAAIQ,EAAI,SAAW,EAAG,CACpB,QAAQ,IAAIC,EAAM,OAAO,2DAA2D,CAAC,EACrF,MACF,CAGA,IAAMC,EAAc,CAAC,EACrB,QAAWC,KAAMH,EAAK,CACpB,IAAMI,EAAQ,MAAMC,EAAoBF,CAAE,EAC1C,GAAKC,EACL,QAAWE,KAAKF,EAAO,CACrB,IAAMG,EAAKD,EAAE,KAAK,YAAY,GAChBR,EAAK,MAAQS,IAAOR,EAAIQ,EAAG,SAASR,CAAC,IACxCG,EAAK,KAAK,CAAE,OAAQC,EAAI,KAAMG,CAAE,CAAC,CAC9C,CACF,CAEA,GAAIJ,EAAK,SAAW,EAAG,CACrB,QAAQ,IACND,EAAM,OAAO,mBAAmBJ,CAAO,QAAQG,EAAI,MAAM,qBAAqB,CAChF,EACA,MACF,CAGA,IAAMQ,EAAS,IAAI,IACnB,QAAWC,KAAKP,EAAM,CACpB,IAAMQ,EAAMF,EAAO,IAAIC,EAAE,KAAK,IAAI,GAAK,CAAC,EACxCC,EAAI,KAAKD,CAAC,EACVD,EAAO,IAAIC,EAAE,KAAK,KAAMC,CAAG,CAC7B,CAEA,IAAMC,EAAO,MAAMC,EAAa,EAChC,OAAW,CAACC,EAAMC,CAAK,IAAKN,EAAQ,CAClC,IAAMO,EAAQD,EAAM,CAAC,EAAG,KACxB,QAAQ,IAAI,EACZ,QAAQ,IAAIb,EAAM,KAAK,MAAMY,CAAI,CAAC,EAClC,IAAMG,EAAKD,EAAM,QAAU,MAAME,EAAaF,EAAM,OAAO,CAAC,GAAK,GAC3DG,EAAKH,EAAM,QAAU,MAAME,EAAaF,EAAM,OAAO,CAAC,GAAK,IAC7DC,GAAME,IAAI,QAAQ,IAAIjB,EAAM,IAAI,KAAK,CAACe,EAAIE,CAAE,EAAE,OAAO,OAAO,EAAE,KAAK,KAAK,CAAC,EAAE,CAAC,EAChF,QAAWC,KAAKJ,EAAM,OAAS,CAAC,EAAG,QAAQ,IAAId,EAAM,KAAK,UAAOkB,CAAC,EAAE,CAAC,EACrE,IAAMC,EAAU,MAAM,QAAQ,IAC5BN,EAAM,IAAI,MAAOL,IACH,MAAMY,EAAUZ,EAAE,MAAM,IACxB,MAAQA,EAAE,MACvB,CACH,EACA,QAAQ,IAAIR,EAAM,IAAI,SAASmB,EAAQ,KAAK,IAAI,CAAC,EAAE,CAAC,EACpD,IAAME,EAAUX,EAAKE,CAAI,EACrBS,GACF,QAAQ,IACNrB,EAAM,IAAI,eAAeqB,EAAQ,KAAK,UAAUA,EAAQ,SAAS,MAAM,EAAG,EAAE,CAAC,GAAG,CAClF,CAEJ,CACA,QAAQ,IAAI,CACd","names":["chalk","readdir","listLocalDictIds","readdir","paths","f","runWordLookup","keyword","opts","q","ids","chalk","hits","id","words","loadLocalDictionary","w","wl","byName","h","arr","book","loadMistakes","name","group","first","us","wrapPhonetic","uk","t","sources","findEntry","mistake"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwerty-cli",
3
- "version": "0.0.1-alpha.19",
3
+ "version": "0.0.1-alpha.21",
4
4
  "description": "Terminal clone of qwerty-learner: typing practice for English vocabulary, with chapters, dictation, mistake book, and audio.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,2 +0,0 @@
1
- import{b as P}from"./chunk-MFGIEKBU.js";import{d as E}from"./chunk-VTIB5Q36.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-VIOZNKSK.js";import"./chunk-NA5UNUVL.js";import"./chunk-E6BBQALJ.js";import{useState as k}from"react";import{Box as g,Text as c,useInput as V}from"ink";import{jsx as d,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:"enum",path:"wordDisplay",labelKey:"wordDisplay",options:["auto","huge","standard"]},{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(),y=$(e.defaultDict),[x,K]=k(0),[m,h]=k(!1),[w,p]=k(""),[v,u]=k(null),r=f[x],D=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(D?"false":"true");return}if(r.kind==="enum"&&(n.leftArrow||n.rightArrow)){let t=r.options.indexOf(String(D)),T=n.rightArrow?1:-1,R=r.options[(t+T+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(D??"")),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:[d(c,{bold:!0,color:l.accent,children:s.config.title}),d(g,{marginTop:1,flexDirection:"column",flexGrow:1,children:f.map((i,n)=>{let t=n===x,T=N(e,i.path),R=j(i,T,t&&m?w:null,s,i.path==="defaultDict"?y:""),A=s.config.fields[i.labelKey],I=" ".repeat(Math.max(0,z-B(A)));return S(g,{children:[d(c,{color:t?l.accent:l.muted,children:t?"\u258C ":" "}),S(c,{bold:t,color:t?l.text:l.muted,children:[A,I]}),d(c,{color:t?l.accent:l.muted,children:R})]},i.path)})}),v&&d(g,{marginTop:1,children:S(c,{color:l.error,children:["! ",v]})}),d(g,{marginTop:1,children:d(c,{color:l.muted,children:L(r,m,s)})})]})}function j(a,e,o,s,y){return o!==null?`${o}_`:a.kind==="bool"?e?`\u2713 ${s.common.on}`:`\u2717 ${s.common.off}`:a.kind==="dictRef"?e?C(y||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-YYZQ7TUJ.js.map
@@ -1,2 +0,0 @@
1
- import{b as gt,e as yt,h as It}from"./chunk-6ROGUGNX.js";import{a as lt,b as dt,c as mt,d as pt,e as ft,f as ht}from"./chunk-BIBS2Q3E.js";import{e as ut}from"./chunk-EBAA2ZKH.js";import{b as O}from"./chunk-MFGIEKBU.js";import"./chunk-VTIB5Q36.js";import{a as V,b as Tt,c as St}from"./chunk-G3DQB7FI.js";import{a as wt}from"./chunk-GULN5HRV.js";import{b as xt,e as _}from"./chunk-QG7ZTS2G.js";import{b as X,d as W,f as c,g as bt}from"./chunk-VIOZNKSK.js";import"./chunk-NA5UNUVL.js";import"./chunk-E6BBQALJ.js";import{useState as H,useEffect as K,useRef as et}from"react";import{Box as p,Text as x,useApp as Me,useInput as j}from"ink";function Ct(t,e=Math.random){let r=[...t];for(let n=r.length-1;n>0;n--){let o=Math.floor(e()*(n+1)),u=r[n];r[n]=r[o],r[o]=u}return r}function Mt(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 kt(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 Et(t,e,r){if(e==="random"){let n=r===void 0?Math.random:Mt(r);return Ct(t,n)}return t}function G(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:G(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:o}=Wt(t.current.input,e);if(o==="correct"){let u={word:n.target,errors:n.errorsThisWord,durationMs:r-t.current.wordStartedAt},s=t.current.wordIndex+1,a=[...t.results,u];return s>=t.playlist.length?{session:{...t,results:a,current:null,finishedAt:r},effect:o}:{session:{...t,results:a,current:{wordIndex:s,wordStartedAt:r,input:G(t.playlist[s].name)}},effect:o}}return{session:{...t,current:{...t.current,input:n}},effect:o}}function At(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,o=[...t.results,r];return n>=t.playlist.length?{session:{...t,results:o,current:null,finishedAt:e},effect:"skipped"}:{session:{...t,results:o,current:{wordIndex:n,wordStartedAt:e,input:G(t.playlist[n].name)}},effect:"skipped"}}function Z(t){let e=t.results.reduce((o,u)=>o+u.errors,0),r=(t.finishedAt??Date.now())-t.startedAt,n={};for(let o of t.results)o.errors>0&&(n[o.word]=(n[o.word]??0)+o.errors);return{wordCount:t.results.length,errors:e,durationMs:r,perWordErrors:n}}import{useEffect as Bt,useReducer as ce,useRef as se,useState as ae}from"react";import{useInput as ue,useApp as le}from"ink";function de(t,e){if(e.type==="start")return{session:J(e.playlist,e.now),lastEffect:null,effectSeq:0};if(e.type==="skip"){let r=At(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 o=Q(t.session,{type:"backspace"},e.now);return{session:o.session,lastEffect:o.effect,effectSeq:t.effectSeq+1}}if(e.input.length===0)return t;let r=t.session,n=t.lastEffect;for(let o of e.input){let u=Q(r,{type:"char",ch:o},e.now);if(r=u.session,n=u.effect,r.finishedAt!==null)break}return{session:r,lastEffect:n,effectSeq:t.effectSeq+1}}return t}function me(t){let e=[...t];if(e.some(n=>n.codePointAt(0)>=128))return{kind:"ime",cleaned:""};let r=e.filter(n=>{let o=n.codePointAt(0);return o>=32&&o<=126}).join("");return{kind:r.length>0?"valid":"noise",cleaned:r}}function Pt({playlist:t,onComplete:e,onTab:r,onEscape:n,onSkip:o,onImeBlock:u,onValidInput:s,enabled:a=!0}){let[l,b]=ce(de,void 0,()=>({session:J(t,Date.now()),lastEffect:null,effectSeq:0})),I=se(!1),[T,C]=ae(0),{exit:w}=le();return ue((f,g)=>{if(g.ctrl&&f==="c"){w();return}if(g.ctrl&&f==="n"){o?.(),b({type:"skip",now:Date.now()});return}if(g.escape){n?.();return}if(g.tab){r?.();return}if(g.upArrow||g.downArrow||g.leftArrow||g.rightArrow||g.return||g.ctrl||g.meta)return;let{kind:M,cleaned:B}=me(f);if(M==="ime"){u?.();return}M!=="noise"&&(s?.(),b({type:"event",input:B,key:g,now:Date.now()}))},{isActive:a}),Bt(()=>{l.session.finishedAt!==null&&!I.current&&(I.current=!0,e(l.session))},[l.session,e]),Bt(()=>{if(l.session.finishedAt!==null)return;let f=setInterval(()=>C(g=>g+1),1e3);return()=>clearInterval(f)},[l.session.finishedAt]),{session:l.session,lastEffect:l.lastEffect,effectSeq:l.effectSeq,tick:T}}import{useEffect as pe,useRef as fe}from"react";function vt(t){let e=fe(!1);return pe(()=>{e.current||(e.current=!0,lt(!t.enabled).catch(()=>{}))},[t.enabled]),{keystroke:()=>t.enabled&&dt(),correct:()=>t.enabled&&mt(),wrong:()=>t.enabled&&pt(),pronounce:r=>{t.enabled&&t.autoplayPronunciation&&ft(r,t.accent,t.pronunciationRate,t.pronunciationSource)},prefetch:r=>{t.enabled&&ht(r,t.accent,t.pronunciationSource)}}}import{useCallback as he}from"react";function $t(t){return he(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 wt(r),It({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(([,u])=>u>0);if(n.length===0)return;let o=await V();for(let[u,s]of n)o=St(o,u,t.dictId,s);await Tt(o)},[t.dictId,t.chapterIndex,t.mode])}function Rt(t=process.env){return t.KITTY_WINDOW_ID?{supportsDoubleHeight:!1}:t.VTE_VERSION?{supportsDoubleHeight:!1}:t.KONSOLE_VERSION?{supportsDoubleHeight:!1}:t.WT_SESSION?{supportsDoubleHeight:!0}:t.TERM_PROGRAM==="Apple_Terminal"?{supportsDoubleHeight:!0}:t.TERM_PROGRAM==="iTerm.app"?{supportsDoubleHeight:!0}:t.TERM_PROGRAM==="WezTerm"?{supportsDoubleHeight:!0}:t.TERM_PROGRAM==="mintty"?{supportsDoubleHeight:!0}:t.ALACRITTY_SOCKET||t.ALACRITTY_LOG?{supportsDoubleHeight:!0}:t.XTERM_VERSION?{supportsDoubleHeight:!0}:{supportsDoubleHeight:!1}}import{Box as ge,Text as Dt,useStdout as xe}from"ink";import{jsx as Lt,jsxs as we}from"react/jsx-runtime";var Nt=new Map;function be(t){let e=Nt.get(t);if(e)return e;let r=parseInt(t.slice(1,3),16),n=parseInt(t.slice(3,5),16),o=parseInt(t.slice(5,7),16),u=`\x1B[1;38;2;${r};${n};${o}m`;return Nt.set(t,u),u}var ye="\x1B[0m";function _t({target:t,typed:e,error:r=!1,hideTarget:n=!1}){let{stdout:o}=xe(),u=o?.columns??80,s=[...t],a=[...e],l=s.length*2,b=Math.max(0,Math.floor((u-l)/2)),I=" ".repeat(Math.floor(b/2)),T=s.map((f,g)=>{let M=g<a.length,B=n&&!M?"_":M?a[g]:f,v=r?c.error:M?c.accent:c.muted;return`${be(v)}${B}${ye}`}).join(""),C=`\x1B#3${I}${T}`,w=`\x1B#4${I}${T}`;return we(ge,{flexDirection:"column",paddingY:3,width:"100%",children:[Lt(Dt,{children:C}),Lt(Dt,{children:w})]})}import{jsx as Ht}from"react/jsx-runtime";var tt=null;function Te(){return tt||(tt=Rt()),tt}function Ot(t){let{cfg:e}=O(),r=process.stdout?.columns??80,n=Te(),o=e.wordDisplay==="huge"||e.wordDisplay==="auto"&&n.supportsDoubleHeight,u=r>=2*[...t.target].length+4;return o&&u?Ht(_t,{...t}):Ht(bt,{...t})}import{Box as N,Text as m,useStdout as Se}from"ink";import{Fragment as Ce,jsx as d,jsxs as P}from"react/jsx-runtime";var jt=28;function qt(t){let e=Math.floor(t/1e3),r=Math.floor(e/60),n=e%60;return`${r}:${String(n).padStart(2,"0")}`}function Ie(){let{stdout:t}=Se(),e=t?.columns??80;return Math.max(20,e-jt)}function A({left:t,right:e}){let r=Ie();return P(N,{children:[d(N,{width:r,children:t}),d(N,{width:jt,justifyContent:"flex-end",children:e})]})}function Vt(t){let e=W(),r=[...t.target],n=[...t.typed],o=P(N,{children:[r.map((C,w)=>{let f=w<n.length,g=t.hideTarget&&!f?"_":f?n[w]:C,M=t.error?c.error:f?c.accent:c.muted;return d(m,{bold:!0,color:M,children:g},w)}),t.phonetic&&P(Ce,{children:[d(m,{children:" "}),d(m,{italic:!0,dimColor:!0,color:c.muted,children:t.phonetic})]})]}),u=t.translation.length>0?d(m,{color:c.primary,children:t.translation[0]}):d(m,{children:" "}),s=t.info,a=Number.isInteger(s.accPct)?`${s.accPct}`:s.accPct.toFixed(1),l=!t.imeBlocked&&t.audioWarning!==null,b=t.imeBlocked?d(m,{color:c.warning,children:e.practice.imeWarningShort}):l?d(m,{color:c.warning,children:e.practice.audioWarningShort}):s.visible?d(m,{color:c.muted,children:`${s.dictName} \xB7 ${s.chapterLabel}`}):d(m,{children:" "}),I=t.imeBlocked||l?d(m,{children:" "}):s.visible?d(m,{color:c.muted,children:`${s.completed}/${s.total} \xB7 ${s.wpm}wpm \xB7 ${a}%`}):d(m,{children:" "}),T=t.imeBlocked||l?d(m,{children:" "}):s.visible?d(m,{color:c.muted,children:qt(s.elapsedMs)}):d(m,{children:" "});return P(N,{flexDirection:"column",children:[d(A,{left:o,right:b}),d(A,{left:d(m,{children:" "}),right:I}),d(A,{left:u,right:T})]})}function Gt(){let t=W();return P(N,{flexDirection:"column",children:[d(A,{left:d(m,{color:c.warning,children:t.stealth.paused}),right:d(m,{color:c.muted,children:t.stealth.pausedHintRight})}),d(A,{left:d(m,{children:" "}),right:P(m,{color:c.muted,children:["Esc ",t.common.back]})}),d(A,{left:d(m,{children:" "}),right:d(m,{children:" "})})]})}function Ft(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 ${qt(t.durationMs)}`;return P(N,{flexDirection:"column",children:[d(A,{left:d(m,{color:c.success,children:n}),right:d(m,{color:c.muted,children:e.stealth.nextHintRight})}),d(A,{left:d(m,{children:" "}),right:P(m,{color:c.muted,children:["Esc ",e.common.back]})}),d(A,{left:d(m,{children:" "}),right:d(m,{children:" "})})]})}import{jsx as i,jsxs as E}from"react/jsx-runtime";function $r({params:t}){let{dictId:e,chapterIndex:r,mode:n}=t,{cfg:o}=O(),u=W(),[s,a]=H("loading"),[l,b]=H(null),[I,T]=H(null);return K(()=>{let C=!1;return a("loading"),b(null),T(null),(async()=>{try{let w=await ut(e);if(C)return;if(n==="review"){let B=await V();if(C)return;let v=w.filter(q=>B[q.name]?.count).slice(0,o.chapterSize);if(v.length===0){T(u.practice.errors.noMistakes),a("error");return}b({playlist:v,totalChapters:1}),a("typing");return}let f=kt(w,o.chapterSize);if(f.length===0){T(u.practice.errors.dictEmpty(e)),a("error");return}let g=Math.max(0,Math.min(f.length-1,r)),M=Et(f[g],n);b({playlist:M,totalChapters:f.length}),a("typing")}catch(w){if(C)return;T(w.message),a("error")}})(),()=>{C=!0}},[e,r,n,o.chapterSize,u]),s==="loading"?i(ve,{text:u.practice.loading,color:c.muted}):s==="error"?i(Be,{msg:I??u.practice.errors.unknown}):l?i(ke,{params:t,loaded:l,phase:s,setPhase:a},`${e}-${r}-${n}-${t.stealth?"s":"n"}`):null}function ke({params:t,loaded:e,phase:r,setPhase:n}){let{dictId:o,chapterIndex:u,mode:s}=t,a=t.stealth===!0,{cfg:l}=O(),b=X(),{exit:I}=Me(),T=()=>b.stack.length>1?b.back():I(),C=$t({dictId:o,chapterIndex:u,mode:s}),w=xt(o),f=vt({enabled:!a&&l.sounds.master,accent:l.accent,autoplayPronunciation:!a&&l.autoplayPronunciation,pronunciationRate:l.sounds.pronunciationRate,pronunciationSource:l.sounds.pronunciationSource}),g=gt(),M=l.sounds.master?g.warning:null,B=et(!1),v=et(0),q=et(-1),[zt,nt]=H(!1),[ot,Ut]=H(null),[it,ct]=H(!1);K(()=>{if(ot===null)return;let h=setTimeout(()=>nt(!1),2e3);return()=>clearTimeout(h)},[ot]);let{session:S,lastEffect:$,effectSeq:Y,tick:Xt}=Pt({playlist:e.playlist,enabled:r==="typing",onComplete:h=>{B.current||(B.current=!0,n("summary"),Promise.resolve(C(Z(h))).catch(y=>{console.error("Failed to persist session:",y)}))},onEscape:()=>n(r==="paused"?"typing":"paused"),onTab:a?void 0:()=>{let h=S.current?e.playlist[S.current.wordIndex]:void 0;h&&f.pronounce(h.name)},onImeBlock:()=>ct(!0),onValidInput:()=>ct(!1)});K(()=>{a||Y!==v.current&&(v.current=Y,$!==null&&($==="wrong"&&l.sounds.feedback&&f.wrong(),$==="progress"&&l.sounds.keystroke&&f.keystroke(),$==="correct"&&(l.sounds.feedback&&f.correct(),l.sounds.keystroke&&f.keystroke())))},[a,Y,$,f,l.sounds.feedback,l.sounds.keystroke]),K(()=>{if(a)return;let h=S.current?.wordIndex??-1;if(h===-1||h===q.current)return;q.current=h;let y=e.playlist[h],D=e.playlist[h+1];y&&l.autoplayPronunciation&&f.pronounce(y.name),D&&f.prefetch(D.name)},[a,S.current?.wordIndex,f,l.autoplayPronunciation,e.playlist]),j((h,y)=>{if(y.tab){nt(!0),Ut(Date.now());return}},{isActive:a&&r==="typing"}),j((h,y)=>{if(y.return){n("typing");return}if(y.escape){T();return}},{isActive:r==="paused"}),j((h,y)=>{y.ctrl&&h==="c"&&(yt(!0),I())},{isActive:a&&r==="paused"}),j((h,y)=>{if(y.escape){T();return}if(y.return){let D=u+1;s==="loop"?b.replace({name:"practice",params:{dictId:o,chapterIndex:u,mode:s,stealth:t.stealth}}):s==="review"||D>=e.totalChapters?T():b.replace({name:"practice",params:{dictId:o,chapterIndex:D,mode:s,stealth:t.stealth}});return}if(h==="m"){b.replace({name:"practice",params:{dictId:o,chapterIndex:0,mode:"review",stealth:t.stealth}});return}},{isActive:r==="summary"});let R=S.results.length,Jt=S.results.reduce((h,y)=>h+y.errors,0),z=Date.now()-S.startedAt,st=z/6e4,at=st>0?Math.round(R/st*10)/10:0,k=r==="summary"?Z(S):null;if(a){if(r==="paused")return i(Gt,{});if(r==="summary"&&k){let L=k.durationMs/6e4,re=L>0?Math.round(k.wordCount/L*10)/10:0,ne=Object.keys(k.perWordErrors).length,oe=k.wordCount===0?1:Math.max(0,(k.wordCount-ne)/k.wordCount),ie=Math.round(oe*1e3)/10;return i(Ft,{wordCount:k.wordCount,errors:k.errors,durationMs:k.durationMs,wpm:re,accPct:ie})}let h=S.current?e.playlist[S.current.wordIndex]:e.playlist[e.playlist.length-1],y=S.current?.input??{target:"",typed:"",errorsThisWord:0},D=new Set(S.results.filter(L=>L.errors>0).map(L=>L.word)).size,Zt=R===0?1:Math.max(0,(R-D)/R),te=Math.round(Zt*1e3)/10,ee=s==="review"?"review":`ch ${u+1}/${e.totalChapters}`;return i(Vt,{target:h?.name??"",typed:y.typed,hideTarget:s==="dictation",phonetic:Kt(h,l.accent),translation:h?.trans??[],error:$==="wrong",imeBlocked:it,audioWarning:M,info:{visible:zt,dictName:_(w,24),chapterLabel:ee,completed:R,total:e.playlist.length,wpm:at,accPct:te,elapsedMs:z}})}if(r==="paused")return i(Ae,{dictName:w,chapterIndex:u,totalChapters:e.totalChapters,mode:s,completed:R,total:e.playlist.length});if(r==="summary"&&k)return i($e,{dictName:w,chapterIndex:u,totalChapters:e.totalChapters,mode:s,summary:k});let U=S.current?e.playlist[S.current.wordIndex]:e.playlist[e.playlist.length-1],Qt=S.current?.input??{target:"",typed:"",errorsThisWord:0};return i(Ee,{dictName:w,chapterIndex:u,totalChapters:e.totalChapters,mode:s,accent:l.accent,completed:R,total:e.playlist.length,errors:Jt,wpm:at,elapsedMs:z,target:U?.name??"",typed:Qt.typed,flashError:$==="wrong",hideTarget:s==="dictation",phonetic:Kt(U,l.accent),translation:U?.trans??[],imeBlocked:it,audioWarning:M})}function Kt(t,e){return t?(e==="us"?t.usphone:t.ukphone)??null:null}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 Ee(t){let e=W(),r=t.total===0?0:t.completed/t.total;return E(p,{flexDirection:"column",paddingX:2,paddingY:1,width:"100%",height:"100%",children:[i(We,{dictName:t.dictName,chapterIndex:t.chapterIndex,totalChapters:t.totalChapters,mode:t.mode,accent:t.accent,completed:t.completed,total:t.total,elapsedMs:t.elapsedMs}),E(p,{flexGrow:1,flexDirection:"column",alignItems:"center",justifyContent:"center",children:[i(Ot,{target:t.target,typed:t.typed,error:t.flashError,hideTarget:t.hideTarget}),t.phonetic&&i(p,{marginTop:1,children:i(x,{italic:!0,dimColor:!0,color:c.muted,children:t.phonetic})}),t.translation.length>0&&i(p,{marginTop:1,flexDirection:"column",alignItems:"center",children:t.translation.slice(0,2).map((n,o)=>i(x,{color:c.primary,children:n},o))}),t.imeBlocked&&i(p,{marginTop:1,children:i(x,{color:c.warning,children:e.practice.imeWarning})}),!t.imeBlocked&&t.audioWarning&&i(p,{marginTop:1,children:i(x,{color:c.warning,children:t.audioWarning})})]}),E(p,{flexDirection:"column",children:[i(Yt,{frac:r}),i(p,{justifyContent:"center",marginTop:1,children:E(x,{color:c.muted,children:[t.completed,"/",t.total," \xB7 ",rt(t.elapsedMs)," \xB7 ",t.wpm," ",e.practice.statCards.wpm," \xB7 ",t.errors," ",e.practice.statCards.errors]})}),i(p,{justifyContent:"center",marginTop:1,children:i(x,{color:c.muted,children:e.practice.footers.typing})})]})]})}function We(t){let e=W(),r=e.practice.modes[t.mode],n=e.practice.accents[t.accent],o=_(t.dictName,20),u=t.mode==="review"?`${o} \xB7 ${e.practice.reviewLabel} \xB7 ${n}`:`${o} \xB7 ${e.practice.chapterLabel(t.chapterIndex+1,t.totalChapters)} \xB7 ${r} \xB7 ${n}`,s=`${t.completed}/${t.total} \xB7 ${rt(t.elapsedMs)}`;return E(p,{children:[i(x,{color:c.muted,children:u}),i(p,{flexGrow:1}),i(x,{color:c.muted,children:s})]})}function Yt({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))),o=r-n;return E(p,{justifyContent:"center",children:[i(x,{color:c.accent,children:"\u2501".repeat(n)}),i(x,{color:c.muted,children:"\u2500".repeat(o)})]})}function Ae(t){let e=W(),r=t.total===0?0:t.completed/t.total,n=t.mode==="review"?`${_(t.dictName,20)} \xB7 ${e.practice.reviewLabel}`:`${_(t.dictName,20)} \xB7 ${e.practice.pause.chapter(t.chapterIndex+1,t.totalChapters)}`;return E(p,{flexDirection:"column",alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:[i(x,{bold:!0,color:c.warning,children:e.practice.pause.title}),i(p,{marginTop:1,children:i(x,{color:c.muted,children:n})}),i(p,{marginTop:2,children:i(Yt,{frac:r})}),i(p,{marginTop:1,children:i(x,{color:c.muted,children:e.practice.pause.progress(t.completed,t.total)})}),i(p,{marginTop:2,children:i(x,{color:c.muted,children:e.practice.pause.hint})})]})}function Be({msg:t}){let e=W();return E(p,{flexDirection:"column",alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:[i(x,{color:c.error,children:t}),i(p,{marginTop:2,children:E(x,{color:c.muted,children:["Esc ",e.common.back]})}),i(Pe,{})]})}function Pe(){let t=X();return j((e,r)=>{r.escape&&t.back()}),null}function ve({text:t,color:e}){return i(p,{alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:i(x,{color:e,children:t})})}function $e(t){let{summary:e}=t,r=e.durationMs/6e4,n=r>0?Math.round(e.wordCount/r*10)/10:0,o=Object.keys(e.perWordErrors).length,u=e.wordCount===0?1:Math.max(0,(e.wordCount-o)/e.wordCount),s=Math.round(u*1e3)/10,a=W(),l=a.practice.modes[t.mode],b=_(t.dictName,20),I=t.mode==="review"?`${b} \xB7 ${a.practice.reviewLabel}`:`${b} \xB7 ${a.practice.chapterLabel(t.chapterIndex+1,t.totalChapters)} \xB7 ${l}`,C=`Enter ${t.mode==="loop"?a.practice.summary.loopAgain:t.mode==="review"||t.chapterIndex+1>=t.totalChapters?a.practice.summary.backMenu:a.practice.summary.nextChapter} \xB7 m ${a.practice.summary.reviewMistakes} \xB7 Esc ${a.practice.summary.backMenu}`;return E(p,{flexDirection:"column",alignItems:"center",justifyContent:"center",paddingY:1,width:"100%",height:"100%",children:[i(x,{bold:!0,color:c.success,children:a.practice.chapterComplete}),i(p,{marginTop:1,children:i(x,{color:c.muted,children:I})}),E(p,{marginTop:3,flexDirection:"row",justifyContent:"center",children:[i(F,{label:a.practice.statCards.words,value:String(e.wordCount),color:c.text}),i(F,{label:a.practice.statCards.errors,value:String(e.errors),color:e.errors>0?c.error:c.muted}),i(F,{label:a.practice.statCards.wpm,value:String(n),color:c.accent}),i(F,{label:a.practice.statCards.accuracy,value:`${s}%`,color:c.accent})]}),i(p,{marginTop:2,children:i(x,{color:c.muted,children:a.practice.statCards.elapsed(rt(e.durationMs))})}),i(p,{flexGrow:1}),i(p,{marginTop:2,children:i(x,{color:c.muted,children:C})})]})}function F({label:t,value:e,color:r}){return E(p,{flexDirection:"column",alignItems:"center",marginX:3,children:[i(x,{bold:!0,color:r,children:e}),i(x,{color:c.muted,children:t})]})}export{$r as PracticeScreen};
2
- //# sourceMappingURL=PracticeScreen-PXPBME6H.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/util/term-caps.ts","../src/ui/components/BigWordHuge.tsx","../src/ui/components/BigWordAuto.tsx","../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 { PALETTE } from '../components/BigWord.js';\nimport { BigWordAuto } from '../components/BigWordAuto.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 <BigWordAuto\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","// Detect terminal capabilities relevant to BigWord rendering.\n//\n// VT100 DECDWL/DECDHL (ESC#3 / ESC#4 / ESC#6) let us render the practice\n// word using the terminal's real font at 2× width and 2× height. Support\n// varies: macOS Terminal.app, Windows Terminal, iTerm2, WezTerm, xterm,\n// mintty, Alacritty render correctly; kitty / VTE-based / Konsole either\n// skip the attribute or render glitchy output.\n//\n// Detection is env-only — runtime device-attribute queries (ESC[c) would\n// race against Ink's input pipeline and aren't worth the complexity.\n\nexport type TermCaps = {\n supportsDoubleHeight: boolean;\n};\n\nexport function detectTermCaps(env: NodeJS.ProcessEnv = process.env): TermCaps {\n if (env.KITTY_WINDOW_ID) return { supportsDoubleHeight: false };\n if (env.VTE_VERSION) return { supportsDoubleHeight: false };\n if (env.KONSOLE_VERSION) return { supportsDoubleHeight: false };\n\n if (env.WT_SESSION) return { supportsDoubleHeight: true };\n if (env.TERM_PROGRAM === 'Apple_Terminal') return { supportsDoubleHeight: true };\n if (env.TERM_PROGRAM === 'iTerm.app') return { supportsDoubleHeight: true };\n if (env.TERM_PROGRAM === 'WezTerm') return { supportsDoubleHeight: true };\n if (env.TERM_PROGRAM === 'mintty') return { supportsDoubleHeight: true };\n if (env.ALACRITTY_SOCKET || env.ALACRITTY_LOG) return { supportsDoubleHeight: true };\n if (env.XTERM_VERSION) return { supportsDoubleHeight: true };\n\n return { supportsDoubleHeight: false };\n}\n","import { Box, Text, useStdout } from 'ink';\nimport { PALETTE } from './BigWord.js';\n\n// Renders the practice word using VT100 DECDHL (double-height + double-width\n// line attribute). The terminal draws the SAME glyphs as a single-cell line\n// but at 2× scale — real terminal font, no rasterization, no figlet/ASCII art.\n//\n// DECDHL requires two physical rows with identical content: ESC#3 for the\n// top half, ESC#4 for the bottom half. The terminal merges them into one\n// visual 2×-tall glyph row. So we emit 2 Ink rows here.\n//\n// Width math:\n// each character occupies 2 visual cells under DECDHL\n// padding chars are also doubled (DECDHL is a line attribute, applies to all)\n// so actual_pad_chars = visual_pad / 2\n//\n// Ink's width measurement (via string-width) strips the ESC#3 prefix to 0\n// columns but counts the raw spaces of `padding`; we don't constrain the\n// container width so Ink's miscount is harmless.\n\ntype Props = {\n target: string;\n typed: string;\n error?: boolean;\n hideTarget?: boolean;\n};\n\nconst SGR_CACHE = new Map<string, string>();\n\nfunction sgrBold(hex: string): string {\n const cached = SGR_CACHE.get(hex);\n if (cached) return cached;\n const r = parseInt(hex.slice(1, 3), 16);\n const g = parseInt(hex.slice(3, 5), 16);\n const b = parseInt(hex.slice(5, 7), 16);\n const sgr = `\\x1b[1;38;2;${r};${g};${b}m`;\n SGR_CACHE.set(hex, sgr);\n return sgr;\n}\n\nconst RESET = '\\x1b[0m';\n\nexport function BigWordHuge({ target, typed, error = false, hideTarget = false }: Props) {\n const { stdout } = useStdout();\n const cols = stdout?.columns ?? 80;\n const chars = [...target];\n const typedChars = [...typed];\n\n const visualWidth = chars.length * 2;\n const visualPad = Math.max(0, Math.floor((cols - visualWidth) / 2));\n const actualPad = ' '.repeat(Math.floor(visualPad / 2));\n\n const body = chars\n .map((ch, i) => {\n const isTyped = i < typedChars.length;\n const display = hideTarget && !isTyped ? '_' : isTyped ? typedChars[i]! : ch;\n const color = error ? PALETTE.error : isTyped ? PALETTE.accent : PALETTE.muted;\n return `${sgrBold(color)}${display}${RESET}`;\n })\n .join('');\n\n const topLine = `\\x1b#3${actualPad}${body}`;\n const bottomLine = `\\x1b#4${actualPad}${body}`;\n\n return (\n <Box flexDirection=\"column\" paddingY={3} width=\"100%\">\n <Text>{topLine}</Text>\n <Text>{bottomLine}</Text>\n </Box>\n );\n}\n","import { useAppState } from '../app-state.js';\nimport { detectTermCaps, type TermCaps } from '../../util/term-caps.js';\nimport { BigWord } from './BigWord.js';\nimport { BigWordHuge } from './BigWordHuge.js';\n\ntype Props = {\n target: string;\n typed: string;\n error?: boolean;\n hideTarget?: boolean;\n};\n\nlet cachedCaps: TermCaps | null = null;\nfunction getCaps(): TermCaps {\n if (!cachedCaps) cachedCaps = detectTermCaps();\n return cachedCaps;\n}\n\nexport function BigWordAuto(props: Props) {\n const { cfg } = useAppState();\n const cols = process.stdout?.columns ?? 80;\n const caps = getCaps();\n\n const wantsHuge =\n cfg.wordDisplay === 'huge' ||\n (cfg.wordDisplay === 'auto' && caps.supportsDoubleHeight);\n const fits = cols >= 2 * [...props.target].length + 4;\n\n return wantsHuge && fits ? <BigWordHuge {...props} /> : <BigWord {...props} />;\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","c","classifyInputBatch","input","chars","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","detectTermCaps","env","Box","Text","useStdout","jsx","jsxs","SGR_CACHE","sgrBold","hex","cached","g","b","sgr","RESET","BigWordHuge","target","typed","error","hideTarget","stdout","useStdout","cols","chars","typedChars","visualWidth","visualPad","actualPad","body","ch","i","isTyped","display","color","PALETTE","topLine","bottomLine","Box","Text","jsx","cachedCaps","getCaps","detectTermCaps","BigWordAuto","props","cfg","useAppState","cols","caps","wantsHuge","fits","BigWordHuge","BigWord","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","BigWordAuto","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"]}
@@ -1,2 +0,0 @@
1
- import{a as j,d as F}from"./chunk-G3DQB7FI.js";import{b as N,c as $,d as O,f as V,h as Y}from"./chunk-GULN5HRV.js";import{b as B,d as k,e as y}from"./chunk-QG7ZTS2G.js";import{b as R,d as C,f as o}from"./chunk-VIOZNKSK.js";import"./chunk-NA5UNUVL.js";import"./chunk-E6BBQALJ.js";import{useEffect as G,useState as D}from"react";import{Box as i,Text as r,useInput as U,useStdout as q}from"ink";import{jsx as e,jsxs as c}from"react/jsx-runtime";var W=[7,14,30,90];function it(){let s=R(),t=C(),[n,u]=D(null),[x,g]=D(null),[d,b]=D(1);if(G(()=>{(async()=>{let[l,a]=await Promise.all([N(),j()]);u(l),g(a)})()},[]),U((l,a)=>{if(a.escape){s.back();return}a.rightArrow&&b(M=>(M+1)%W.length),a.leftArrow&&b(M=>(M-1+W.length)%W.length)}),!n||!x)return e(i,{alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:e(r,{color:o.muted,children:t.stats.loading})});if(n.length===0)return c(i,{flexDirection:"column",alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:[e(r,{color:o.muted,children:t.stats.none}),e(i,{marginTop:1,children:e(r,{color:o.muted,children:t.stats.nonePractice})}),e(i,{marginTop:2,children:c(r,{color:o.muted,children:["Esc ",t.common.back]})})]});let m=W[d],S=V(n),f=n.reduce((l,a)=>l+a.wordCount,0),w=n.reduce((l,a)=>l+a.errors,0),T=n.reduce((l,a)=>l+a.durationMs,0),v=n.reduce((l,a)=>l+(a.wordCount-Object.keys(a.perWordErrors).length),0),H=T>0?Math.round(f/(T/6e4)*10)/10:0,X=f===0?1:v/f,_=n.slice(-5).reverse(),I=F(x,8),p=Y(n,m),A=p.peakWpmLifetime>0?Math.min(1,p.avgWpm/p.peakWpmLifetime):0,L=Math.min(1,p.avgAccPct/100),P=m>0?Math.min(1,p.sessionsInWindow/m):0;return c(i,{flexDirection:"column",paddingX:2,paddingY:1,width:"100%",height:"100%",children:[e(r,{bold:!0,color:o.accent,children:t.stats.title}),c(i,{marginTop:1,flexDirection:"column",children:[e(r,{color:o.muted,children:t.stats.lifetime}),c(i,{marginTop:1,children:[e(h,{label:t.stats.sessions,value:String(n.length)}),e(h,{label:t.stats.words,value:String(f)}),e(h,{label:t.stats.errors,value:String(w)}),e(h,{label:t.stats.wpm,value:String(H),accent:!0}),e(h,{label:t.stats.accuracy,value:`${Math.round(X*1e3)/10}%`,accent:!0}),e(h,{label:t.stats.streak,value:`${S}d`,accent:!0})]})]}),c(i,{marginTop:2,flexDirection:"column",children:[e(r,{color:o.muted,children:t.stats.last(m)}),c(i,{marginTop:1,flexDirection:"column",borderStyle:"round",borderColor:o.muted,paddingX:2,paddingY:0,children:[e(E,{label:t.stats.bars.speed,frac:A,valueText:`${p.avgWpm} wpm`,pct:Math.round(A*100)}),e(E,{label:t.stats.bars.accuracy,frac:L,valueText:`${p.avgAccPct}%`,pct:Math.round(L*100)}),e(E,{label:t.stats.bars.sessions,frac:P,valueText:`${p.sessionsInWindow} / ${m}`,pct:Math.round(P*100)})]})]}),c(i,{marginTop:2,flexDirection:"column",children:[e(r,{color:o.muted,children:t.stats.recent}),_.map((l,a)=>e(z,{session:l,units:t.stats.recentUnits},a))]}),I.length>0&&c(i,{marginTop:2,flexDirection:"column",children:[e(r,{color:o.muted,children:t.stats.topMistakes}),I.map(([l,a])=>e(J,{word:l,count:a.count,dictIds:a.dictIds,multiSuffix:t.stats.multiDictSuffix},l))]}),e(i,{flexGrow:1}),e(i,{marginTop:1,children:e(r,{color:o.muted,children:t.stats.footer})})]})}function z({session:s,units:t}){let n=B(s.dictId),u=y(n,14);return c(i,{children:[c(r,{color:o.muted,children:[" ",s.ts.replace("T"," ").slice(0,16)," "]}),e(r,{color:o.text,children:u.padEnd(14)}),c(r,{color:o.muted,children:[" ","ch",String(s.chapter+1).padStart(3)," ",s.mode.padEnd(9)," ",String(s.wordCount).padStart(3),t.words," ",s.errors,t.errors," ",$(s),t.wpm," ",Math.round(O(s)*1e3)/10,"%"]})]})}function J({word:s,count:t,dictIds:n,multiSuffix:u}){let x=n[0]??"",g=B(x),d=n.length>1?u(n.length-1):"";return c(i,{children:[c(r,{color:o.error,children:[" ",String(t).padStart(3)," "]}),e(r,{color:o.text,children:s.padEnd(20)}),c(r,{color:o.muted,children:[y(g,20),d]})]})}function h({label:s,value:t,accent:n=!1}){return c(i,{marginRight:3,children:[c(r,{color:o.muted,children:[s," "]}),e(r,{bold:!0,color:n?o.accent:o.text,children:t})]})}function K(s){return{barWidth:Math.max(0,Math.min(60,s-11-24)),labelWidth:9,valueWidth:8,pctWidth:5,marginLeft:2}}function E({label:s,frac:t,valueText:n,pct:u}){let{stdout:x}=q(),g=x?.columns??80,d=K(g),b=Math.max(0,Math.min(1,t)),m=Math.round(d.barWidth*b),S=d.barWidth-m,f=k(s,d.labelWidth),w=k(n,d.valueWidth),T=String(u).padStart(d.pctWidth-1," ")+"%",v=" ".repeat(d.marginLeft);return c(i,{children:[e(r,{color:o.muted,children:f}),e(r,{color:o.accent,children:"\u2501".repeat(m)}),e(r,{color:o.muted,children:"\u2500".repeat(S)}),c(r,{color:o.text,children:[v,w]}),e(r,{color:o.muted,children:T})]})}export{it as StatsViewer,K as calcMetricBarLayout};
2
- //# sourceMappingURL=StatsViewer-JIFWFPDL.js.map
@@ -1,2 +0,0 @@
1
- import{c as b}from"./chunk-EBAA2ZKH.js";import"./chunk-VTIB5Q36.js";import{a as M}from"./chunk-G3DQB7FI.js";import{b as B,e as h}from"./chunk-QG7ZTS2G.js";import{b as L,d as g,f as t}from"./chunk-VIOZNKSK.js";import"./chunk-NA5UNUVL.js";import{a as I}from"./chunk-E6BBQALJ.js";import{useEffect as E,useState as u}from"react";import{Box as n,Text as r,useInput as S}from"ink";import{readdir as $}from"fs/promises";import{Fragment as G,jsx as o,jsxs as d}from"react/jsx-runtime";async function N(){try{return(await $(I.dictsDir)).filter(e=>e.endsWith(".json")&&!e.endsWith(".meta.json")).map(e=>e.replace(/\.json$/,""))}catch{return[]}}function O(){let i=L(),e=g(),[m,x]=u(""),[l,w]=u([]),[W,C]=u({}),[H,j]=u(!0),[k,f]=u(0);E(()=>{(async()=>{let a=await N(),c=[];for(let s of a){let y=await b(s);if(y)for(let A of y)c.push({dictId:s,word:A})}w(c),C(await M()),j(!1)})()},[]);let T=m.toLowerCase().trim(),p=T?l.filter(a=>a.word.name.toLowerCase().includes(T)).slice(0,50):[];if(S((a,c)=>{if(c.escape){i.back();return}if(c.upArrow){f(s=>Math.max(0,s-1));return}if(c.downArrow){f(s=>Math.min(p.length-1,s+1));return}if(c.backspace||c.delete){x(s=>s.slice(0,-1)),f(0);return}a&&!c.ctrl&&!c.meta&&a.trim().length>0&&(x(s=>s+a),f(0))}),H)return o(n,{alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:o(r,{color:t.muted,children:e.word.indexing})});if(l.length===0)return d(n,{flexDirection:"column",alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:[o(r,{color:t.muted,children:e.word.none}),o(n,{marginTop:1,children:o(r,{color:t.muted,children:e.word.pullFirst})}),o(n,{marginTop:2,children:d(r,{color:t.muted,children:["[Esc] ",e.common.back]})})]});let D=p[k];return d(n,{flexDirection:"column",paddingX:2,paddingY:1,width:"100%",height:"100%",children:[d(n,{children:[o(r,{bold:!0,color:t.accent,children:e.word.title}),o(n,{flexGrow:1}),o(r,{color:t.muted,children:e.word.countAcross(l.length)})]}),d(n,{marginTop:1,children:[o(r,{color:t.muted,children:"> "}),o(r,{color:t.text,children:m}),o(r,{color:t.accent,children:"_"})]}),d(n,{marginTop:1,flexGrow:1,children:[d(n,{flexDirection:"column",width:"40%",children:[p.map((a,c)=>o(q,{hit:a,active:c===k},`${a.dictId}-${a.word.name}-${c}`)),p.length===0&&T&&o(r,{color:t.muted,children:e.word.noMatches(m)})]}),o(n,{flexDirection:"column",width:"60%",paddingLeft:2,children:D&&o(v,{hit:D,book:W})})]}),o(n,{marginTop:1,children:o(r,{color:t.muted,children:e.word.footer})})]})}function q({hit:i,active:e}){let m=B(i.dictId);return d(n,{children:[o(r,{color:e?t.accent:t.muted,children:e?"\u258C ":" "}),o(r,{bold:e,color:e?t.text:t.muted,children:i.word.name.padEnd(20)}),o(r,{color:t.muted,children:h(m,18)})]})}function v({hit:i,book:e}){let m=g(),x=B(i.dictId);return d(G,{children:[o(r,{bold:!0,color:t.text,children:i.word.name}),d(n,{marginTop:1,children:[i.word.usphone&&d(r,{dimColor:!0,color:t.muted,children:["US ",i.word.usphone," "]}),i.word.ukphone&&d(r,{dimColor:!0,color:t.muted,children:["UK ",i.word.ukphone]})]}),o(n,{marginTop:1,flexDirection:"column",children:(i.word.trans??[]).map((l,w)=>d(r,{color:t.primary,children:["\xB7 ",l]},w))}),o(n,{marginTop:1,children:o(r,{color:t.muted,children:m.word.inDict(h(x,22))})}),e[i.word.name]&&o(n,{marginTop:1,children:o(r,{color:t.error,children:m.word.mistakes(e[i.word.name].count,e[i.word.name].lastSeen.slice(0,10))})})]})}export{O as WordLookup};
2
- //# sourceMappingURL=WordLookup-6JFXQYSQ.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/ui/screens/WordLookup.tsx"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport { Box, Text, useInput } from 'ink';\nimport { useNav } from '../nav.js';\nimport { useStrings } from '../../i18n/context.js';\nimport { useDictName } from '../registry-context.js';\nimport { PALETTE } from '../components/BigWord.js';\nimport { readdir } from 'node:fs/promises';\nimport { paths } from '../../infra/paths.js';\nimport { loadLocalDictionary } from '../../infra/dict-downloader.js';\nimport { loadMistakes, type MistakeBook } from '../../domain/mistakes.js';\nimport type { Word } from '../../domain/dictionary.js';\nimport { truncateName } from '../../util/dict-name.js';\n\ntype Hit = { dictId: string; word: Word };\n\nasync function listLocalDictIds(): Promise<string[]> {\n try {\n const files = await readdir(paths.dictsDir);\n return files\n .filter((f) => f.endsWith('.json') && !f.endsWith('.meta.json'))\n .map((f) => f.replace(/\\.json$/, ''));\n } catch {\n return [];\n }\n}\n\nexport function WordLookup() {\n const nav = useNav();\n const t = useStrings();\n const [query, setQuery] = useState('');\n const [allWords, setAllWords] = useState<Hit[]>([]);\n const [book, setBook] = useState<MistakeBook>({});\n const [loading, setLoading] = useState(true);\n const [selected, setSelected] = useState(0);\n\n useEffect(() => {\n void (async () => {\n const ids = await listLocalDictIds();\n const collected: Hit[] = [];\n for (const id of ids) {\n const words = await loadLocalDictionary(id);\n if (!words) continue;\n for (const w of words) collected.push({ dictId: id, word: w });\n }\n setAllWords(collected);\n setBook(await loadMistakes());\n setLoading(false);\n })();\n }, []);\n\n const q = query.toLowerCase().trim();\n const filtered = q\n ? allWords.filter((h) => h.word.name.toLowerCase().includes(q)).slice(0, 50)\n : [];\n\n useInput((input, key) => {\n if (key.escape) {\n nav.back();\n return;\n }\n if (key.upArrow) {\n setSelected((i) => Math.max(0, i - 1));\n return;\n }\n if (key.downArrow) {\n setSelected((i) => Math.min(filtered.length - 1, i + 1));\n return;\n }\n if (key.backspace || key.delete) {\n setQuery((s) => s.slice(0, -1));\n setSelected(0);\n return;\n }\n if (input && !key.ctrl && !key.meta && input.trim().length > 0) {\n setQuery((s) => s + input);\n setSelected(0);\n }\n });\n\n if (loading) {\n return (\n <Box alignItems=\"center\" justifyContent=\"center\" width=\"100%\" height=\"100%\">\n <Text color={PALETTE.muted}>{t.word.indexing}</Text>\n </Box>\n );\n }\n\n if (allWords.length === 0) {\n return (\n <Box flexDirection=\"column\" alignItems=\"center\" justifyContent=\"center\" width=\"100%\" height=\"100%\">\n <Text color={PALETTE.muted}>{t.word.none}</Text>\n <Box marginTop={1}>\n <Text color={PALETTE.muted}>{t.word.pullFirst}</Text>\n </Box>\n <Box marginTop={2}>\n <Text color={PALETTE.muted}>[Esc] {t.common.back}</Text>\n </Box>\n </Box>\n );\n }\n\n const current = filtered[selected];\n\n return (\n <Box flexDirection=\"column\" paddingX={2} paddingY={1} width=\"100%\" height=\"100%\">\n <Box>\n <Text bold color={PALETTE.accent}>{t.word.title}</Text>\n <Box flexGrow={1} />\n <Text color={PALETTE.muted}>{t.word.countAcross(allWords.length)}</Text>\n </Box>\n\n <Box marginTop={1}>\n <Text color={PALETTE.muted}>{'> '}</Text>\n <Text color={PALETTE.text}>{query}</Text>\n <Text color={PALETTE.accent}>_</Text>\n </Box>\n\n <Box marginTop={1} flexGrow={1}>\n <Box flexDirection=\"column\" width=\"40%\">\n {filtered.map((h, i) => (\n <HitRow key={`${h.dictId}-${h.word.name}-${i}`} hit={h} active={i === selected} />\n ))}\n {filtered.length === 0 && q && (\n <Text color={PALETTE.muted}>{t.word.noMatches(query)}</Text>\n )}\n </Box>\n\n <Box flexDirection=\"column\" width=\"60%\" paddingLeft={2}>\n {current && <Detail hit={current} book={book} />}\n </Box>\n </Box>\n\n <Box marginTop={1}>\n <Text color={PALETTE.muted}>{t.word.footer}</Text>\n </Box>\n </Box>\n );\n}\n\nfunction HitRow({ hit, active }: { hit: Hit; active: boolean }) {\n const name = useDictName(hit.dictId);\n return (\n <Box>\n <Text color={active ? PALETTE.accent : PALETTE.muted}>{active ? '▌ ' : ' '}</Text>\n <Text bold={active} color={active ? PALETTE.text : PALETTE.muted}>\n {hit.word.name.padEnd(20)}\n </Text>\n <Text color={PALETTE.muted}>{truncateName(name, 18)}</Text>\n </Box>\n );\n}\n\nfunction Detail({ hit, book }: { hit: Hit; book: MistakeBook }) {\n const t = useStrings();\n const name = useDictName(hit.dictId);\n return (\n <>\n <Text bold color={PALETTE.text}>{hit.word.name}</Text>\n <Box marginTop={1}>\n {hit.word.usphone && (\n <Text dimColor color={PALETTE.muted}>US {hit.word.usphone} </Text>\n )}\n {hit.word.ukphone && (\n <Text dimColor color={PALETTE.muted}>UK {hit.word.ukphone}</Text>\n )}\n </Box>\n <Box marginTop={1} flexDirection=\"column\">\n {(hit.word.trans ?? []).map((tr, i) => (\n <Text key={i} color={PALETTE.primary}>· {tr}</Text>\n ))}\n </Box>\n <Box marginTop={1}>\n <Text color={PALETTE.muted}>{t.word.inDict(truncateName(name, 22))}</Text>\n </Box>\n {book[hit.word.name] && (\n <Box marginTop={1}>\n <Text color={PALETTE.error}>\n {t.word.mistakes(book[hit.word.name]!.count, book[hit.word.name]!.lastSeen.slice(0, 10))}\n </Text>\n </Box>\n )}\n </>\n );\n}\n"],"mappings":"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","names":["useEffect","useState","Box","Text","useInput","readdir","Fragment","jsx","jsxs","listLocalDictIds","readdir","paths","f","WordLookup","nav","useNav","t","useStrings","query","setQuery","useState","allWords","setAllWords","book","setBook","loading","setLoading","selected","setSelected","useEffect","ids","collected","id","words","loadLocalDictionary","w","loadMistakes","q","filtered","h","useInput","input","key","i","Box","Text","PALETTE","current","HitRow","Detail","hit","active","name","useDictName","truncateName","tr"]}
@@ -1,2 +0,0 @@
1
- import{c as y}from"./chunk-NA5UNUVL.js";import{createContext as l,useContext as d,useEffect as p,useState as f}from"react";import{jsx as R}from"react/jsx-runtime";var g=l(null);function v({children:t}){let[e,r]=f(null),[o,n]=f(new Map);return p(()=>{let i=!1;return(async()=>{try{let s=await y();if(i)return;let u=new Map;for(let a of s)u.set(a.id,a);r(s),n(u)}catch{i||(r([]),n(new Map))}})(),()=>{i=!0}},[]),R(g.Provider,{value:{registry:e,byId:o},children:t})}function x(){let t=d(g);if(!t)throw new Error("useRegistry must be used inside RegistryProvider");return t}function C(t){let{byId:e}=x();return t?e.get(t)?.name??t:""}var b=/\x1b\[[0-9;]*m/g;function m(t){return t.replace(b,"")}function c(t){let e=m(t),r=0;for(let o of e){let n=o.codePointAt(0);r+=n>11904&&n<64256?2:1}return r}function M(t,e){return t+" ".repeat(Math.max(0,e-c(t)))}function I(t,e){if(c(t)<=e)return t;let r="",o=0;for(let n of t){let i=n.codePointAt(0),s=i>11904&&i<64256?2:1;if(o+s>e-1)break;r+=n,o+=s}return r+"\u2026"}export{v as a,C as b,c,M as d,I as e};
2
- //# sourceMappingURL=chunk-QG7ZTS2G.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/ui/registry-context.tsx","../src/util/text.ts","../src/util/dict-name.ts"],"sourcesContent":["import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';\nimport { loadRegistry } from '../infra/registry-store.js';\nimport type { Registry, DictionaryEntry } from '../domain/dictionary.js';\n\ntype RegistryContextValue = {\n registry: Registry | null;\n byId: Map<string, DictionaryEntry>;\n};\n\nconst RegistryContext = createContext<RegistryContextValue | null>(null);\n\nexport function RegistryProvider({ children }: { children: ReactNode }) {\n const [registry, setRegistry] = useState<Registry | null>(null);\n const [byId, setById] = useState<Map<string, DictionaryEntry>>(new Map());\n\n useEffect(() => {\n let cancelled = false;\n (async () => {\n try {\n const reg = await loadRegistry();\n if (cancelled) return;\n const map = new Map<string, DictionaryEntry>();\n for (const e of reg) map.set(e.id, e);\n setRegistry(reg);\n setById(map);\n } catch {\n if (!cancelled) {\n setRegistry([]);\n setById(new Map());\n }\n }\n })();\n return () => {\n cancelled = true;\n };\n }, []);\n\n return (\n <RegistryContext.Provider value={{ registry, byId }}>\n {children}\n </RegistryContext.Provider>\n );\n}\n\nexport function useRegistry(): RegistryContextValue {\n const ctx = useContext(RegistryContext);\n if (!ctx) throw new Error('useRegistry must be used inside RegistryProvider');\n return ctx;\n}\n\nexport function useDictName(id: string | undefined): string {\n const { byId } = useRegistry();\n if (!id) return '';\n const entry = byId.get(id);\n return entry?.name ?? id;\n}\n","const ANSI_RE = /\\x1b\\[[0-9;]*m/g;\n\nexport function stripAnsi(s: string): string {\n return s.replace(ANSI_RE, '');\n}\n\nexport function visibleWidth(s: string): number {\n const plain = stripAnsi(s);\n let w = 0;\n for (const ch of plain) {\n const code = ch.codePointAt(0)!;\n w += code > 0x2e80 && code < 0xfb00 ? 2 : 1;\n }\n return w;\n}\n\nexport function pad(s: string, n: number): string {\n return s + ' '.repeat(Math.max(0, n - visibleWidth(s)));\n}\n\nexport function truncate(s: string, max: number): string {\n if (visibleWidth(s) <= max) return s;\n const plain = stripAnsi(s);\n let out = '';\n let w = 0;\n for (const ch of plain) {\n const code = ch.codePointAt(0)!;\n const cw = code > 0x2e80 && code < 0xfb00 ? 2 : 1;\n if (w + cw > max - 1) break;\n out += ch;\n w += cw;\n }\n return out + '…';\n}\n","import { visibleWidth } from './text.js';\n\nexport function truncateName(name: string, max: number): string {\n if (visibleWidth(name) <= max) return name;\n let out = '';\n let w = 0;\n for (const ch of name) {\n const code = ch.codePointAt(0)!;\n const cw = code > 0x2e80 && code < 0xfb00 ? 2 : 1;\n if (w + cw > max - 1) break;\n out += ch;\n w += cw;\n }\n return out + '…';\n}\n"],"mappings":"wCAAA,OAAS,iBAAAA,EAAe,cAAAC,EAAY,aAAAC,EAAW,YAAAC,MAAgC,QAsC3E,cAAAC,MAAA,oBA7BJ,IAAMC,EAAkBC,EAA2C,IAAI,EAEhE,SAASC,EAAiB,CAAE,SAAAC,CAAS,EAA4B,CACtE,GAAM,CAACC,EAAUC,CAAW,EAAIC,EAA0B,IAAI,EACxD,CAACC,EAAMC,CAAO,EAAIF,EAAuC,IAAI,GAAK,EAExE,OAAAG,EAAU,IAAM,CACd,IAAIC,EAAY,GAChB,OAAC,SAAY,CACX,GAAI,CACF,IAAMC,EAAM,MAAMC,EAAa,EAC/B,GAAIF,EAAW,OACf,IAAMG,EAAM,IAAI,IAChB,QAAWC,KAAKH,EAAKE,EAAI,IAAIC,EAAE,GAAIA,CAAC,EACpCT,EAAYM,CAAG,EACfH,EAAQK,CAAG,CACb,MAAQ,CACDH,IACHL,EAAY,CAAC,CAAC,EACdG,EAAQ,IAAI,GAAK,EAErB,CACF,GAAG,EACI,IAAM,CACXE,EAAY,EACd,CACF,EAAG,CAAC,CAAC,EAGHX,EAACC,EAAgB,SAAhB,CAAyB,MAAO,CAAE,SAAAI,EAAU,KAAAG,CAAK,EAC/C,SAAAJ,EACH,CAEJ,CAEO,SAASY,GAAoC,CAClD,IAAMC,EAAMC,EAAWjB,CAAe,EACtC,GAAI,CAACgB,EAAK,MAAM,IAAI,MAAM,kDAAkD,EAC5E,OAAOA,CACT,CAEO,SAASE,EAAYC,EAAgC,CAC1D,GAAM,CAAE,KAAAZ,CAAK,EAAIQ,EAAY,EAC7B,OAAKI,EACSZ,EAAK,IAAIY,CAAE,GACX,MAAQA,EAFN,EAGlB,CCvDA,IAAMC,EAAU,kBAET,SAASC,EAAUC,EAAmB,CAC3C,OAAOA,EAAE,QAAQF,EAAS,EAAE,CAC9B,CAEO,SAASG,EAAaD,EAAmB,CAC9C,IAAME,EAAQH,EAAUC,CAAC,EACrBG,EAAI,EACR,QAAWC,KAAMF,EAAO,CACtB,IAAMG,EAAOD,EAAG,YAAY,CAAC,EAC7BD,GAAKE,EAAO,OAAUA,EAAO,MAAS,EAAI,CAC5C,CACA,OAAOF,CACT,CAEO,SAASG,EAAIN,EAAWO,EAAmB,CAChD,OAAOP,EAAI,IAAI,OAAO,KAAK,IAAI,EAAGO,EAAIN,EAAaD,CAAC,CAAC,CAAC,CACxD,CChBO,SAASQ,EAAaC,EAAcC,EAAqB,CAC9D,GAAIC,EAAaF,CAAI,GAAKC,EAAK,OAAOD,EACtC,IAAIG,EAAM,GACNC,EAAI,EACR,QAAWC,KAAML,EAAM,CACrB,IAAMM,EAAOD,EAAG,YAAY,CAAC,EACvBE,EAAKD,EAAO,OAAUA,EAAO,MAAS,EAAI,EAChD,GAAIF,EAAIG,EAAKN,EAAM,EAAG,MACtBE,GAAOE,EACPD,GAAKG,CACP,CACA,OAAOJ,EAAM,QACf","names":["createContext","useContext","useEffect","useState","jsx","RegistryContext","createContext","RegistryProvider","children","registry","setRegistry","useState","byId","setById","useEffect","cancelled","reg","loadRegistry","map","e","useRegistry","ctx","useContext","useDictName","id","ANSI_RE","stripAnsi","s","visibleWidth","plain","w","ch","code","pad","n","truncateName","name","max","visibleWidth","out","w","ch","code","cw"]}