qwerty-cli 0.0.1-alpha.16 → 0.0.1-alpha.18

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 (37) hide show
  1. package/dist/{ConfigEditor-KAL2MLQO.js → ConfigEditor-ZJ52VG3I.js} +2 -2
  2. package/dist/DictBrowser-ZQ773KQV.js +2 -0
  3. package/dist/DictBrowser-ZQ773KQV.js.map +1 -0
  4. package/dist/{HelpScreen-O5RSN3C4.js → HelpScreen-Y3TWEEWZ.js} +2 -2
  5. package/dist/{PracticeScreen-FOCGEFWL.js → PracticeScreen-PWXXG33U.js} +2 -2
  6. package/dist/{StatsViewer-XOOMUPGL.js → StatsViewer-XARAMVZW.js} +2 -2
  7. package/dist/{StatsViewer-XOOMUPGL.js.map → StatsViewer-XARAMVZW.js.map} +1 -1
  8. package/dist/{WordLookup-MWWYQBCM.js → WordLookup-LKBEPDTB.js} +2 -2
  9. package/dist/chunk-FQ2MEK7M.js +2 -0
  10. package/dist/chunk-FQ2MEK7M.js.map +1 -0
  11. package/dist/chunk-GULN5HRV.js +2 -0
  12. package/dist/chunk-GULN5HRV.js.map +1 -0
  13. package/dist/chunk-TCYEMBFW.js +3 -0
  14. package/dist/chunk-TCYEMBFW.js.map +1 -0
  15. package/dist/cli.js +1 -1
  16. package/dist/menu.impl-HBP6CFSW.js +2 -0
  17. package/dist/{menu.impl-4C7HT3VD.js.map → menu.impl-HBP6CFSW.js.map} +1 -1
  18. package/dist/practice.impl-AVTRDL7X.js +2 -0
  19. package/dist/practice.impl-AVTRDL7X.js.map +1 -0
  20. package/dist/{stats.impl-3I7BB7X3.js → stats.impl-WX3BFWI3.js} +3 -3
  21. package/dist/{stats.impl-3I7BB7X3.js.map → stats.impl-WX3BFWI3.js.map} +1 -1
  22. package/package.json +1 -1
  23. package/dist/DictBrowser-OF274IOX.js +0 -2
  24. package/dist/DictBrowser-OF274IOX.js.map +0 -1
  25. package/dist/chunk-4EJYJITR.js +0 -3
  26. package/dist/chunk-4EJYJITR.js.map +0 -1
  27. package/dist/chunk-HJIGZU3E.js +0 -2
  28. package/dist/chunk-HJIGZU3E.js.map +0 -1
  29. package/dist/chunk-UWTJMVJ6.js +0 -2
  30. package/dist/chunk-UWTJMVJ6.js.map +0 -1
  31. package/dist/menu.impl-4C7HT3VD.js +0 -2
  32. package/dist/practice.impl-OLNLJLIA.js +0 -2
  33. package/dist/practice.impl-OLNLJLIA.js.map +0 -1
  34. /package/dist/{ConfigEditor-KAL2MLQO.js.map → ConfigEditor-ZJ52VG3I.js.map} +0 -0
  35. /package/dist/{HelpScreen-O5RSN3C4.js.map → HelpScreen-Y3TWEEWZ.js.map} +0 -0
  36. /package/dist/{PracticeScreen-FOCGEFWL.js.map → PracticeScreen-PWXXG33U.js.map} +0 -0
  37. /package/dist/{WordLookup-MWWYQBCM.js.map → WordLookup-LKBEPDTB.js.map} +0 -0
@@ -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 !== 'default' && (\n <Box>\n <Text bold color={PALETTE.accent}>\n {t.app.title}\n </Text>\n <Text color={PALETTE.muted}> · {t.app.subtitle}</Text>\n </Box>\n )}\n\n <Box marginTop={cfg.stealth === 'default' ? 0 : 2} flexDirection=\"column\">\n {items.map((it, i) => {\n const active = i === selected;\n const pad = ' '.repeat(Math.max(0, labelW - visibleWidth(it.label)));\n return (\n <Box key={it.key}>\n <Text color={active ? PALETTE.accent : PALETTE.muted}>{active ? '▌ ' : ' '}</Text>\n <Text color={active ? PALETTE.accent : PALETTE.muted}>[{it.key}]</Text>\n <Text>{' '}</Text>\n <Text bold={active} color={active ? PALETTE.text : PALETTE.muted}>\n {it.label}\n {pad}\n </Text>\n <Text color={PALETTE.muted}>{it.hint}</Text>\n </Box>\n );\n })}\n </Box>\n\n <Box marginTop={2}>\n <Text color={PALETTE.muted}>\n {t.mainMenu.hint}\n {' · '}\n {t.mainMenu.helpHint}\n </Text>\n </Box>\n\n {audio.warning && (\n <Box marginTop={1}>\n <Text color={PALETTE.warning}>{t.audio.noPlayer}</Text>\n </Box>\n )}\n </Box>\n );\n}\n","import chalk from 'chalk';\nimport boxen from 'boxen';\nimport type { SessionReport } from '../infra/session-tracker.js';\nimport type { Strings } from '../i18n/strings.js';\nimport { leaveAltScreen } from './altscreen.js';\nimport { visibleWidth } from './text.js';\n\nexport function ensureMainScreen(): void {\n leaveAltScreen();\n}\n\nfunction fmtDuration(ms: number, lang: 'zh' | 'en'): string {\n const total = Math.floor(ms / 1000);\n const m = Math.floor(total / 60);\n const s = total % 60;\n if (lang === 'zh') {\n if (m === 0) return `${s} 秒`;\n return `${m} 分 ${s} 秒`;\n }\n if (m === 0) return `${s}s`;\n return `${m}m ${s}s`;\n}\n\nfunction accColor(pct: number): (s: string) => string {\n if (pct >= 90) return chalk.green;\n if (pct < 75) return chalk.dim;\n return (s) => s;\n}\n\nfunction buildLines(r: SessionReport, t: Strings, lang: 'zh' | 'en'): string[] {\n const lines: string[] = [];\n\n // Align labels in a single visual column. Use the longest label's\n // visible width as the target (CJK counted at 2 cols by visibleWidth).\n const allLabels: string[] = [t.report.duration];\n if (r.chaptersCompleted === 0) {\n allLabels.push(t.report.notPracticed);\n } else {\n allLabels.push(\n t.report.practiced,\n t.report.chapters,\n t.report.words,\n t.report.accuracy,\n t.report.wpm,\n );\n if (r.newMistakeWords > 0) allLabels.push(t.report.newMistakes);\n }\n const labelW = Math.max(...allLabels.map(visibleWidth));\n const padLabel = (s: string) =>\n s + ' '.repeat(Math.max(0, labelW - visibleWidth(s)));\n const row = (label: string, value: string) =>\n `${chalk.dim(padLabel(label))} ${value}`;\n\n lines.push(row(t.report.duration, fmtDuration(r.totalDurationMs, lang)));\n\n if (r.chaptersCompleted === 0) {\n lines.push(chalk.dim(t.report.notPracticed));\n } else {\n lines.push(row(t.report.practiced, fmtDuration(r.practiceMs, lang)));\n lines.push(row(t.report.chapters, String(r.chaptersCompleted)));\n lines.push(row(t.report.words, String(r.wordCount)));\n const accPct = Math.round(r.accuracy * 1000) / 10;\n lines.push(row(t.report.accuracy, accColor(accPct)(`${accPct}%`)));\n lines.push(row(t.report.wpm, String(r.wpm)));\n if (r.newMistakeWords > 0) {\n lines.push(row(t.report.newMistakes, chalk.red(String(r.newMistakeWords))));\n }\n }\n\n lines.push('');\n lines.push(chalk.dim.italic(t.report.farewell));\n return lines;\n}\n\nexport function printSessionReport(r: SessionReport, t: Strings, lang: 'zh' | 'en'): void {\n if (r.startedAt === null && r.chaptersCompleted === 0) return;\n const content = buildLines(r, t, lang).join('\\n');\n console.log(\n boxen(content, {\n title: chalk.bold.cyan(t.report.title),\n titleAlignment: 'left',\n borderStyle: 'round',\n borderColor: 'gray',\n padding: { top: 0, bottom: 0, left: 3, right: 3 },\n margin: { top: 1, bottom: 1, left: 2, right: 0 },\n }),\n );\n}\n"],"mappings":"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","names":["ENTER","LEAVE","active","signalsRegistered","ensureSignals","cleanup","enterAltScreen","leaveAltScreen","Suspense","lazy","useRef","Box","Text","useApp","useInput","useEffect","useState","Box","useStdout","jsx","Fullscreen","children","stdout","useStdout","size","setSize","useState","useEffect","enterAltScreen","onResize","leaveAltScreen","Box","useState","Box","Text","useApp","useInput","jsx","jsxs","MainMenu","cfg","selected","setSelected","useState","exit","useApp","nav","useNav","audio","useAudioStatus","t","useStrings","defaultDictName","useDictName","m","startPractice","stealth","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"]}
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-3O5SL5QY.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-3O5SL5QY.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-3O5SL5QY.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-JAQ2GXCS.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-JAQ2GXCS.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-JAQ2GXCS.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-JAQ2GXCS.js");await n(t,a)}),o.command("rm <id>").description("Remove a local dictionary").action(async t=>{let{dictRemove:a}=await import("./dict.impl-JAQ2GXCS.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-XVZCEC2O.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-OLNLJLIA.js");await a(o,t)})}import{Command as f}from"commander";function d(){return new f("stats").description("Show practice history and trends").option("-d, --days <n>","window size for trend (default 14)","14").option("--top <n>","how many top mistakes to show (default 10)","10").action(async o=>{let{runStats:t}=await import("./stats.impl-3I7BB7X3.js");await t(o)})}import{Command as C}from"commander";function s(){return new C("word").argument("<keyword>").description("Look up a word across local dictionaries").option("--exact","require exact (case-insensitive) match").action(async(o,t)=>{let{runWordLookup:a}=await import("./word.impl-CUBY4EZ4.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-OLNLJLIA.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-4C7HT3VD.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.16");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-3O5SL5QY.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-3O5SL5QY.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-3O5SL5QY.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-JAQ2GXCS.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-JAQ2GXCS.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-JAQ2GXCS.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-JAQ2GXCS.js");await n(t,a)}),o.command("rm <id>").description("Remove a local dictionary").action(async t=>{let{dictRemove:a}=await import("./dict.impl-JAQ2GXCS.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-XVZCEC2O.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-AVTRDL7X.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-CUBY4EZ4.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-AVTRDL7X.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-HBP6CFSW.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.18");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-TCYEMBFW.js";import{d as n,g as a,i as m}from"./chunk-6ROGUGNX.js";import"./chunk-BIBS2Q3E.js";import"./chunk-CQRKGMPU.js";import{a as i}from"./chunk-7LTZGB7F.js";import"./chunk-GULN5HRV.js";import"./chunk-QG7ZTS2G.js";import{e}from"./chunk-FQ2MEK7M.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-AVTRDL7X.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-HBP6CFSW.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":"0UAAA,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":"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"]}
@@ -0,0 +1,2 @@
1
+ import{a as c,b as g,c as h,d as x}from"./chunk-TCYEMBFW.js";import{f as m,g as l,i as u}from"./chunk-6ROGUGNX.js";import"./chunk-BIBS2Q3E.js";import"./chunk-CQRKGMPU.js";import{a}from"./chunk-7LTZGB7F.js";import{b as f,e as p}from"./chunk-GULN5HRV.js";import"./chunk-QG7ZTS2G.js";import{e as d}from"./chunk-FQ2MEK7M.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-AVTRDL7X.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/practice.impl.ts"],"sourcesContent":["import chalk from 'chalk';\nimport { render } from 'ink';\nimport { createElement } from 'react';\nimport { loadConfig } from '../infra/config-store.js';\nimport { App } from '../ui/App.js';\nimport { start as startSession, report as sessionReport } from '../infra/session-tracker.js';\nimport { printSessionReport, ensureMainScreen } from '../util/report.js';\nimport { pickStrings } from '../i18n/context.js';\nimport { enterAltScreen } from '../util/altscreen.js';\nimport { consumeSilentExit } from '../util/post-exit-action.js';\nimport { 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,7 +1,7 @@
1
- import{b as $,c as g,d as u,e as y,f as a,h}from"./chunk-HJIGZU3E.js";import{a as m,d as p}from"./chunk-G3DQB7FI.js";import"./chunk-E6BBQALJ.js";import t from"chalk";async function T(d){let l=Math.max(1,Number(d.days)||14),w=Math.max(1,Number(d.top)||10),r=await $(),M=await m();if(r.length===0){console.log(t.yellow("No practice history yet. Run `qwerty practice <dict>` to get started."));return}let e=h(r,l),k=y(r),n=r.reduce((o,s)=>o+s.wordCount,0),b=r.reduce((o,s)=>o+s.errors,0),i=r.reduce((o,s)=>o+s.durationMs,0),f=r.reduce((o,s)=>o+(s.wordCount-Object.keys(s.perWordErrors).length),0),S=i>0?Math.round(n/(i/6e4)*10)/10:0,v=n===0?1:f/n;console.log(t.bold(`
1
+ import{a as y,d as h}from"./chunk-G3DQB7FI.js";import{b as m,c as p,d as $,f as g,g as a,i as u}from"./chunk-GULN5HRV.js";import"./chunk-E6BBQALJ.js";import t from"chalk";async function T(d){let l=Math.max(1,Number(d.days)||14),w=Math.max(1,Number(d.top)||10),r=await m(),M=await y();if(r.length===0){console.log(t.yellow("No practice history yet. Run `qwerty practice <dict>` to get started."));return}let e=u(r,l),k=g(r),n=r.reduce((o,s)=>o+s.wordCount,0),b=r.reduce((o,s)=>o+s.errors,0),i=r.reduce((o,s)=>o+s.durationMs,0),f=r.reduce((o,s)=>o+(s.wordCount-Object.keys(s.perWordErrors).length),0),S=i>0?Math.round(n/(i/6e4)*10)/10:0,v=n===0?1:f/n;console.log(t.bold(`
2
2
  Lifetime`)),console.log(` ${t.dim("sessions")} ${r.length} ${t.dim("words")} ${n} ${t.dim("errors")} ${b}`),console.log(` ${t.dim("avg wpm")} ${S} ${t.dim("avg accuracy")} ${Math.round(v*1e3)/10}% ${t.dim("streak")} ${t.bold(k)}d`),console.log(t.bold(`
3
3
  Last ${l} days`)),console.log(` ${t.dim("wpm ")} ${a(e.map(o=>o.wpm))} ${t.dim("max")} ${Math.round(Math.max(...e.map(o=>o.wpm)))}`),console.log(` ${t.dim("accuracy")} ${a(e.map(o=>o.accuracy*100))} ${t.dim("range")} ${Math.round(Math.min(...e.map(o=>o.accuracy*100)))}-${Math.round(Math.max(...e.map(o=>o.accuracy*100)))}%`),console.log(` ${t.dim("sessions")} ${a(e.map(o=>o.sessions))}`);let x=r.slice(-5).reverse();console.log(t.bold(`
4
- Last 5 sessions`));for(let o of x){let s=g(o),E=Math.round(u(o)*1e3)/10;console.log(` ${t.dim(o.ts.replace("T"," ").slice(0,16))} ${t.cyan(o.dictId.padEnd(14))} ch${String(o.chapter+1).padStart(3)} ${o.mode.padEnd(9)} ${String(o.wordCount).padStart(3)}w ${o.errors}err ${s}wpm ${E}%`)}let c=p(M,w);if(c.length>0){console.log(t.bold(`
4
+ Last 5 sessions`));for(let o of x){let s=p(o),E=Math.round($(o)*1e3)/10;console.log(` ${t.dim(o.ts.replace("T"," ").slice(0,16))} ${t.cyan(o.dictId.padEnd(14))} ch${String(o.chapter+1).padStart(3)} ${o.mode.padEnd(9)} ${String(o.wordCount).padStart(3)}w ${o.errors}err ${s}wpm ${E}%`)}let c=h(M,w);if(c.length>0){console.log(t.bold(`
5
5
  Top ${c.length} mistakes`));for(let[o,s]of c)console.log(` ${t.red(String(s.count).padStart(3))} ${t.bold(o.padEnd(20))} ${t.dim(s.dictIds.join(", "))}`)}else console.log(t.bold(`
6
6
  Top mistakes`)),console.log(t.dim(" none \u2014 keep going"));console.log()}export{T as runStats};
7
- //# sourceMappingURL=stats.impl-3I7BB7X3.js.map
7
+ //# sourceMappingURL=stats.impl-WX3BFWI3.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/commands/stats.impl.ts"],"sourcesContent":["import chalk from 'chalk';\nimport {\n loadSessions,\n computeWPM,\n accuracy,\n dailyBuckets,\n sparkline,\n dailyStreak,\n} from '../domain/stats.js';\nimport { loadMistakes, topN } from '../domain/mistakes.js';\n\nexport async function runStats(opts: { days: string; top: string }): Promise<void> {\n const days = Math.max(1, Number(opts.days) || 14);\n const topCount = Math.max(1, Number(opts.top) || 10);\n const sessions = await loadSessions();\n const book = await loadMistakes();\n\n if (sessions.length === 0) {\n console.log(\n chalk.yellow('No practice history yet. Run `qwerty practice <dict>` to get started.'),\n );\n return;\n }\n\n const buckets = dailyBuckets(sessions, days);\n const streak = dailyStreak(sessions);\n const totalWords = sessions.reduce((a, s) => a + s.wordCount, 0);\n const totalErrors = sessions.reduce((a, s) => a + s.errors, 0);\n const totalMs = sessions.reduce((a, s) => a + s.durationMs, 0);\n const firstTryWords = sessions.reduce(\n (a, s) => a + (s.wordCount - Object.keys(s.perWordErrors).length),\n 0,\n );\n const overallWpm = totalMs > 0 ? Math.round((totalWords / (totalMs / 60000)) * 10) / 10 : 0;\n const overallAcc = totalWords === 0 ? 1 : firstTryWords / totalWords;\n\n console.log(chalk.bold('\\nLifetime'));\n console.log(\n ` ${chalk.dim('sessions')} ${sessions.length} ${chalk.dim('words')} ${totalWords} ${chalk.dim('errors')} ${totalErrors}`,\n );\n console.log(\n ` ${chalk.dim('avg wpm')} ${overallWpm} ${chalk.dim('avg accuracy')} ${Math.round(overallAcc * 1000) / 10}% ${chalk.dim('streak')} ${chalk.bold(streak)}d`,\n );\n\n console.log(chalk.bold(`\\nLast ${days} days`));\n console.log(\n ` ${chalk.dim('wpm ')} ${sparkline(buckets.map((b) => b.wpm))} ${chalk.dim('max')} ${Math.round(Math.max(...buckets.map((b) => b.wpm)))}`,\n );\n console.log(\n ` ${chalk.dim('accuracy')} ${sparkline(buckets.map((b) => b.accuracy * 100))} ${chalk.dim('range')} ${Math.round(Math.min(...buckets.map((b) => b.accuracy * 100)))}-${Math.round(Math.max(...buckets.map((b) => b.accuracy * 100)))}%`,\n );\n console.log(` ${chalk.dim('sessions')} ${sparkline(buckets.map((b) => b.sessions))}`);\n\n const recent = sessions.slice(-5).reverse();\n console.log(chalk.bold('\\nLast 5 sessions'));\n for (const s of recent) {\n const wpm = computeWPM(s);\n const acc = Math.round(accuracy(s) * 1000) / 10;\n console.log(\n ` ${chalk.dim(s.ts.replace('T', ' ').slice(0, 16))} ${chalk.cyan(s.dictId.padEnd(14))} ch${String(s.chapter + 1).padStart(3)} ${s.mode.padEnd(9)} ${String(s.wordCount).padStart(3)}w ${s.errors}err ${wpm}wpm ${acc}%`,\n );\n }\n\n const top = topN(book, topCount);\n if (top.length > 0) {\n console.log(chalk.bold(`\\nTop ${top.length} mistakes`));\n for (const [word, entry] of top) {\n console.log(\n ` ${chalk.red(String(entry.count).padStart(3))} ${chalk.bold(word.padEnd(20))} ${chalk.dim(entry.dictIds.join(', '))}`,\n );\n }\n } else {\n console.log(chalk.bold('\\nTop mistakes'));\n console.log(chalk.dim(' none — keep going'));\n }\n console.log();\n}\n"],"mappings":"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","names":["chalk","runStats","opts","days","topCount","sessions","loadSessions","book","loadMistakes","chalk","buckets","dailyBuckets","streak","dailyStreak","totalWords","a","totalErrors","totalMs","firstTryWords","overallWpm","overallAcc","sparkline","b","recent","s","wpm","computeWPM","acc","accuracy","top","topN","word","entry"]}
1
+ {"version":3,"sources":["../src/commands/stats.impl.ts"],"sourcesContent":["import chalk from 'chalk';\nimport {\n loadSessions,\n computeWPM,\n accuracy,\n dailyBuckets,\n sparkline,\n dailyStreak,\n} from '../domain/stats.js';\nimport { loadMistakes, topN } from '../domain/mistakes.js';\n\nexport async function runStats(opts: { days: string; top: string }): Promise<void> {\n const days = Math.max(1, Number(opts.days) || 14);\n const topCount = Math.max(1, Number(opts.top) || 10);\n const sessions = await loadSessions();\n const book = await loadMistakes();\n\n if (sessions.length === 0) {\n console.log(\n chalk.yellow('No practice history yet. Run `qwerty practice <dict>` to get started.'),\n );\n return;\n }\n\n const buckets = dailyBuckets(sessions, days);\n const streak = dailyStreak(sessions);\n const totalWords = sessions.reduce((a, s) => a + s.wordCount, 0);\n const totalErrors = sessions.reduce((a, s) => a + s.errors, 0);\n const totalMs = sessions.reduce((a, s) => a + s.durationMs, 0);\n const firstTryWords = sessions.reduce(\n (a, s) => a + (s.wordCount - Object.keys(s.perWordErrors).length),\n 0,\n );\n const overallWpm = totalMs > 0 ? Math.round((totalWords / (totalMs / 60000)) * 10) / 10 : 0;\n const overallAcc = totalWords === 0 ? 1 : firstTryWords / totalWords;\n\n console.log(chalk.bold('\\nLifetime'));\n console.log(\n ` ${chalk.dim('sessions')} ${sessions.length} ${chalk.dim('words')} ${totalWords} ${chalk.dim('errors')} ${totalErrors}`,\n );\n console.log(\n ` ${chalk.dim('avg wpm')} ${overallWpm} ${chalk.dim('avg accuracy')} ${Math.round(overallAcc * 1000) / 10}% ${chalk.dim('streak')} ${chalk.bold(streak)}d`,\n );\n\n console.log(chalk.bold(`\\nLast ${days} days`));\n console.log(\n ` ${chalk.dim('wpm ')} ${sparkline(buckets.map((b) => b.wpm))} ${chalk.dim('max')} ${Math.round(Math.max(...buckets.map((b) => b.wpm)))}`,\n );\n console.log(\n ` ${chalk.dim('accuracy')} ${sparkline(buckets.map((b) => b.accuracy * 100))} ${chalk.dim('range')} ${Math.round(Math.min(...buckets.map((b) => b.accuracy * 100)))}-${Math.round(Math.max(...buckets.map((b) => b.accuracy * 100)))}%`,\n );\n console.log(` ${chalk.dim('sessions')} ${sparkline(buckets.map((b) => b.sessions))}`);\n\n const recent = sessions.slice(-5).reverse();\n console.log(chalk.bold('\\nLast 5 sessions'));\n for (const s of recent) {\n const wpm = computeWPM(s);\n const acc = Math.round(accuracy(s) * 1000) / 10;\n console.log(\n ` ${chalk.dim(s.ts.replace('T', ' ').slice(0, 16))} ${chalk.cyan(s.dictId.padEnd(14))} ch${String(s.chapter + 1).padStart(3)} ${s.mode.padEnd(9)} ${String(s.wordCount).padStart(3)}w ${s.errors}err ${wpm}wpm ${acc}%`,\n );\n }\n\n const top = topN(book, topCount);\n if (top.length > 0) {\n console.log(chalk.bold(`\\nTop ${top.length} mistakes`));\n for (const [word, entry] of top) {\n console.log(\n ` ${chalk.red(String(entry.count).padStart(3))} ${chalk.bold(word.padEnd(20))} ${chalk.dim(entry.dictIds.join(', '))}`,\n );\n }\n } else {\n console.log(chalk.bold('\\nTop mistakes'));\n console.log(chalk.dim(' none — keep going'));\n }\n console.log();\n}\n"],"mappings":"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","names":["chalk","runStats","opts","days","topCount","sessions","loadSessions","book","loadMistakes","chalk","buckets","dailyBuckets","streak","dailyStreak","totalWords","a","totalErrors","totalMs","firstTryWords","overallWpm","overallAcc","sparkline","b","recent","s","wpm","computeWPM","acc","accuracy","top","topN","word","entry"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwerty-cli",
3
- "version": "0.0.1-alpha.16",
3
+ "version": "0.0.1-alpha.18",
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{a as F,d as H,f as N}from"./chunk-DURXS5MX.js";import{b as z}from"./chunk-CQRKGMPU.js";import"./chunk-7LTZGB7F.js";import{b as q,d as J,f as e}from"./chunk-UWTJMVJ6.js";import{b as Y,c as _}from"./chunk-NA5UNUVL.js";import"./chunk-E6BBQALJ.js";import{useEffect as at,useMemo as dt,useState as B}from"react";import{Box as l,Text as c,useInput as st,useStdout as mt}from"ink";import{useState as lt}from"react";import{Box as M,Text as I,useInput as ct}from"ink";import{jsx as A,jsxs as K}from"react/jsx-runtime";function G({title:D,items:g,onClose:w}){let f=g.map((s,u)=>s.disabled?-1:u).filter(s=>s>=0),o=f[0]??0,[P,v]=lt(o);ct((s,u)=>{if(u.escape){w();return}if(u.upArrow){let a=f.indexOf(P),d=f[(a-1+f.length)%f.length];d!==void 0&&v(d);return}if(u.downArrow){let a=f.indexOf(P),d=f[(a+1)%f.length];d!==void 0&&v(d);return}if(u.return){let a=g[P];a&&!a.disabled&&a.run();return}for(let a=0;a<g.length;a++){let d=g[a];if(!d.disabled&&d.key&&s===d.key){d.run();return}}});let L=Math.max(...g.map(s=>s.label.length)),S=Math.max(L+8,D.length+4,24);return K(M,{flexDirection:"column",borderStyle:"round",borderColor:e.accent,paddingX:2,paddingY:1,width:S,children:[A(M,{marginBottom:1,children:A(I,{bold:!0,color:e.accent,children:D})}),g.map((s,u)=>{let a=u===P,d=s.disabled?e.muted:a?e.text:e.muted;return K(M,{children:[A(I,{color:a?e.accent:e.muted,children:a?"\u258C ":" "}),A(I,{bold:a,color:d,children:s.label})]},u)}),A(M,{marginTop:1,children:A(I,{color:e.muted,children:"\u2191/\u2193 \xB7 Enter \xB7 Esc"})})]})}import{Fragment as ut,jsx as t,jsxs as h}from"react/jsx-runtime";function Mt({params:D}){let g=q(),{cfg:w,setCfg:f}=z(),o=J(),{stdout:P}=mt(),[v,L]=B([]),[S,s]=B(!0),[u,a]=B(0),[d,O]=B(""),[x,y]=B(null),[Q,E]=B(0),[C,p]=B(null),U=async()=>{let n=await _(),i=await Promise.all(n.map(async m=>({entry:m,local:await H(m.id)})));L(i),s(!1)};at(()=>{U()},[Q]);let b=dt(()=>d?v.filter(n=>Y([n.entry],d).length>0):v,[d,v]),R=Math.max(0,Math.min(b.length-1,u)),r=b[R],V=P?.rows??24,$=Math.max(6,V-8),W=Math.floor($/2),j=Math.max(0,Math.min(b.length-$,R-W)),Z=Math.min(b.length,j+$),X=n=>{g.replace({name:"practice",params:{dictId:n,chapterIndex:0,mode:w.defaultMode,stealth:w.stealth==="default"}})},tt=async(n,i=!0)=>{await f({...w,defaultDict:n}),p(null),i&&(D?.pickerMode==="choose-then-practice"?X(n):g.back())},et=n=>{p(null),y({kind:"removing",id:n}),(async()=>{try{await N(n),y(null),E(i=>i+1)}catch(i){y({kind:"error",id:n,msg:i.message})}})()},nt=n=>{p(null),y({kind:"pulling",id:n}),(async()=>{try{await F(n),y(null),E(i=>i+1)}catch(i){y({kind:"error",id:n,msg:i.message})}})()},ot=()=>{p(null),y({kind:"refreshing"}),E(n=>n+1),y(null)};if(st((n,i)=>{if(C===null){if(i.escape){g.back();return}if(i.upArrow){a(m=>Math.max(0,m-1));return}if(i.downArrow){a(m=>Math.min(b.length-1,m+1));return}if(i.ctrl&&n==="k"){p("more");return}if(i.return){r&&p("item");return}if(i.backspace||i.delete){O(m=>m.slice(0,-1)),a(0);return}if(n&&!i.ctrl&&!i.meta){let m=[...n].filter(T=>{let k=T.codePointAt(0);return k>=32&&k!==127}).join("");if(m.length===0)return;O(T=>T+m),a(0)}}}),S)return t(l,{alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:t(c,{color:e.muted,children:o.dict.loading})});let rt=r?[{label:o.dict.action.setDefault,run:()=>{tt(r.entry.id,D?.pickerMode!==void 0)}},{label:o.dict.action.practice,run:()=>X(r.entry.id)},{label:o.dict.action.delete,disabled:!r.local,run:()=>et(r.entry.id)},{label:o.common.cancel,run:()=>p(null)}]:[],it=[{label:o.dict.command.pull,disabled:!r,run:()=>r&&nt(r.entry.id)},{label:o.dict.command.import,disabled:!0,run:()=>{}},{label:o.dict.command.refreshList,run:()=>ot()},{label:o.common.cancel,run:()=>p(null)}];return C==="item"&&r?t(l,{alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:t(G,{title:`${o.dict.action.title} \xB7 ${r.entry.name}`,items:rt,onClose:()=>p(null)})}):C==="more"?t(l,{alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:t(G,{title:o.dict.command.title,items:it,onClose:()=>p(null)})}):h(l,{flexDirection:"column",paddingX:2,paddingY:1,width:"100%",height:"100%",children:[h(l,{children:[t(c,{bold:!0,color:e.accent,children:o.dict.title}),t(l,{flexGrow:1}),t(c,{color:e.muted,children:d?`${o.dict.filterPlaceholder}: ${d}_`:`${o.dict.filterPlaceholder}_`}),h(c,{color:e.muted,children:[" ",o.dict.entries(b.length)]})]}),h(l,{marginTop:1,flexGrow:1,children:[t(l,{flexDirection:"column",width:"75%",paddingRight:1,children:b.slice(j,Z).map((n,i)=>{let T=j+i===R,k=w.defaultDict===n.entry.id;return h(l,{children:[t(l,{width:2,children:t(c,{color:T?e.accent:e.muted,children:T?"\u258C ":" "})}),t(l,{width:2,children:t(c,{color:n.local?e.accent:e.muted,children:n.local?"\u25CF":"\u25CB"})}),t(l,{width:2,children:t(c,{color:k?e.success:e.muted,children:k?"\u2605":" "})}),t(l,{flexGrow:1,children:t(c,{bold:T,color:T?e.text:e.muted,wrap:"truncate",children:n.entry.name})}),t(l,{width:6,children:t(c,{color:e.muted,children:String(n.entry.length).padStart(5)})})]},n.entry.id)})}),t(l,{flexDirection:"column",width:"25%",paddingLeft:1,children:r&&h(ut,{children:[t(c,{bold:!0,color:e.text,wrap:"wrap",children:r.entry.name}),t(c,{color:e.muted,children:r.entry.id}),t(l,{marginTop:1,children:h(c,{color:e.muted,wrap:"wrap",children:[r.entry.language," \xB7 ",r.entry.category]})}),t(l,{marginTop:1,children:t(c,{color:e.muted,children:o.dict.wordsLabel(r.entry.length)})}),r.entry.description&&t(l,{marginTop:1,children:t(c,{color:e.primary,wrap:"wrap",children:r.entry.description})}),r.entry.tags.length>0&&t(l,{marginTop:1,children:t(c,{color:e.muted,wrap:"wrap",children:o.dict.tagsLabel(r.entry.tags.join(", "))})}),t(l,{marginTop:1,children:t(c,{color:r.local?e.accent:e.muted,children:r.local?o.dict.local:o.dict.notLocal})}),w.defaultDict===r.entry.id&&t(l,{children:t(c,{color:e.success,children:o.dict.defaultMark})})]})})]}),x&&h(l,{marginTop:1,children:[x.kind==="pulling"&&t(c,{color:e.warning,children:o.dict.pulling(x.id)}),x.kind==="removing"&&t(c,{color:e.warning,children:o.dict.removing(x.id)}),x.kind==="refreshing"&&h(c,{color:e.warning,children:[o.dict.command.refreshList,"\u2026"]}),x.kind==="error"&&t(c,{color:e.error,children:o.dict.errorOn(x.id,x.msg)})]}),t(l,{marginTop:1,children:t(c,{color:e.muted,children:o.dict.footer})})]})}export{Mt as DictBrowser};
2
- //# sourceMappingURL=DictBrowser-OF274IOX.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/ui/screens/DictBrowser.tsx","../src/ui/components/ActionPanel.tsx"],"sourcesContent":["import { useEffect, useMemo, useState } from 'react';\nimport { Box, Text, useInput, useStdout } from 'ink';\nimport { useNav, type DictParams } from '../nav.js';\nimport { useAppState } from '../app-state.js';\nimport { useStrings } from '../../i18n/context.js';\nimport { PALETTE } from '../components/BigWord.js';\nimport { ActionPanel, type ActionItem } from '../components/ActionPanel.js';\nimport { loadRegistry } from '../../infra/registry-store.js';\nimport {\n isLocallyAvailable,\n pullDictionary,\n removeDictionary,\n} from '../../infra/dict-downloader.js';\nimport type { DictionaryEntry } from '../../domain/dictionary.js';\nimport { filterRegistry } from '../../domain/dictionary.js';\n\ntype Row = { entry: DictionaryEntry; local: boolean };\ntype PendingAction =\n | { kind: 'pulling'; id: string }\n | { kind: 'removing'; id: string }\n | { kind: 'refreshing' }\n | { kind: 'error'; id: string; msg: string }\n | null;\n\ntype Panel = null | 'item' | 'more';\n\nexport function DictBrowser({ params }: { params?: DictParams }) {\n const nav = useNav();\n const { cfg, setCfg } = useAppState();\n const t = useStrings();\n const { stdout } = useStdout();\n const [rows, setRows] = useState<Row[]>([]);\n const [loading, setLoading] = useState(true);\n const [selected, setSelected] = useState(0);\n const [filter, setFilter] = useState('');\n const [pending, setPending] = useState<PendingAction>(null);\n const [tick, setTick] = useState(0);\n const [panel, setPanel] = useState<Panel>(null);\n\n const refresh = async () => {\n const reg = await loadRegistry();\n const flagged = await Promise.all(\n reg.map(async (e) => ({ entry: e, local: await isLocallyAvailable(e.id) })),\n );\n setRows(flagged);\n setLoading(false);\n };\n\n useEffect(() => {\n void refresh();\n }, [tick]);\n\n const filtered = useMemo(\n () => (filter ? rows.filter((r) => filterRegistry([r.entry], filter).length > 0) : rows),\n [filter, rows],\n );\n const safeSelected = Math.max(0, Math.min(filtered.length - 1, selected));\n const current = filtered[safeSelected];\n\n const rowsTotal = stdout?.rows ?? 24;\n const visibleH = Math.max(6, rowsTotal - 8);\n const half = Math.floor(visibleH / 2);\n const start = Math.max(0, Math.min(filtered.length - visibleH, safeSelected - half));\n const end = Math.min(filtered.length, start + visibleH);\n\n const goPractice = (id: string) => {\n nav.replace({\n name: 'practice',\n params: {\n dictId: id,\n chapterIndex: 0,\n mode: cfg.defaultMode,\n stealth: cfg.stealth === 'default',\n },\n });\n };\n\n const doSetDefault = async (id: string, navigate = true) => {\n await setCfg({ ...cfg, defaultDict: id });\n setPanel(null);\n if (navigate) {\n if (params?.pickerMode === 'choose-then-practice') {\n goPractice(id);\n } else {\n nav.back();\n }\n }\n };\n\n const doDelete = (id: string) => {\n setPanel(null);\n setPending({ kind: 'removing', id });\n void (async () => {\n try {\n await removeDictionary(id);\n setPending(null);\n setTick((n) => n + 1);\n } catch (err) {\n setPending({ kind: 'error', id, msg: (err as Error).message });\n }\n })();\n };\n\n const doPull = (id: string) => {\n setPanel(null);\n setPending({ kind: 'pulling', id });\n void (async () => {\n try {\n await pullDictionary(id);\n setPending(null);\n setTick((n) => n + 1);\n } catch (err) {\n setPending({ kind: 'error', id, msg: (err as Error).message });\n }\n })();\n };\n\n const doRefreshList = () => {\n setPanel(null);\n setPending({ kind: 'refreshing' });\n setTick((n) => n + 1);\n setPending(null);\n };\n\n useInput((input, key) => {\n if (panel !== null) return;\n\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.ctrl && input === 'k') {\n setPanel('more');\n return;\n }\n if (key.return) {\n if (current) setPanel('item');\n return;\n }\n if (key.backspace || key.delete) {\n setFilter((f) => f.slice(0, -1));\n setSelected(0);\n return;\n }\n if (input && !key.ctrl && !key.meta) {\n // Keep printable codepoints (>= 0x20, != 0x7F DEL). Removing the old\n // `input.length === 1` cap so IME multi-char commits like '考研' (length 2)\n // and pasted runs reach the filter. filterRegistry already matches the\n // dictionary `name` field, which is Chinese for ~75% of the registry —\n // the bug was that the keystroke never made it to filter state.\n const cleaned = [...input]\n .filter((c) => {\n const cp = c.codePointAt(0)!;\n return cp >= 0x20 && cp !== 0x7f;\n })\n .join('');\n if (cleaned.length === 0) return;\n setFilter((f) => f + cleaned);\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.dict.loading}</Text>\n </Box>\n );\n }\n\n const itemPanelItems: ActionItem[] = current\n ? [\n {\n label: t.dict.action.setDefault,\n run: () => void doSetDefault(current.entry.id, params?.pickerMode !== undefined),\n },\n {\n label: t.dict.action.practice,\n run: () => goPractice(current.entry.id),\n },\n {\n label: t.dict.action.delete,\n disabled: !current.local,\n run: () => doDelete(current.entry.id),\n },\n { label: t.common.cancel, run: () => setPanel(null) },\n ]\n : [];\n\n const morePanelItems: ActionItem[] = [\n {\n label: t.dict.command.pull,\n disabled: !current,\n run: () => current && doPull(current.entry.id),\n },\n {\n label: t.dict.command.import,\n disabled: true,\n run: () => undefined,\n },\n { label: t.dict.command.refreshList, run: () => doRefreshList() },\n { label: t.common.cancel, run: () => setPanel(null) },\n ];\n\n if (panel === 'item' && current) {\n return (\n <Box alignItems=\"center\" justifyContent=\"center\" width=\"100%\" height=\"100%\">\n <ActionPanel\n title={`${t.dict.action.title} · ${current.entry.name}`}\n items={itemPanelItems}\n onClose={() => setPanel(null)}\n />\n </Box>\n );\n }\n if (panel === 'more') {\n return (\n <Box alignItems=\"center\" justifyContent=\"center\" width=\"100%\" height=\"100%\">\n <ActionPanel\n title={t.dict.command.title}\n items={morePanelItems}\n onClose={() => setPanel(null)}\n />\n </Box>\n );\n }\n\n return (\n <Box flexDirection=\"column\" paddingX={2} paddingY={1} width=\"100%\" height=\"100%\">\n <Box>\n <Text bold color={PALETTE.accent}>\n {t.dict.title}\n </Text>\n <Box flexGrow={1} />\n <Text color={PALETTE.muted}>\n {filter ? `${t.dict.filterPlaceholder}: ${filter}_` : `${t.dict.filterPlaceholder}_`}\n </Text>\n <Text color={PALETTE.muted}>\n {' '}\n {t.dict.entries(filtered.length)}\n </Text>\n </Box>\n\n <Box marginTop={1} flexGrow={1}>\n <Box flexDirection=\"column\" width=\"75%\" paddingRight={1}>\n {filtered.slice(start, end).map((row, vi) => {\n const i = start + vi;\n const active = i === safeSelected;\n const isDefault = cfg.defaultDict === row.entry.id;\n return (\n <Box key={row.entry.id}>\n <Box width={2}>\n <Text color={active ? PALETTE.accent : PALETTE.muted}>{active ? '▌ ' : ' '}</Text>\n </Box>\n <Box width={2}>\n <Text color={row.local ? PALETTE.accent : PALETTE.muted}>\n {row.local ? '●' : '○'}\n </Text>\n </Box>\n <Box width={2}>\n <Text color={isDefault ? PALETTE.success : PALETTE.muted}>\n {isDefault ? '★' : ' '}\n </Text>\n </Box>\n <Box flexGrow={1}>\n <Text bold={active} color={active ? PALETTE.text : PALETTE.muted} wrap=\"truncate\">\n {row.entry.name}\n </Text>\n </Box>\n <Box width={6}>\n <Text color={PALETTE.muted}>{String(row.entry.length).padStart(5)}</Text>\n </Box>\n </Box>\n );\n })}\n </Box>\n\n <Box flexDirection=\"column\" width=\"25%\" paddingLeft={1}>\n {current && (\n <>\n <Text bold color={PALETTE.text} wrap=\"wrap\">\n {current.entry.name}\n </Text>\n <Text color={PALETTE.muted}>{current.entry.id}</Text>\n <Box marginTop={1}>\n <Text color={PALETTE.muted} wrap=\"wrap\">\n {current.entry.language} · {current.entry.category}\n </Text>\n </Box>\n <Box marginTop={1}>\n <Text color={PALETTE.muted}>{t.dict.wordsLabel(current.entry.length)}</Text>\n </Box>\n {current.entry.description && (\n <Box marginTop={1}>\n <Text color={PALETTE.primary} wrap=\"wrap\">{current.entry.description}</Text>\n </Box>\n )}\n {current.entry.tags.length > 0 && (\n <Box marginTop={1}>\n <Text color={PALETTE.muted} wrap=\"wrap\">{t.dict.tagsLabel(current.entry.tags.join(', '))}</Text>\n </Box>\n )}\n <Box marginTop={1}>\n <Text color={current.local ? PALETTE.accent : PALETTE.muted}>\n {current.local ? t.dict.local : t.dict.notLocal}\n </Text>\n </Box>\n {cfg.defaultDict === current.entry.id && (\n <Box>\n <Text color={PALETTE.success}>{t.dict.defaultMark}</Text>\n </Box>\n )}\n </>\n )}\n </Box>\n </Box>\n\n {pending && (\n <Box marginTop={1}>\n {pending.kind === 'pulling' && (\n <Text color={PALETTE.warning}>{t.dict.pulling(pending.id)}</Text>\n )}\n {pending.kind === 'removing' && (\n <Text color={PALETTE.warning}>{t.dict.removing(pending.id)}</Text>\n )}\n {pending.kind === 'refreshing' && (\n <Text color={PALETTE.warning}>{t.dict.command.refreshList}…</Text>\n )}\n {pending.kind === 'error' && (\n <Text color={PALETTE.error}>{t.dict.errorOn(pending.id, pending.msg)}</Text>\n )}\n </Box>\n )}\n\n <Box marginTop={1}>\n <Text color={PALETTE.muted}>{t.dict.footer}</Text>\n </Box>\n </Box>\n );\n}\n","import { useState } from 'react';\nimport { Box, Text, useInput } from 'ink';\nimport { PALETTE } from './BigWord.js';\n\nexport type ActionItem = {\n key?: string;\n label: string;\n disabled?: boolean;\n run: () => void | Promise<void>;\n};\n\ntype Props = {\n title: string;\n items: ActionItem[];\n onClose: () => void;\n};\n\nexport function ActionPanel({ title, items, onClose }: Props) {\n const enabledIndices = items\n .map((it, i) => (it.disabled ? -1 : i))\n .filter((i) => i >= 0);\n const initial = enabledIndices[0] ?? 0;\n const [selected, setSelected] = useState(initial);\n\n useInput((input, key) => {\n if (key.escape) {\n onClose();\n return;\n }\n if (key.upArrow) {\n const cur = enabledIndices.indexOf(selected);\n const next = enabledIndices[(cur - 1 + enabledIndices.length) % enabledIndices.length];\n if (next !== undefined) setSelected(next);\n return;\n }\n if (key.downArrow) {\n const cur = enabledIndices.indexOf(selected);\n const next = enabledIndices[(cur + 1) % enabledIndices.length];\n if (next !== undefined) setSelected(next);\n return;\n }\n if (key.return) {\n const item = items[selected];\n if (item && !item.disabled) {\n void item.run();\n }\n return;\n }\n for (let i = 0; i < items.length; i++) {\n const it = items[i]!;\n if (it.disabled) continue;\n if (it.key && input === it.key) {\n void it.run();\n return;\n }\n }\n });\n\n const maxLabel = Math.max(...items.map((it) => it.label.length));\n const width = Math.max(maxLabel + 8, title.length + 4, 24);\n\n return (\n <Box\n flexDirection=\"column\"\n borderStyle=\"round\"\n borderColor={PALETTE.accent}\n paddingX={2}\n paddingY={1}\n width={width}\n >\n <Box marginBottom={1}>\n <Text bold color={PALETTE.accent}>\n {title}\n </Text>\n </Box>\n {items.map((it, i) => {\n const active = i === selected;\n const color = it.disabled\n ? PALETTE.muted\n : active\n ? PALETTE.text\n : PALETTE.muted;\n return (\n <Box key={i}>\n <Text color={active ? PALETTE.accent : PALETTE.muted}>\n {active ? '▌ ' : ' '}\n </Text>\n <Text bold={active} color={color}>\n {it.label}\n </Text>\n </Box>\n );\n })}\n <Box marginTop={1}>\n <Text color={PALETTE.muted}>↑/↓ · Enter · Esc</Text>\n </Box>\n </Box>\n );\n}\n"],"mappings":"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","names":["useEffect","useMemo","useState","Box","Text","useInput","useStdout","useState","Box","Text","useInput","jsx","jsxs","ActionPanel","title","items","onClose","enabledIndices","it","i","initial","selected","setSelected","useState","useInput","input","key","cur","next","item","maxLabel","width","Box","PALETTE","Text","active","color","Fragment","jsx","jsxs","DictBrowser","params","nav","useNav","cfg","setCfg","useAppState","t","useStrings","stdout","useStdout","rows","setRows","useState","loading","setLoading","selected","setSelected","filter","setFilter","pending","setPending","tick","setTick","panel","setPanel","refresh","reg","loadRegistry","flagged","e","isLocallyAvailable","useEffect","filtered","useMemo","r","filterRegistry","safeSelected","current","rowsTotal","visibleH","half","start","end","goPractice","id","doSetDefault","navigate","doDelete","removeDictionary","n","err","doPull","pullDictionary","doRefreshList","useInput","input","key","i","f","cleaned","c","cp","Box","Text","PALETTE","itemPanelItems","morePanelItems","ActionPanel","row","vi","active","isDefault"]}
@@ -1,3 +0,0 @@
1
- import{a as R,b as P,c as H}from"./chunk-6ROGUGNX.js";import{a as E,b as k}from"./chunk-CQRKGMPU.js";import{a as D,b as C,c as S,e as I}from"./chunk-QG7ZTS2G.js";import{a as L,b as M,c as N,d as B,f as a}from"./chunk-UWTJMVJ6.js";var J="\x1B[?1049h\x1B[?25l\x1B[2J\x1B[H",z="\x1B[?25h\x1B[?1049l",w=!1,$=!1;function K(){if($)return;$=!0;let e=()=>{if(w){try{process.stdout.write(z)}catch{}w=!1}};process.once("exit",e),process.once("SIGINT",()=>{e(),process.exit(130)}),process.once("SIGTERM",()=>{e(),process.exit(143)})}function W(){w||process.stdout.isTTY&&process.env.QWERTY_NO_ALTSCREEN!=="1"&&(K(),process.stdout.write(J),w=!0)}function A(){if(w){try{process.stdout.write(z)}catch{}w=!1}}import{Suspense as re,lazy as y,useRef as oe}from"react";import{Box as ne,Text as ie,useApp as se,useInput as ae}from"ink";import{useEffect as _,useState as O}from"react";import{Box as Q,useStdout as X}from"ink";import{jsx as U}from"react/jsx-runtime";function F({children:e}){let{stdout:t}=X(),[s,r]=O(()=>({rows:t?.rows??24,cols:t?.columns??80}));return _(()=>{W();let o=()=>{r({rows:process.stdout.rows??24,cols:process.stdout.columns??80})};return process.stdout.on("resize",o),()=>{process.stdout.off("resize",o),A()}},[]),U(Q,{width:s.cols,height:s.rows,flexDirection:"column",children:e})}import{useState as Z}from"react";import{Box as v,Text as f,useApp as ee,useInput as te}from"ink";import{jsx as g,jsxs as x}from"react/jsx-runtime";function Y({cfg:e}){let[t,s]=Z(0),{exit:r}=ee(),o=M(),h=P(),p=B(),l=C(e.defaultDict),n=p.mainMenu.items,d=c=>{if(c&&e.defaultDict){H({type:"stealth",dictId:e.defaultDict,chapterIndex:0,mode:e.defaultMode}),r();return}e.defaultDict?o.navigate({name:"practice",params:{dictId:e.defaultDict,chapterIndex:0,mode:e.defaultMode,stealth:c}}):o.navigate({name:"dict",params:{pickerMode:"choose-then-practice"}})},m=[{key:"p",label:n.practiceLabel,hint:e.defaultDict?n.practiceHintWith(I(l,24)):n.practiceHintNone,run:()=>d(e.stealth==="default")}];(e.stealth==="menu"||e.stealth==="default")&&m.push({key:"b",label:n.stealthLabel,hint:n.stealthHint,run:()=>d(!0)}),m.push({key:"d",label:n.dictLabel,hint:n.dictHint,run:()=>o.navigate({name:"dict"})},{key:"w",label:n.wordLabel,hint:n.wordHint,run:()=>o.navigate({name:"word"})},{key:"s",label:n.statsLabel,hint:n.statsHint,run:()=>o.navigate({name:"stats"})},{key:"c",label:n.configLabel,hint:n.configHint,run:()=>o.navigate({name:"config"})},{key:"q",label:n.quitLabel,hint:n.quitHint,run:()=>r()});let V=Math.max(...m.map(c=>S(c.label)))+4;return te((c,b)=>{if(b.escape){r();return}if(b.upArrow&&s(u=>(u-1+m.length)%m.length),b.downArrow&&s(u=>(u+1)%m.length),b.return){m[t].run();return}if(c==="?"){o.navigate({name:"help"});return}for(let u of m)if(c===u.key){u.run();return}}),x(v,{flexDirection:"column",paddingX:2,paddingY:1,width:"100%",children:[e.stealth==="off"&&x(v,{children:[g(f,{bold:!0,color:a.accent,children:p.app.title}),x(f,{color:a.muted,children:[" \xB7 ",p.app.subtitle]})]}),g(v,{marginTop:e.stealth==="off"?2:0,flexDirection:"column",children:m.map((c,b)=>{let u=b===t,G=" ".repeat(Math.max(0,V-S(c.label)));return x(v,{children:[g(f,{color:u?a.accent:a.muted,children:u?"\u258C ":" "}),x(f,{color:u?a.accent:a.muted,children:["[",c.key,"]"]}),g(f,{children:" "}),x(f,{bold:u,color:u?a.text:a.muted,children:[c.label,G]}),g(f,{color:a.muted,children:c.hint})]},c.key)})}),g(v,{marginTop:2,children:x(f,{color:a.muted,children:[p.mainMenu.hint," \xB7 ",p.mainMenu.helpHint]})}),h.warning&&g(v,{marginTop:1,children:g(f,{color:a.warning,children:p.audio.noPlayer})})]})}import{jsx as i}from"react/jsx-runtime";var ce=y(()=>import("./PracticeScreen-FOCGEFWL.js").then(e=>({default:e.PracticeScreen}))),ue=y(()=>import("./DictBrowser-OF274IOX.js").then(e=>({default:e.DictBrowser}))),pe=y(()=>import("./ConfigEditor-KAL2MLQO.js").then(e=>({default:e.ConfigEditor}))),le=y(()=>import("./StatsViewer-XOOMUPGL.js").then(e=>({default:e.StatsViewer}))),me=y(()=>import("./WordLookup-MWWYQBCM.js").then(e=>({default:e.WordLookup}))),de=y(()=>import("./HelpScreen-O5RSN3C4.js").then(e=>({default:e.HelpScreen})));function fe(){return i(ne,{alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:i(ie,{color:a.muted,children:"\u2026"})})}function _e({initial:e,initialCfg:t,inline:s=!1}){return i(E,{initialCfg:t,children:i(he,{children:i(D,{children:i(R,{disabled:!t.sounds.master,children:i(L,{initial:e,children:s?i(j,{inline:!0}):i(F,{children:i(j,{})})})})})})})}function he({children:e}){let{cfg:t}=k();return i(N,{pref:t.language,children:e})}function ge(e){if(e.name==="practice"){let t=e.params;return`practice:${t.dictId}:${t.chapterIndex}:${t.mode}:${t.stealth?"s":"n"}`}return e.name}function j({inline:e=!1}){let t=M(),{cfg:s}=k(),{exit:r}=se(),o=oe(null);ae((n,d)=>{d.ctrl&&n==="c"&&r()});let h=t.current,p=ge(h);o.current!==p&&(!e&&process.stdout.isTTY&&process.stdout.write("\x1B[2J\x1B[H"),o.current=p);let l=(()=>{switch(h.name){case"main":return i(Y,{cfg:s});case"practice":return i(ce,{params:h.params});case"dict":return i(ue,{params:h.params});case"config":return i(pe,{});case"stats":return i(le,{});case"word":return i(me,{});case"help":return i(de,{})}})();return i(re,{fallback:i(fe,{}),children:l})}import T from"chalk";import xe from"boxen";function tt(){A()}function q(e,t){let s=Math.floor(e/1e3),r=Math.floor(s/60),o=s%60;return t==="zh"?r===0?`${o} \u79D2`:`${r} \u5206 ${o} \u79D2`:r===0?`${o}s`:`${r}m ${o}s`}function be(e){return e>=90?T.green:e<75?T.dim:t=>t}function Se(e,t,s){let r=[],o=[t.report.duration,t.report.practiced,t.report.chapters,t.report.words,t.report.accuracy,t.report.wpm];e.newMistakeWords>0&&o.push(t.report.newMistakes);let h=Math.max(...o.map(S)),p=d=>d+" ".repeat(Math.max(0,h-S(d))),l=(d,m)=>`${T.dim(p(d))} ${m}`;r.push(l(t.report.duration,q(e.totalDurationMs,s))),r.push(l(t.report.practiced,q(e.practiceMs,s))),r.push(l(t.report.chapters,String(e.chaptersCompleted))),r.push(l(t.report.words,String(e.wordCount)));let n=Math.round(e.accuracy*1e3)/10;return r.push(l(t.report.accuracy,be(n)(`${n}%`))),r.push(l(t.report.wpm,String(e.wpm))),e.newMistakeWords>0&&r.push(l(t.report.newMistakes,T.red(String(e.newMistakeWords)))),r.push(""),r.push(T.dim.italic(t.report.farewell)),r}function rt(e,t,s){if(e.chaptersCompleted===0)return;let r=Se(e,t,s).join(`
2
- `);console.log(xe(r,{title:T.bold.cyan(t.report.title),titleAlignment:"left",borderStyle:"round",borderColor:"gray",padding:{top:0,bottom:0,left:3,right:3},margin:{top:1,bottom:1,left:2,right:0}}))}export{W as a,_e as b,tt as c,rt as d};
3
- //# sourceMappingURL=chunk-4EJYJITR.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/util/altscreen.ts","../src/ui/App.tsx","../src/ui/Fullscreen.tsx","../src/ui/screens/MainMenu.tsx","../src/util/report.ts"],"sourcesContent":["// Shared alt-screen state. Both the command entry point (which writes\n// ENTER before render() to avoid Ink's first frame leaking onto the main\n// screen) and Fullscreen.tsx (which used to write ENTER inside useEffect)\n// route through this module so enter/leave are idempotent.\n\nconst ENTER = '\\x1b[?1049h\\x1b[?25l\\x1b[2J\\x1b[H';\nconst LEAVE = '\\x1b[?25h\\x1b[?1049l';\n\nlet active = false;\nlet signalsRegistered = false;\n\nfunction ensureSignals(): void {\n if (signalsRegistered) return;\n signalsRegistered = true;\n const cleanup = () => {\n if (!active) return;\n try {\n process.stdout.write(LEAVE);\n } catch {\n // stream may be closed\n }\n active = false;\n };\n process.once('exit', cleanup);\n process.once('SIGINT', () => {\n cleanup();\n process.exit(130);\n });\n process.once('SIGTERM', () => {\n cleanup();\n process.exit(143);\n });\n}\n\nexport function enterAltScreen(): void {\n if (active) return;\n if (!process.stdout.isTTY) return;\n if (process.env.QWERTY_NO_ALTSCREEN === '1') return;\n ensureSignals();\n process.stdout.write(ENTER);\n active = true;\n}\n\nexport function leaveAltScreen(): void {\n if (!active) return;\n try {\n process.stdout.write(LEAVE);\n } catch {\n // stream may be closed\n }\n active = false;\n}\n\nexport function isAltScreenActive(): boolean {\n return active;\n}\n","import { Suspense, lazy, useRef, type ReactNode } from 'react';\nimport { Box, Text, useApp, useInput } from 'ink';\nimport { NavProvider, useNav, type ScreenFrame } from './nav.js';\nimport { Fullscreen } from './Fullscreen.js';\nimport { AudioStatusProvider } from './audio-context.js';\nimport { AppStateProvider, useAppState } from './app-state.js';\nimport { StringsProvider } from '../i18n/context.js';\nimport { RegistryProvider } from './registry-context.js';\nimport { MainMenu } from './screens/MainMenu.js';\nimport { PALETTE } from './components/BigWord.js';\nimport type { Config } from '../infra/config-store.js';\n\n// MainMenu stays eager — it's the initial screen for `qwerty` (no args) and\n// must render instantly. All other screens are lazy so their module graphs\n// (DictBrowser pulls registry helpers, StatsViewer pulls stats domain, etc.)\n// only load when the user navigates to them.\nconst PracticeScreen = lazy(() =>\n import('./screens/PracticeScreen.js').then((m) => ({ default: m.PracticeScreen })),\n);\nconst DictBrowser = lazy(() =>\n import('./screens/DictBrowser.js').then((m) => ({ default: m.DictBrowser })),\n);\nconst ConfigEditor = lazy(() =>\n import('./screens/ConfigEditor.js').then((m) => ({ default: m.ConfigEditor })),\n);\nconst StatsViewer = lazy(() =>\n import('./screens/StatsViewer.js').then((m) => ({ default: m.StatsViewer })),\n);\nconst WordLookup = lazy(() =>\n import('./screens/WordLookup.js').then((m) => ({ default: m.WordLookup })),\n);\nconst HelpScreen = lazy(() =>\n import('./screens/HelpScreen.js').then((m) => ({ default: m.HelpScreen })),\n);\n\nfunction LazyFallback() {\n return (\n <Box alignItems=\"center\" justifyContent=\"center\" width=\"100%\" height=\"100%\">\n <Text color={PALETTE.muted}>…</Text>\n </Box>\n );\n}\n\nexport function App({\n initial,\n initialCfg,\n inline = false,\n}: {\n initial: ScreenFrame;\n initialCfg: Config;\n inline?: boolean;\n}) {\n return (\n <AppStateProvider initialCfg={initialCfg}>\n <LangBridge>\n <RegistryProvider>\n <AudioStatusProvider disabled={!initialCfg.sounds.master}>\n <NavProvider initial={initial}>\n {inline ? <Router inline /> : <Fullscreen><Router /></Fullscreen>}\n </NavProvider>\n </AudioStatusProvider>\n </RegistryProvider>\n </LangBridge>\n </AppStateProvider>\n );\n}\n\nfunction LangBridge({ children }: { children: ReactNode }) {\n const { cfg } = useAppState();\n return <StringsProvider pref={cfg.language}>{children}</StringsProvider>;\n}\n\nfunction screenKey(frame: ScreenFrame): string {\n if (frame.name === 'practice') {\n const p = frame.params;\n return `practice:${p.dictId}:${p.chapterIndex}:${p.mode}:${p.stealth ? 's' : 'n'}`;\n }\n return frame.name;\n}\n\nfunction Router({ inline = false }: { inline?: boolean }) {\n const nav = useNav();\n const { cfg } = useAppState();\n const { exit } = useApp();\n const lastKeyRef = useRef<string | null>(null);\n\n useInput((input, key) => {\n if (key.ctrl && input === 'c') exit();\n });\n\n const frame = nav.current;\n const key = screenKey(frame);\n if (lastKeyRef.current !== key) {\n if (!inline && process.stdout.isTTY) process.stdout.write('\\x1b[2J\\x1b[H');\n lastKeyRef.current = key;\n }\n\n const screen = (() => {\n switch (frame.name) {\n case 'main':\n return <MainMenu cfg={cfg} />;\n case 'practice':\n return <PracticeScreen params={frame.params} />;\n case 'dict':\n return <DictBrowser params={frame.params} />;\n case 'config':\n return <ConfigEditor />;\n case 'stats':\n return <StatsViewer />;\n case 'word':\n return <WordLookup />;\n case 'help':\n return <HelpScreen />;\n }\n })();\n\n return <Suspense fallback={<LazyFallback />}>{screen}</Suspense>;\n}\n","import { useEffect, useState, type ReactNode } from 'react';\nimport { Box, useStdout } from 'ink';\nimport { enterAltScreen, leaveAltScreen } from '../util/altscreen.js';\n\nexport function Fullscreen({ children }: { children: ReactNode }) {\n const { stdout } = useStdout();\n const [size, setSize] = useState(() => ({\n rows: stdout?.rows ?? 24,\n cols: stdout?.columns ?? 80,\n }));\n\n useEffect(() => {\n // enterAltScreen is idempotent — if the command entry already wrote the\n // ENTER sequence pre-render (the normal path for menu / non-stealth\n // practice), this is a no-op. Acts as a safety net for any code path\n // that mounts the App without pre-render alt-screen setup.\n enterAltScreen();\n\n const onResize = () => {\n setSize({ rows: process.stdout.rows ?? 24, cols: process.stdout.columns ?? 80 });\n };\n process.stdout.on('resize', onResize);\n\n return () => {\n process.stdout.off('resize', onResize);\n leaveAltScreen();\n };\n }, []);\n\n return (\n <Box width={size.cols} height={size.rows} flexDirection=\"column\">\n {children}\n </Box>\n );\n}\n","import { useState } from 'react';\nimport { Box, Text, useApp, useInput } from 'ink';\nimport { useNav } from '../nav.js';\nimport { useAudioStatus } from '../audio-context.js';\nimport { useStrings } from '../../i18n/context.js';\nimport { useDictName } from '../registry-context.js';\nimport { PALETTE } from '../components/BigWord.js';\nimport { visibleWidth } from '../../util/text.js';\nimport { truncateName } from '../../util/dict-name.js';\nimport { setPostExitAction } from '../../util/post-exit-action.js';\nimport type { Config } from '../../infra/config-store.js';\n\ntype Item = { key: string; label: string; hint: string; run: () => void };\n\nexport function MainMenu({ cfg }: { cfg: Config }) {\n const [selected, setSelected] = useState(0);\n const { exit } = useApp();\n const nav = useNav();\n const audio = useAudioStatus();\n const t = useStrings();\n const defaultDictName = useDictName(cfg.defaultDict);\n\n const m = t.mainMenu.items;\n const startPractice = (stealth: boolean) => {\n // Stealth needs Ink rendered in inline mode (no alt-screen), but the menu\n // is currently running inside an alt-screen render(). Hand the dict +\n // mode off via a single-shot store and exit Ink; the CLI entry point\n // consumes the action and re-renders inline.\n if (stealth && cfg.defaultDict) {\n setPostExitAction({\n type: 'stealth',\n dictId: cfg.defaultDict,\n chapterIndex: 0,\n mode: cfg.defaultMode,\n });\n exit();\n return;\n }\n if (cfg.defaultDict) {\n nav.navigate({\n name: 'practice',\n params: {\n dictId: cfg.defaultDict,\n chapterIndex: 0,\n mode: cfg.defaultMode,\n stealth,\n },\n });\n } else {\n nav.navigate({ name: 'dict', params: { pickerMode: 'choose-then-practice' } });\n }\n };\n\n const items: Item[] = [\n {\n key: 'p',\n label: m.practiceLabel,\n hint: cfg.defaultDict\n ? m.practiceHintWith(truncateName(defaultDictName, 24))\n : m.practiceHintNone,\n run: () => startPractice(cfg.stealth === 'default'),\n },\n ];\n\n if (cfg.stealth === 'menu' || cfg.stealth === 'default') {\n items.push({\n key: 'b',\n label: m.stealthLabel,\n hint: m.stealthHint,\n run: () => startPractice(true),\n });\n }\n\n items.push(\n { key: 'd', label: m.dictLabel, hint: m.dictHint, run: () => nav.navigate({ name: 'dict' }) },\n { key: 'w', label: m.wordLabel, hint: m.wordHint, run: () => nav.navigate({ name: 'word' }) },\n { key: 's', label: m.statsLabel, hint: m.statsHint, run: () => nav.navigate({ name: 'stats' }) },\n { key: 'c', label: m.configLabel, hint: m.configHint, run: () => nav.navigate({ name: 'config' }) },\n { key: 'q', label: m.quitLabel, hint: m.quitHint, run: () => exit() },\n );\n\n const labelW = Math.max(...items.map((it) => visibleWidth(it.label))) + 4;\n\n useInput((input, key) => {\n if (key.escape) {\n exit();\n return;\n }\n if (key.upArrow) setSelected((i) => (i - 1 + items.length) % items.length);\n if (key.downArrow) setSelected((i) => (i + 1) % items.length);\n if (key.return) {\n items[selected]!.run();\n return;\n }\n if (input === '?') {\n nav.navigate({ name: 'help' });\n return;\n }\n for (const it of items) {\n if (input === it.key) {\n it.run();\n return;\n }\n }\n });\n\n return (\n <Box flexDirection=\"column\" paddingX={2} paddingY={1} width=\"100%\">\n {cfg.stealth === '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[] = [\n t.report.duration,\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 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 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 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.chaptersCompleted === 0) return;\n const content = buildLines(r, t, lang).join('\\n');\n console.log(\n boxen(content, {\n title: chalk.bold.cyan(t.report.title),\n titleAlignment: 'left',\n borderStyle: 'round',\n borderColor: 'gray',\n padding: { top: 0, bottom: 0, left: 3, right: 3 },\n margin: { top: 1, bottom: 1, left: 2, right: 0 },\n }),\n );\n}\n"],"mappings":"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","names":["ENTER","LEAVE","active","signalsRegistered","ensureSignals","cleanup","enterAltScreen","leaveAltScreen","Suspense","lazy","useRef","Box","Text","useApp","useInput","useEffect","useState","Box","useStdout","jsx","Fullscreen","children","stdout","useStdout","size","setSize","useState","useEffect","enterAltScreen","onResize","leaveAltScreen","Box","useState","Box","Text","useApp","useInput","jsx","jsxs","MainMenu","cfg","selected","setSelected","useState","exit","useApp","nav","useNav","audio","useAudioStatus","t","useStrings","defaultDictName","useDictName","m","startPractice","stealth","setPostExitAction","items","truncateName","labelW","it","visibleWidth","useInput","input","key","i","Box","Text","PALETTE","active","pad","jsx","PracticeScreen","lazy","m","DictBrowser","ConfigEditor","StatsViewer","WordLookup","HelpScreen","LazyFallback","Box","Text","PALETTE","App","initial","initialCfg","inline","AppStateProvider","LangBridge","RegistryProvider","AudioStatusProvider","NavProvider","Router","Fullscreen","children","cfg","useAppState","StringsProvider","screenKey","frame","p","nav","useNav","exit","useApp","lastKeyRef","useRef","useInput","input","key","screen","MainMenu","Suspense","chalk","boxen","ensureMainScreen","leaveAltScreen","fmtDuration","ms","lang","total","m","s","accColor","pct","chalk","buildLines","r","lines","allLabels","labelW","visibleWidth","padLabel","row","label","value","accPct","printSessionReport","content","boxen"]}
@@ -1,2 +0,0 @@
1
- import{a as w,g as h,h as S}from"./chunk-E6BBQALJ.js";import{z as i}from"zod";var D=i.object({ts:i.string(),dictId:i.string(),chapter:i.number().int().nonnegative(),mode:i.string(),wordCount:i.number().int().nonnegative(),errors:i.number().int().nonnegative(),durationMs:i.number().int().nonnegative(),perWordErrors:i.record(i.string(),i.number().int().nonnegative()).default({})});async function k(e){await h(w.stats,e)}async function C(){return(await S(w.stats)).map(t=>D.safeParse(t)).filter(t=>t.success).map(t=>t.data)}function y(e){if(e.durationMs===0)return 0;let t=e.durationMs/6e4;return Math.round(e.wordCount/t*10)/10}function b(e){if(e.wordCount===0)return 1;let t=Object.keys(e.perWordErrors).length;return Math.max(0,Math.min(1,(e.wordCount-t)/e.wordCount))}function R(e,t=new Date){if(e.length===0)return 0;let a=new Set;for(let n of e)a.add(n.ts.slice(0,10));let r=0,s=new Date(t);for(;;){let n=s.toISOString().slice(0,10);if(!a.has(n))break;r++,s.setUTCDate(s.getUTCDate()-1)}return r}var l=["\u2581","\u2582","\u2583","\u2584","\u2585","\u2586","\u2587","\u2588"];function v(e){if(e.length===0)return"";let t=Math.max(...e,1),a=Math.min(...e,0),r=Math.max(1,t-a);return e.map(s=>{let n=Math.floor((s-a)/r*(l.length-1));return l[Math.max(0,Math.min(l.length-1,n))]}).join("")}function P(e,t,a=new Date){if(e.length===0||t<=0)return{avgWpm:0,peakWpmLifetime:0,avgAccPct:0,sessionsInWindow:0,days:t};let r=new Date(a);r.setUTCDate(r.getUTCDate()-(t-1));let s=r.toISOString().slice(0,10),n=0,m=0,u=0,o=0,p=0;for(let c of e){let f=y(c);f>p&&(p=f),!(c.ts.slice(0,10)<s)&&(o++,n+=c.wordCount,m+=f*c.wordCount,u+=b(c)*c.wordCount)}if(n===0)return{avgWpm:0,peakWpmLifetime:p,avgAccPct:0,sessionsInWindow:o,days:t};let g=Math.round(m/n*10)/10,d=Math.round(u/n*1e3)/10;return{avgWpm:g,peakWpmLifetime:p,avgAccPct:d,sessionsInWindow:o,days:t}}function A(e,t,a=new Date){let r=[],s=new Map;for(let m of e){let u=m.ts.slice(0,10),o=s.get(u)??[];o.push(m),s.set(u,o)}let n=new Date(a);n.setUTCDate(n.getUTCDate()-(t-1));for(let m=0;m<t;m++){let u=n.toISOString().slice(0,10),o=s.get(u)??[];if(o.length===0)r.push({date:u,wpm:0,accuracy:0,sessions:0});else{let p=o.reduce((d,c)=>d+y(c),0)/o.length,g=o.reduce((d,c)=>d+b(c),0)/o.length;r.push({date:u,wpm:p,accuracy:g,sessions:o.length})}n.setUTCDate(n.getUTCDate()+1)}return r}export{k as a,C as b,y as c,b as d,R as e,v as f,P as g,A as h};
2
- //# sourceMappingURL=chunk-HJIGZU3E.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/domain/stats.ts"],"sourcesContent":["import { z } from 'zod';\nimport { paths } from '../infra/paths.js';\nimport { appendJsonl, readJsonl } from '../infra/fs-store.js';\n\nexport const SessionRecordSchema = z.object({\n ts: z.string(),\n dictId: z.string(),\n chapter: z.number().int().nonnegative(),\n mode: z.string(),\n wordCount: z.number().int().nonnegative(),\n errors: z.number().int().nonnegative(),\n durationMs: z.number().int().nonnegative(),\n perWordErrors: z.record(z.string(), z.number().int().nonnegative()).default({}),\n});\n\nexport type SessionRecord = z.infer<typeof SessionRecordSchema>;\n\nexport async function appendSession(record: SessionRecord): Promise<void> {\n await appendJsonl(paths.stats, record);\n}\n\nexport async function loadSessions(): Promise<SessionRecord[]> {\n const rows = await readJsonl<unknown>(paths.stats);\n return rows\n .map((r) => SessionRecordSchema.safeParse(r))\n .filter((r): r is { success: true; data: SessionRecord } => r.success)\n .map((r) => r.data);\n}\n\nexport function computeWPM(record: SessionRecord): number {\n if (record.durationMs === 0) return 0;\n const minutes = record.durationMs / 60000;\n return Math.round((record.wordCount / minutes) * 10) / 10;\n}\n\nexport function accuracy(record: SessionRecord): number {\n // Word-level: fraction of words finished without any mistake (first-try rate).\n if (record.wordCount === 0) return 1;\n const wordsWithErrors = Object.keys(record.perWordErrors).length;\n return Math.max(0, Math.min(1, (record.wordCount - wordsWithErrors) / record.wordCount));\n}\n\nexport function dailyStreak(sessions: SessionRecord[], now = new Date()): number {\n if (sessions.length === 0) return 0;\n const days = new Set<string>();\n for (const s of sessions) days.add(s.ts.slice(0, 10));\n let streak = 0;\n const cur = new Date(now);\n while (true) {\n const key = cur.toISOString().slice(0, 10);\n if (!days.has(key)) break;\n streak++;\n cur.setUTCDate(cur.getUTCDate() - 1);\n }\n return streak;\n}\n\nconst SPARK = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];\n\nexport function sparkline(values: number[]): string {\n if (values.length === 0) return '';\n const max = Math.max(...values, 1);\n const min = Math.min(...values, 0);\n const range = Math.max(1, max - min);\n return values\n .map((v) => {\n const idx = Math.floor(((v - min) / range) * (SPARK.length - 1));\n return SPARK[Math.max(0, Math.min(SPARK.length - 1, idx))]!;\n })\n .join('');\n}\n\nexport type DailyMetric = 'wpm' | 'accuracy' | 'sessions';\n\nexport function dailyValues(\n sessions: SessionRecord[],\n days: number,\n metric: DailyMetric,\n now = new Date(),\n): number[] {\n const buckets = dailyBuckets(sessions, days, now);\n if (metric === 'wpm') return buckets.map((b) => b.wpm);\n if (metric === 'accuracy') return buckets.map((b) => b.accuracy * 100);\n return buckets.map((b) => b.sessions);\n}\n\nexport type WindowAggregate = {\n avgWpm: number;\n peakWpmLifetime: number;\n avgAccPct: number;\n sessionsInWindow: number;\n days: number;\n};\n\nexport function windowAggregate(\n sessions: SessionRecord[],\n days: number,\n now = new Date(),\n): WindowAggregate {\n if (sessions.length === 0 || days <= 0) {\n return { avgWpm: 0, peakWpmLifetime: 0, avgAccPct: 0, sessionsInWindow: 0, days };\n }\n const cutoff = new Date(now);\n cutoff.setUTCDate(cutoff.getUTCDate() - (days - 1));\n const cutoffKey = cutoff.toISOString().slice(0, 10);\n\n let wordsInWindow = 0;\n let wpmWeightedSum = 0;\n let accWeightedSum = 0;\n let sessionsInWindow = 0;\n let peakWpmLifetime = 0;\n for (const s of sessions) {\n const wpm = computeWPM(s);\n if (wpm > peakWpmLifetime) peakWpmLifetime = wpm;\n if (s.ts.slice(0, 10) < cutoffKey) continue;\n sessionsInWindow++;\n wordsInWindow += s.wordCount;\n wpmWeightedSum += wpm * s.wordCount;\n accWeightedSum += accuracy(s) * s.wordCount;\n }\n if (wordsInWindow === 0) {\n return { avgWpm: 0, peakWpmLifetime, avgAccPct: 0, sessionsInWindow, days };\n }\n const avgWpm = Math.round((wpmWeightedSum / wordsInWindow) * 10) / 10;\n const avgAccPct = Math.round((accWeightedSum / wordsInWindow) * 1000) / 10;\n return { avgWpm, peakWpmLifetime, avgAccPct, sessionsInWindow, days };\n}\n\nexport type DailyBucket = { date: string; wpm: number; accuracy: number; sessions: number };\n\nexport function dailyBuckets(sessions: SessionRecord[], days: number, now = new Date()): DailyBucket[] {\n const out: DailyBucket[] = [];\n const byDay = new Map<string, SessionRecord[]>();\n for (const s of sessions) {\n const key = s.ts.slice(0, 10);\n const arr = byDay.get(key) ?? [];\n arr.push(s);\n byDay.set(key, arr);\n }\n const cur = new Date(now);\n cur.setUTCDate(cur.getUTCDate() - (days - 1));\n for (let i = 0; i < days; i++) {\n const key = cur.toISOString().slice(0, 10);\n const todays = byDay.get(key) ?? [];\n if (todays.length === 0) {\n out.push({ date: key, wpm: 0, accuracy: 0, sessions: 0 });\n } else {\n const wpm = todays.reduce((a, s) => a + computeWPM(s), 0) / todays.length;\n const acc = todays.reduce((a, s) => a + accuracy(s), 0) / todays.length;\n out.push({ date: key, wpm, accuracy: acc, sessions: todays.length });\n }\n cur.setUTCDate(cur.getUTCDate() + 1);\n }\n return out;\n}\n"],"mappings":"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","names":["z","SessionRecordSchema","z","appendSession","record","appendJsonl","paths","loadSessions","readJsonl","r","computeWPM","minutes","accuracy","wordsWithErrors","dailyStreak","sessions","now","days","s","streak","cur","key","SPARK","sparkline","values","max","min","range","v","idx","windowAggregate","sessions","days","now","cutoff","cutoffKey","wordsInWindow","wpmWeightedSum","accWeightedSum","sessionsInWindow","peakWpmLifetime","s","wpm","computeWPM","accuracy","avgWpm","avgAccPct","dailyBuckets","out","byDay","key","arr","cur","i","todays","a","acc"]}
@@ -1,2 +0,0 @@
1
- import{createContext as k,useContext as E,useState as v,useCallback as g}from"react";import{jsx as L}from"react/jsx-runtime";var h=k(null);function N({initial:t,children:e}){let[n,r]=v([t]),s=g(i=>{r(o=>[...o,i])},[]),a=g(i=>{r(o=>o.length===0?[i]:[...o.slice(0,-1),i])},[]),u=g(()=>{r(i=>i.length>1?i.slice(0,-1):i)},[]),c=g(i=>{r([i])},[]),l=n[n.length-1];return L(h.Provider,{value:{current:l,stack:n,navigate:s,replace:a,back:u,reset:c},children:e})}function D(){let t=E(h);if(!t)throw new Error("useNav must be used inside NavProvider");return t}import{createContext as x,useContext as S,useMemo as $}from"react";var d={app:{title:"qwerty",subtitle:"typing practice for the terminal"},common:{back:"back",quit:"quit",on:"on",off:"off",cancel:"cancel"},mainMenu:{items:{practiceLabel:"Practice",practiceHintWith:t=>`start ${t}`,practiceHintNone:"pick a dictionary",dictLabel:"Dictionaries",dictHint:"browse, pull, set default",wordLabel:"Word lookup",wordHint:"search local dicts",statsLabel:"Stats",statsHint:"history & trends",configLabel:"Config",configHint:"edit preferences",stealthLabel:"Stealth",stealthHint:"quiet practice mode",quitLabel:"Quit",quitHint:"Esc or Ctrl+C also exits"},hint:"\u2191/\u2193 navigate \xB7 Enter select \xB7 letters jump",helpHint:"? help"},dict:{title:"Dictionaries",loading:"loading dictionaries\u2026",entries:t=>`${t} entries`,filterPlaceholder:"type to filter",local:"local \u2713",notLocal:"not local",defaultMark:"default \u2605",tagsLabel:t=>`tags: ${t}`,wordsLabel:t=>`${t} words`,pulling:t=>`pulling ${t}\u2026`,removing:t=>`removing ${t}\u2026`,errorOn:(t,e)=>`error on ${t}: ${e}`,footer:"\u2191/\u2193 select \xB7 Enter actions \xB7 Ctrl+K more \xB7 Esc back",action:{title:"current dictionary",setDefault:"set as default",practice:"practice now",delete:"delete local"},command:{title:"more actions",pull:"pull selected",import:"import .json",refreshList:"update dictionary list"}},config:{title:"Config",fields:{defaultDict:"default dict",defaultMode:"default mode",accent:"accent",mirror:"dict mirror",chapterSize:"chapter size",autoplayPronunciation:"autoplay pronunciation",soundsMaster:"sounds master",soundsKeystroke:"sounds keystroke",soundsFeedback:"sounds feedback",soundsPronunciationRate:"pronunciation rate",soundsPronunciationSource:"pronunciation source",language:"language",stealth:"stealth mode"},enumValues:{stealth:{off:"off",menu:"show in menu",default:"default practice"},soundsPronunciationSource:{youdao:"Youdao",dictapi:"Wiktionary"},defaultMode:{order:"order",dictation:"dictation",review:"review",random:"random",loop:"loop"}},hints:{editing:"type to edit \xB7 Enter save \xB7 Esc cancel",bool:"space toggle \xB7 \u2191/\u2193 move \xB7 Esc back",enum:"\u2190/\u2192 cycle \xB7 \u2191/\u2193 move \xB7 Esc back",dictRef:"Enter pick dict \xB7 \u2191/\u2193 move \xB7 Esc back",stringOrInt:"Enter edit \xB7 \u2191/\u2193 move \xB7 Esc back"}},stats:{title:"Stats \xB7 overview",loading:"loading stats\u2026",none:"No practice history yet.",nonePractice:"Run a practice session first.",lifetime:"lifetime",sessions:"sessions",words:"words",errors:"errors",wpm:"wpm",accuracy:"accuracy",streak:"streak",last:t=>`last ${t} days (\u2190/\u2192 cycle window)`,cycleWindow:"\u2190/\u2192 cycle window \xB7 Esc back",recent:"recent sessions",topMistakes:"top mistakes",footer:"\u2190/\u2192 cycle window \xB7 Esc back",maxLabel:"max",recentUnits:{words:"w",errors:"err",wpm:"wpm"},multiDictSuffix:t=>` +${t} more`,bars:{speed:"speed",accuracy:"accuracy",sessions:"sessions"}},word:{title:"Word lookup",indexing:"indexing local dictionaries\u2026",none:"No local dictionaries.",pullFirst:"Pull one in Dictionaries first.",countAcross:t=>`${t} words across local dicts`,noMatches:t=>`no matches for "${t}"`,inDict:t=>`in: ${t}`,mistakes:(t,e)=>`mistakes: ${t} (last ${e})`,footer:"type to filter \xB7 \u2191/\u2193 select \xB7 Esc back"},practice:{loading:"loading\u2026",paused:"PAUSED",chapterComplete:"CHAPTER COMPLETE",chapterLabel:(t,e)=>`chapter ${t}/${e}`,reviewLabel:"review",statusBar:{mode:"mode",accent:"accent"},modes:{order:"order",dictation:"dictation",review:"review",random:"random",loop:"loop"},accents:{us:"us",uk:"uk"},statCards:{words:"words",errors:"errors",wpm:"wpm",accuracy:"accuracy",elapsed:t=>`elapsed ${t}`},pause:{title:"PAUSED",chapter:(t,e)=>`chapter ${t}/${e}`,progress:(t,e)=>`${t}/${e}`,hint:"Enter resume \xB7 Esc back to menu"},summary:{loopAgain:"again",nextChapter:"next chapter",reviewMistakes:"review mistakes",backMenu:"back to menu"},footers:{typing:"Ctrl+N skip \xB7 Esc pause \xB7 Tab replay"},errors:{noMistakes:"No mistakes to review yet. Practice some chapters first.",dictEmpty:t=>`Dictionary ${t} is empty.`,unknown:"Unknown error"},imeWarning:"! Non-English input detected \u2014 switch your IME to English.",imeWarningShort:"! IME",audioWarningShort:"! audio"},audio:{noPlayer:"! No audio player found on PATH (looked for afplay/ffplay/mpg123/paplay/aplay/powershell). Sounds disabled."},report:{title:"Session summary",duration:"duration",practiced:"practiced",chapters:"chapters",words:"words",accuracy:"accuracy",wpm:"wpm",newMistakes:"new mistakes",farewell:"see you next time."},help:{title:"Help",subtitle:"all shortcuts",sections:{main:"main menu",practice:"practice",dict:"dictionaries",config:"config",stats:"stats",word:"word lookup",global:"global"},keys:{navigate:"\u2191/\u2193 navigate items",select:"Enter confirm / continue",letterJump:"letter jump to menu item",pause:"Esc pause practice",skip:"Ctrl+N skip current word (neutral)",replay:"Tab replay pronunciation",resume:"Enter resume from pause",backMenu:"Esc back to previous screen",backScreen:"Esc close panel or back",nextChapter:"Enter next chapter",reviewMistakes:"m review mistakes",filter:"type to filter list",itemActions:"Enter open actions panel",moreActions:"Ctrl+K more actions panel",cycleWindow:"\u2190/\u2192 cycle day window",stealthToggle:"Ctrl+I toggle stealth info row",helpScreen:"? open this help screen",quit:"Ctrl+C quit immediately"},footer:"Esc back"},stealth:{paused:"paused",chapterDone:"chapter done",resumeHint:"Enter resume \xB7 Esc menu",nextHint:"Enter next \xB7 Esc menu",pausedHintRight:"Enter resume",nextHintRight:"Enter next",infoChipLabel:"info",infoFmt:(t,e,n,r,s,a)=>`${t} \xB7 ${e} \xB7 ${n}/${r} \xB7 ${s} wpm \xB7 ${a}%`}},p={app:{title:"qwerty",subtitle:"\u7EC8\u7AEF\u952E\u76D8\u7EC3\u4E60"},common:{back:"\u8FD4\u56DE",quit:"\u9000\u51FA",on:"\u5F00",off:"\u5173",cancel:"\u53D6\u6D88"},mainMenu:{items:{practiceLabel:"\u7EC3\u4E60",practiceHintWith:t=>`\u5F00\u59CB ${t}`,practiceHintNone:"\u8BF7\u5148\u9009\u8BCD\u5178",dictLabel:"\u8BCD\u5178",dictHint:"\u6D4F\u89C8\u3001\u4E0B\u8F7D\u3001\u8BBE\u4E3A\u9ED8\u8BA4",wordLabel:"\u67E5\u8BCD",wordHint:"\u5728\u672C\u5730\u8BCD\u5178\u4E2D\u641C\u7D22",statsLabel:"\u7EDF\u8BA1",statsHint:"\u5386\u53F2\u4E0E\u8D8B\u52BF",configLabel:"\u8BBE\u7F6E",configHint:"\u4FEE\u6539\u504F\u597D",stealthLabel:"\u795E\u9690",stealthHint:"\u795E\u9690\u7EC3\u4E60\u6A21\u5F0F",quitLabel:"\u9000\u51FA",quitHint:"Esc \u6216 Ctrl+C \u9000\u51FA"},hint:"\u2191/\u2193 \u79FB\u52A8 \xB7 Enter \u786E\u8BA4 \xB7 \u5B57\u6BCD\u76F4\u8FBE",helpHint:"? \u5E2E\u52A9"},dict:{title:"\u8BCD\u5178",loading:"\u52A0\u8F7D\u8BCD\u5178\u4E2D\u2026",entries:t=>`${t} \u90E8\u8BCD\u5178`,filterPlaceholder:"\u8F93\u5165\u8FC7\u6EE4",local:"\u5DF2\u4E0B\u8F7D \u2713",notLocal:"\u672A\u4E0B\u8F7D",defaultMark:"\u9ED8\u8BA4 \u2605",tagsLabel:t=>`\u6807\u7B7E:${t}`,wordsLabel:t=>`${t} \u8BCD`,pulling:t=>`\u62C9\u53D6 ${t} \u4E2D\u2026`,removing:t=>`\u5220\u9664 ${t} \u4E2D\u2026`,errorOn:(t,e)=>`${t} \u51FA\u9519:${e}`,footer:"\u2191/\u2193 \u9009\u62E9 \xB7 Enter \u64CD\u4F5C \xB7 Ctrl+K \u66F4\u591A \xB7 Esc \u8FD4\u56DE",action:{title:"\u5F53\u524D\u8BCD\u5178",setDefault:"\u8BBE\u4E3A\u9ED8\u8BA4",practice:"\u7ACB\u5373\u7EC3\u4E60",delete:"\u5220\u9664\u672C\u5730"},command:{title:"\u66F4\u591A\u529F\u80FD",pull:"\u62C9\u53D6\u9009\u4E2D",import:"\u5BFC\u5165 .json",refreshList:"\u66F4\u65B0\u8BCD\u5178\u5217\u8868"}},config:{title:"\u8BBE\u7F6E",fields:{defaultDict:"\u9ED8\u8BA4\u8BCD\u5178",defaultMode:"\u9ED8\u8BA4\u6A21\u5F0F",accent:"\u53D1\u97F3",mirror:"\u8BCD\u5178\u955C\u50CF\u6E90",chapterSize:"\u7AE0\u8282\u5355\u8BCD\u6570",autoplayPronunciation:"\u81EA\u52A8\u64AD\u653E\u53D1\u97F3",soundsMaster:"\u97F3\u6548\u603B\u5F00\u5173",soundsKeystroke:"\u6309\u952E\u97F3",soundsFeedback:"\u53CD\u9988\u97F3",soundsPronunciationRate:"\u53D1\u97F3\u901F\u7387",soundsPronunciationSource:"\u53D1\u97F3\u97F3\u6E90",language:"\u8BED\u8A00",stealth:"\u795E\u9690\u6A21\u5F0F"},enumValues:{stealth:{off:"\u5173\u95ED",menu:"\u4E3B\u83DC\u5355\u663E\u793A",default:"\u9ED8\u8BA4\u7EC3\u4E60\u6A21\u5F0F"},soundsPronunciationSource:{youdao:"\u6709\u9053\u8BCD\u5178",dictapi:"\u7EF4\u57FA\u8BCD\u5178"},defaultMode:{order:"\u987A\u5E8F",dictation:"\u9ED8\u5199",review:"\u590D\u4E60",random:"\u4E71\u5E8F",loop:"\u5FAA\u73AF"}},hints:{editing:"\u8F93\u5165\u4FEE\u6539 \xB7 Enter \u4FDD\u5B58 \xB7 Esc \u53D6\u6D88",bool:"\u7A7A\u683C\u5207\u6362 \xB7 \u2191/\u2193 \u79FB\u52A8 \xB7 Esc \u8FD4\u56DE",enum:"\u2190/\u2192 \u5207\u6362 \xB7 \u2191/\u2193 \u79FB\u52A8 \xB7 Esc \u8FD4\u56DE",dictRef:"Enter \u9009\u8BCD\u5178 \xB7 \u2191/\u2193 \u79FB\u52A8 \xB7 Esc \u8FD4\u56DE",stringOrInt:"Enter \u7F16\u8F91 \xB7 \u2191/\u2193 \u79FB\u52A8 \xB7 Esc \u8FD4\u56DE"}},stats:{title:"\u7EDF\u8BA1 \xB7 \u6982\u89C8",loading:"\u52A0\u8F7D\u7EDF\u8BA1\u4E2D\u2026",none:"\u8FD8\u6CA1\u6709\u7EC3\u4E60\u8BB0\u5F55\u3002",nonePractice:"\u5148\u6765\u4E00\u6B21\u7EC3\u4E60\u5427\u3002",lifetime:"\u7D2F\u8BA1",sessions:"\u4F1A\u8BDD",words:"\u8BCD\u6570",errors:"\u9519\u8BEF",wpm:"\u901F\u5EA6",accuracy:"\u51C6\u786E\u7387",streak:"\u8FDE\u7EED\u5929\u6570",last:t=>`\u6700\u8FD1 ${t} \u5929 (\u2190/\u2192 \u5207\u6362\u7A97\u53E3)`,cycleWindow:"\u2190/\u2192 \u5207\u6362\u7A97\u53E3 \xB7 Esc \u8FD4\u56DE",recent:"\u6700\u8FD1\u4F1A\u8BDD",topMistakes:"\u9AD8\u9891\u9519\u8BCD",footer:"\u2190/\u2192 \u5207\u6362\u7A97\u53E3 \xB7 Esc \u8FD4\u56DE",maxLabel:"\u6700\u5927",recentUnits:{words:"\u8BCD",errors:"\u9519",wpm:"\u901F"},multiDictSuffix:t=>` \u7B49 ${t} \u90E8`,bars:{speed:"\u901F\u5EA6",accuracy:"\u51C6\u786E\u7387",sessions:"\u4F1A\u8BDD"}},word:{title:"\u67E5\u8BCD",indexing:"\u7D22\u5F15\u672C\u5730\u8BCD\u5178\u4E2D\u2026",none:"\u6CA1\u6709\u672C\u5730\u8BCD\u5178\u3002",pullFirst:"\u5148\u5728\u300C\u8BCD\u5178\u300D\u4E2D\u62C9\u53D6\u4E00\u90E8\u3002",countAcross:t=>`\u672C\u5730\u8BCD\u5178\u5171 ${t} \u8BCD`,noMatches:t=>`\u6CA1\u6709\u5339\u914D\u300C${t}\u300D\u7684\u8BCD`,inDict:t=>`\u6765\u6E90:${t}`,mistakes:(t,e)=>`\u9519\u8FC7 ${t} \u6B21 (\u6700\u8FD1 ${e})`,footer:"\u8F93\u5165\u8FC7\u6EE4 \xB7 \u2191/\u2193 \u9009\u62E9 \xB7 Esc \u8FD4\u56DE"},practice:{loading:"\u52A0\u8F7D\u4E2D\u2026",paused:"\u5DF2\u6682\u505C",chapterComplete:"\u672C\u7AE0\u5B8C\u6210",chapterLabel:(t,e)=>`\u7B2C ${t}/${e} \u7AE0`,reviewLabel:"\u590D\u4E60",statusBar:{mode:"\u6A21\u5F0F",accent:"\u53D1\u97F3"},modes:{order:"\u987A\u5E8F",dictation:"\u9ED8\u5199",review:"\u590D\u4E60",random:"\u4E71\u5E8F",loop:"\u5FAA\u73AF"},accents:{us:"\u7F8E",uk:"\u82F1"},statCards:{words:"\u8BCD\u6570",errors:"\u9519\u8BEF",wpm:"\u901F\u5EA6",accuracy:"\u51C6\u786E\u7387",elapsed:t=>`\u8017\u65F6 ${t}`},pause:{title:"\u5DF2\u6682\u505C",chapter:(t,e)=>`\u7B2C ${t}/${e} \u7AE0`,progress:(t,e)=>`${t}/${e}`,hint:"Enter \u7EE7\u7EED \xB7 Esc \u8FD4\u56DE\u83DC\u5355"},summary:{loopAgain:"\u518D\u6765\u4E00\u904D",nextChapter:"\u4E0B\u4E00\u7AE0",reviewMistakes:"\u590D\u4E60\u9519\u8BCD",backMenu:"\u8FD4\u56DE\u83DC\u5355"},footers:{typing:"Ctrl+N \u8DF3\u8FC7 \xB7 Esc \u6682\u505C \xB7 Tab \u91CD\u64AD"},errors:{noMistakes:"\u9519\u8BCD\u672C\u662F\u7A7A\u7684\u3002\u5148\u7EC3\u4E60\u51E0\u7AE0\u5427\u3002",dictEmpty:t=>`\u8BCD\u5178 ${t} \u662F\u7A7A\u7684\u3002`,unknown:"\u672A\u77E5\u9519\u8BEF"},imeWarning:"! \u68C0\u6D4B\u5230\u4E2D\u6587/\u65E5\u6587/\u97E9\u6587\u8F93\u5165\uFF0C\u8BF7\u5207\u6362\u5230\u82F1\u6587\u8F93\u5165\u6CD5",imeWarningShort:"! \u4E2D\u6587\u8F93\u5165",audioWarningShort:"! \u65E0\u97F3\u6548"},audio:{noPlayer:"! \u672A\u5728 PATH \u4E2D\u627E\u5230\u97F3\u9891\u64AD\u653E\u5668(\u5C1D\u8BD5 afplay/ffplay/mpg123/paplay/aplay/powershell)\u3002\u97F3\u6548\u5DF2\u7981\u7528\u3002"},report:{title:"\u672C\u6B21\u4F1A\u8BDD",duration:"\u603B\u65F6\u957F",practiced:"\u7EC3\u4E60\u7528\u65F6",chapters:"\u5B8C\u6210\u7AE0\u8282",words:"\u8BCD\u6570",accuracy:"\u51C6\u786E\u7387",wpm:"\u901F\u5EA6",newMistakes:"\u65B0\u9519\u8BCD",farewell:"\u4E0B\u6B21\u89C1\u3002"},help:{title:"\u5E2E\u52A9",subtitle:"\u5168\u90E8\u5FEB\u6377\u952E",sections:{main:"\u4E3B\u83DC\u5355",practice:"\u7EC3\u4E60",dict:"\u8BCD\u5178",config:"\u8BBE\u7F6E",stats:"\u7EDF\u8BA1",word:"\u67E5\u8BCD",global:"\u5168\u5C40"},keys:{navigate:"\u2191/\u2193 \u79FB\u52A8\u9009\u9879",select:"Enter \u786E\u8BA4 / \u7EE7\u7EED",letterJump:"\u5B57\u6BCD\u952E \u76F4\u8FBE\u83DC\u5355\u9879",pause:"Esc \u6682\u505C\u7EC3\u4E60",skip:"Ctrl+N \u8DF3\u8FC7\u5F53\u524D\u8BCD(\u4E0D\u8BA1\u9519)",replay:"Tab \u91CD\u64AD\u53D1\u97F3",resume:"Enter \u7EE7\u7EED\u7EC3\u4E60",backMenu:"Esc \u8FD4\u56DE\u4E0A\u4E00\u5C4F",backScreen:"Esc \u5173\u95ED\u9762\u677F / \u8FD4\u56DE",nextChapter:"Enter \u4E0B\u4E00\u7AE0",reviewMistakes:"m \u590D\u4E60\u9519\u8BCD",filter:"\u8F93\u5165 \u8FC7\u6EE4\u5217\u8868",itemActions:"Enter \u5F39\u51FA\u52A8\u4F5C\u9762\u677F",moreActions:"Ctrl+K \u5F39\u51FA\u66F4\u591A\u529F\u80FD",cycleWindow:"\u2190/\u2192 \u5207\u6362\u65E5\u7A97\u53E3",stealthToggle:"Ctrl+I \u5207\u6362\u795E\u9690\u4FE1\u606F\u884C",helpScreen:"? \u6253\u5F00\u672C\u5E2E\u52A9\u9875",quit:"Ctrl+C \u7ACB\u5373\u9000\u51FA"},footer:"Esc \u8FD4\u56DE"},stealth:{paused:"paused",chapterDone:"chapter done",resumeHint:"Enter resume \xB7 Esc menu",nextHint:"Enter next \xB7 Esc menu",pausedHintRight:"Enter \u7EE7\u7EED",nextHintRight:"Enter \u4E0B\u4E00\u7AE0",infoChipLabel:"\u4FE1\u606F",infoFmt:(t,e,n,r,s,a)=>`${t} \xB7 ${e} \xB7 ${n}/${r} \xB7 ${s} wpm \xB7 ${a}%`}};function b(t){if(!t)return null;let e=t.toLowerCase();return e.startsWith("zh")?"zh":e.startsWith("en")?"en":null}function m(t){if(t==="zh"||t==="en")return t;let e=process.env.LC_ALL||process.env.LC_MESSAGES||process.env.LANG||process.env.LANGUAGE,n=b(e);if(n)return n;try{let r=Intl.DateTimeFormat().resolvedOptions().locale,s=b(r);if(s)return s}catch{}return"en"}import{jsx as C}from"react/jsx-runtime";var w=x(null);function I({pref:t,children:e}){let n=$(()=>{let r=m(t);return{lang:r,t:r==="zh"?p:d}},[t]);return C(w.Provider,{value:n,children:e})}function O(){let t=S(w);if(!t)throw new Error("useStrings must be used inside StringsProvider");return t.t}function K(t){let e=m(t);return{lang:e,t:e==="zh"?p:d}}import{Box as P,Text as M}from"ink";import{jsx as y}from"react/jsx-runtime";var f={accent:"#5eead4",muted:"#6b7280",text:"#e5e7eb",primary:"#7dcfff",success:"#86efac",warning:"#fbbf24",error:"#f87171"};function G({target:t,typed:e,error:n=!1,hideTarget:r=!1}){let s=[...t],a=[...e];return y(P,{paddingY:4,justifyContent:"center",children:s.map((u,c)=>{let l=c<a.length,i=r&&!l?"_":l?a[c]:u,o=n?f.error:l?f.accent:f.muted;return y(M,{bold:!0,color:o,children:i},c)})})}export{N as a,D as b,I as c,O as d,K as e,f,G as g};
2
- //# sourceMappingURL=chunk-UWTJMVJ6.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/ui/nav.tsx","../src/i18n/context.tsx","../src/i18n/strings.ts","../src/i18n/locale.ts","../src/ui/components/BigWord.tsx"],"sourcesContent":["import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';\n\nexport type ScreenName =\n | 'main'\n | 'practice'\n | 'dict'\n | 'config'\n | 'stats'\n | 'word'\n | 'help';\n\nexport type PracticeParams = {\n dictId: string;\n chapterIndex: number;\n mode: 'order' | 'dictation' | 'review' | 'random' | 'loop';\n stealth?: boolean;\n};\n\nexport type DictParams = {\n pickerMode?: 'set-default' | 'choose-then-practice';\n};\n\nexport type ScreenFrame =\n | { name: 'main' }\n | { name: 'practice'; params: PracticeParams }\n | { name: 'dict'; params?: DictParams }\n | { name: 'config' }\n | { name: 'stats' }\n | { name: 'word' }\n | { name: 'help' };\n\ntype NavContextValue = {\n current: ScreenFrame;\n stack: ScreenFrame[];\n navigate: (frame: ScreenFrame) => void;\n replace: (frame: ScreenFrame) => void;\n back: () => void;\n reset: (frame: ScreenFrame) => void;\n};\n\nconst NavContext = createContext<NavContextValue | null>(null);\n\nexport function NavProvider({ initial, children }: { initial: ScreenFrame; children: ReactNode }) {\n const [stack, setStack] = useState<ScreenFrame[]>([initial]);\n\n const navigate = useCallback((frame: ScreenFrame) => {\n setStack((s) => [...s, frame]);\n }, []);\n const replace = useCallback((frame: ScreenFrame) => {\n setStack((s) => (s.length === 0 ? [frame] : [...s.slice(0, -1), frame]));\n }, []);\n const back = useCallback(() => {\n setStack((s) => (s.length > 1 ? s.slice(0, -1) : s));\n }, []);\n const reset = useCallback((frame: ScreenFrame) => {\n setStack([frame]);\n }, []);\n\n const current = stack[stack.length - 1]!;\n return (\n <NavContext.Provider value={{ current, stack, navigate, replace, back, reset }}>\n {children}\n </NavContext.Provider>\n );\n}\n\nexport function useNav(): NavContextValue {\n const ctx = useContext(NavContext);\n if (!ctx) throw new Error('useNav must be used inside NavProvider');\n return ctx;\n}\n","import { createContext, useContext, useMemo, type ReactNode } from 'react';\nimport { en, zh, type Strings } from './strings.js';\nimport { detectLocale, type Lang, type LangPref } from './locale.js';\n\nconst StringsContext = createContext<{ lang: Lang; t: Strings } | null>(null);\n\nexport function StringsProvider({\n pref,\n children,\n}: {\n pref: LangPref;\n children: ReactNode;\n}) {\n const value = useMemo(() => {\n const lang = detectLocale(pref);\n return { lang, t: lang === 'zh' ? zh : en };\n }, [pref]);\n return <StringsContext.Provider value={value}>{children}</StringsContext.Provider>;\n}\n\nexport function useStrings(): Strings {\n const ctx = useContext(StringsContext);\n if (!ctx) throw new Error('useStrings must be used inside StringsProvider');\n return ctx.t;\n}\n\nexport function useLang(): Lang {\n const ctx = useContext(StringsContext);\n if (!ctx) throw new Error('useLang must be used inside StringsProvider');\n return ctx.lang;\n}\n\nexport function pickStrings(pref: LangPref): { lang: Lang; t: Strings } {\n const lang = detectLocale(pref);\n return { lang, t: lang === 'zh' ? zh : en };\n}\n","// Centralized string table for the qwerty-cli TUI.\n//\n// Print-only subcommands (qwerty config list, qwerty dict list, etc.) keep\n// English output regardless of locale so shell scripts stay predictable.\n// Only the interactive TUI is localized via this table.\n\nexport type Strings = {\n app: {\n title: string;\n subtitle: string;\n };\n common: {\n back: string;\n quit: string;\n on: string;\n off: string;\n cancel: string;\n };\n mainMenu: {\n items: {\n practiceLabel: string;\n practiceHintWith: (name: string) => string;\n practiceHintNone: string;\n dictLabel: string;\n dictHint: string;\n wordLabel: string;\n wordHint: string;\n statsLabel: string;\n statsHint: string;\n configLabel: string;\n configHint: string;\n stealthLabel: string;\n stealthHint: string;\n quitLabel: string;\n quitHint: string;\n };\n hint: string;\n helpHint: string;\n };\n dict: {\n title: string;\n loading: string;\n entries: (n: number) => string;\n filterPlaceholder: string;\n local: string;\n notLocal: string;\n defaultMark: string;\n tagsLabel: (tags: string) => string;\n wordsLabel: (n: number) => string;\n pulling: (id: string) => string;\n removing: (id: string) => string;\n errorOn: (id: string, msg: string) => string;\n footer: string;\n action: {\n title: string;\n setDefault: string;\n practice: string;\n delete: string;\n };\n command: {\n title: string;\n pull: string;\n import: string;\n refreshList: string;\n };\n };\n config: {\n title: string;\n fields: {\n defaultDict: string;\n defaultMode: string;\n accent: string;\n mirror: string;\n chapterSize: string;\n autoplayPronunciation: string;\n soundsMaster: string;\n soundsKeystroke: string;\n soundsFeedback: string;\n soundsPronunciationRate: string;\n soundsPronunciationSource: string;\n language: string;\n stealth: string;\n };\n enumValues: {\n stealth: { off: string; menu: string; default: string };\n soundsPronunciationSource: { youdao: string; dictapi: string };\n defaultMode: { order: string; dictation: string; review: string; random: string; loop: string };\n };\n hints: {\n editing: string;\n bool: string;\n enum: string;\n dictRef: string;\n stringOrInt: string;\n };\n };\n stats: {\n title: string;\n loading: string;\n none: string;\n nonePractice: string;\n lifetime: string;\n sessions: string;\n words: string;\n errors: string;\n wpm: string;\n accuracy: string;\n streak: string;\n last: (n: number) => string;\n cycleWindow: string;\n recent: string;\n topMistakes: string;\n footer: string;\n maxLabel: string;\n recentUnits: { words: string; errors: string; wpm: string };\n multiDictSuffix: (n: number) => string;\n bars: { speed: string; accuracy: string; sessions: string };\n };\n word: {\n title: string;\n indexing: string;\n none: string;\n pullFirst: string;\n countAcross: (n: number) => string;\n noMatches: (q: string) => string;\n inDict: (name: string) => string;\n mistakes: (n: number, date: string) => string;\n footer: string;\n };\n practice: {\n loading: string;\n paused: string;\n chapterComplete: string;\n chapterLabel: (c: number, t: number) => string;\n reviewLabel: string;\n statusBar: {\n mode: string;\n accent: string;\n };\n modes: {\n order: string;\n dictation: string;\n review: string;\n random: string;\n loop: string;\n };\n accents: {\n us: string;\n uk: string;\n };\n statCards: {\n words: string;\n errors: string;\n wpm: string;\n accuracy: string;\n elapsed: (t: string) => string;\n };\n pause: {\n title: string;\n chapter: (c: number, t: number) => string;\n progress: (completed: number, total: number) => string;\n hint: string;\n };\n summary: {\n loopAgain: string;\n nextChapter: string;\n reviewMistakes: string;\n backMenu: string;\n };\n footers: {\n typing: string;\n };\n errors: {\n noMistakes: string;\n dictEmpty: (id: string) => string;\n unknown: string;\n };\n imeWarning: string;\n imeWarningShort: string;\n audioWarningShort: string;\n };\n audio: {\n noPlayer: string;\n };\n report: {\n title: string;\n duration: string;\n practiced: string;\n chapters: string;\n words: string;\n accuracy: string;\n wpm: string;\n newMistakes: string;\n farewell: string;\n };\n help: {\n title: string;\n subtitle: string;\n sections: {\n main: string;\n practice: string;\n dict: string;\n config: string;\n stats: string;\n word: string;\n global: string;\n };\n keys: {\n navigate: string;\n select: string;\n letterJump: string;\n pause: string;\n skip: string;\n replay: string;\n resume: string;\n backMenu: string;\n backScreen: string;\n nextChapter: string;\n reviewMistakes: string;\n filter: string;\n itemActions: string;\n moreActions: string;\n cycleWindow: string;\n stealthToggle: string;\n helpScreen: string;\n quit: string;\n };\n footer: string;\n };\n stealth: {\n paused: string;\n chapterDone: string;\n resumeHint: string;\n nextHint: string;\n pausedHintRight: string;\n nextHintRight: string;\n infoChipLabel: string;\n infoFmt: (\n dict: string,\n chapter: string,\n completed: number,\n total: number,\n wpm: number,\n accPct: number,\n ) => string;\n };\n};\n\nexport const en: Strings = {\n app: {\n title: 'qwerty',\n subtitle: 'typing practice for the terminal',\n },\n common: {\n back: 'back',\n quit: 'quit',\n on: 'on',\n off: 'off',\n cancel: 'cancel',\n },\n mainMenu: {\n items: {\n practiceLabel: 'Practice',\n practiceHintWith: (name) => `start ${name}`,\n practiceHintNone: 'pick a dictionary',\n dictLabel: 'Dictionaries',\n dictHint: 'browse, pull, set default',\n wordLabel: 'Word lookup',\n wordHint: 'search local dicts',\n statsLabel: 'Stats',\n statsHint: 'history & trends',\n configLabel: 'Config',\n configHint: 'edit preferences',\n stealthLabel: 'Stealth',\n stealthHint: 'quiet practice mode',\n quitLabel: 'Quit',\n quitHint: 'Esc or Ctrl+C also exits',\n },\n hint: '↑/↓ navigate · Enter select · letters jump',\n helpHint: '? help',\n },\n dict: {\n title: 'Dictionaries',\n loading: 'loading dictionaries…',\n entries: (n) => `${n} entries`,\n filterPlaceholder: 'type to filter',\n local: 'local ✓',\n notLocal: 'not local',\n defaultMark: 'default ★',\n tagsLabel: (tags) => `tags: ${tags}`,\n wordsLabel: (n) => `${n} words`,\n pulling: (id) => `pulling ${id}…`,\n removing: (id) => `removing ${id}…`,\n errorOn: (id, msg) => `error on ${id}: ${msg}`,\n footer: '↑/↓ select · Enter actions · Ctrl+K more · Esc back',\n action: {\n title: 'current dictionary',\n setDefault: 'set as default',\n practice: 'practice now',\n delete: 'delete local',\n },\n command: {\n title: 'more actions',\n pull: 'pull selected',\n import: 'import .json',\n refreshList: 'update dictionary list',\n },\n },\n config: {\n title: 'Config',\n fields: {\n defaultDict: 'default dict',\n defaultMode: 'default mode',\n accent: 'accent',\n mirror: 'dict mirror',\n chapterSize: 'chapter size',\n autoplayPronunciation: 'autoplay pronunciation',\n soundsMaster: 'sounds master',\n soundsKeystroke: 'sounds keystroke',\n soundsFeedback: 'sounds feedback',\n soundsPronunciationRate: 'pronunciation rate',\n soundsPronunciationSource: 'pronunciation source',\n language: 'language',\n stealth: 'stealth mode',\n },\n enumValues: {\n stealth: { off: 'off', menu: 'show in menu', default: 'default practice' },\n soundsPronunciationSource: { youdao: 'Youdao', dictapi: 'Wiktionary' },\n defaultMode: { order: 'order', dictation: 'dictation', review: 'review', random: 'random', loop: 'loop' },\n },\n hints: {\n editing: 'type to edit · Enter save · Esc cancel',\n bool: 'space toggle · ↑/↓ move · Esc back',\n enum: '←/→ cycle · ↑/↓ move · Esc back',\n dictRef: 'Enter pick dict · ↑/↓ move · Esc back',\n stringOrInt: 'Enter edit · ↑/↓ move · Esc back',\n },\n },\n stats: {\n title: 'Stats · overview',\n loading: 'loading stats…',\n none: 'No practice history yet.',\n nonePractice: 'Run a practice session first.',\n lifetime: 'lifetime',\n sessions: 'sessions',\n words: 'words',\n errors: 'errors',\n wpm: 'wpm',\n accuracy: 'accuracy',\n streak: 'streak',\n last: (n) => `last ${n} days (←/→ cycle window)`,\n cycleWindow: '←/→ cycle window · Esc back',\n recent: 'recent sessions',\n topMistakes: 'top mistakes',\n footer: '←/→ cycle window · Esc back',\n maxLabel: 'max',\n recentUnits: { words: 'w', errors: 'err', wpm: 'wpm' },\n multiDictSuffix: (n) => ` +${n} more`,\n bars: { speed: 'speed', accuracy: 'accuracy', sessions: 'sessions' },\n },\n word: {\n title: 'Word lookup',\n indexing: 'indexing local dictionaries…',\n none: 'No local dictionaries.',\n pullFirst: 'Pull one in Dictionaries first.',\n countAcross: (n) => `${n} words across local dicts`,\n noMatches: (q) => `no matches for \"${q}\"`,\n inDict: (name) => `in: ${name}`,\n mistakes: (n, date) => `mistakes: ${n} (last ${date})`,\n footer: 'type to filter · ↑/↓ select · Esc back',\n },\n practice: {\n loading: 'loading…',\n paused: 'PAUSED',\n chapterComplete: 'CHAPTER COMPLETE',\n chapterLabel: (c, t) => `chapter ${c}/${t}`,\n reviewLabel: 'review',\n statusBar: {\n mode: 'mode',\n accent: 'accent',\n },\n modes: {\n order: 'order',\n dictation: 'dictation',\n review: 'review',\n random: 'random',\n loop: 'loop',\n },\n accents: {\n us: 'us',\n uk: 'uk',\n },\n statCards: {\n words: 'words',\n errors: 'errors',\n wpm: 'wpm',\n accuracy: 'accuracy',\n elapsed: (t) => `elapsed ${t}`,\n },\n pause: {\n title: 'PAUSED',\n chapter: (c, t) => `chapter ${c}/${t}`,\n progress: (completed, total) => `${completed}/${total}`,\n hint: 'Enter resume · Esc back to menu',\n },\n summary: {\n loopAgain: 'again',\n nextChapter: 'next chapter',\n reviewMistakes: 'review mistakes',\n backMenu: 'back to menu',\n },\n footers: {\n typing: 'Ctrl+N skip · Esc pause · Tab replay',\n },\n errors: {\n noMistakes: 'No mistakes to review yet. Practice some chapters first.',\n dictEmpty: (id) => `Dictionary ${id} is empty.`,\n unknown: 'Unknown error',\n },\n imeWarning: '! Non-English input detected — switch your IME to English.',\n imeWarningShort: '! IME',\n audioWarningShort: '! audio',\n },\n audio: {\n noPlayer: '! No audio player found on PATH (looked for afplay/ffplay/mpg123/paplay/aplay/powershell). Sounds disabled.',\n },\n report: {\n title: 'Session summary',\n duration: 'duration',\n practiced: 'practiced',\n chapters: 'chapters',\n words: 'words',\n accuracy: 'accuracy',\n wpm: 'wpm',\n newMistakes: 'new mistakes',\n farewell: 'see you next time.',\n },\n help: {\n title: 'Help',\n subtitle: 'all shortcuts',\n sections: {\n main: 'main menu',\n practice: 'practice',\n dict: 'dictionaries',\n config: 'config',\n stats: 'stats',\n word: 'word lookup',\n global: 'global',\n },\n keys: {\n navigate: '↑/↓ navigate items',\n select: 'Enter confirm / continue',\n letterJump: 'letter jump to menu item',\n pause: 'Esc pause practice',\n skip: 'Ctrl+N skip current word (neutral)',\n replay: 'Tab replay pronunciation',\n resume: 'Enter resume from pause',\n backMenu: 'Esc back to previous screen',\n backScreen: 'Esc close panel or back',\n nextChapter: 'Enter next chapter',\n reviewMistakes: 'm review mistakes',\n filter: 'type to filter list',\n itemActions: 'Enter open actions panel',\n moreActions: 'Ctrl+K more actions panel',\n cycleWindow: '←/→ cycle day window',\n stealthToggle: 'Ctrl+I toggle stealth info row',\n helpScreen: '? open this help screen',\n quit: 'Ctrl+C quit immediately',\n },\n footer: 'Esc back',\n },\n stealth: {\n paused: 'paused',\n chapterDone: 'chapter done',\n resumeHint: 'Enter resume · Esc menu',\n nextHint: 'Enter next · Esc menu',\n pausedHintRight: 'Enter resume',\n nextHintRight: 'Enter next',\n infoChipLabel: 'info',\n infoFmt: (dict, chapter, completed, total, wpm, accPct) =>\n `${dict} · ${chapter} · ${completed}/${total} · ${wpm} wpm · ${accPct}%`,\n },\n};\n\nexport const zh: Strings = {\n app: {\n title: 'qwerty',\n subtitle: '终端键盘练习',\n },\n common: {\n back: '返回',\n quit: '退出',\n on: '开',\n off: '关',\n cancel: '取消',\n },\n mainMenu: {\n items: {\n practiceLabel: '练习',\n practiceHintWith: (name) => `开始 ${name}`,\n practiceHintNone: '请先选词典',\n dictLabel: '词典',\n dictHint: '浏览、下载、设为默认',\n wordLabel: '查词',\n wordHint: '在本地词典中搜索',\n statsLabel: '统计',\n statsHint: '历史与趋势',\n configLabel: '设置',\n configHint: '修改偏好',\n stealthLabel: '神隐',\n stealthHint: '神隐练习模式',\n quitLabel: '退出',\n quitHint: 'Esc 或 Ctrl+C 退出',\n },\n hint: '↑/↓ 移动 · Enter 确认 · 字母直达',\n helpHint: '? 帮助',\n },\n dict: {\n title: '词典',\n loading: '加载词典中…',\n entries: (n) => `${n} 部词典`,\n filterPlaceholder: '输入过滤',\n local: '已下载 ✓',\n notLocal: '未下载',\n defaultMark: '默认 ★',\n tagsLabel: (tags) => `标签:${tags}`,\n wordsLabel: (n) => `${n} 词`,\n pulling: (id) => `拉取 ${id} 中…`,\n removing: (id) => `删除 ${id} 中…`,\n errorOn: (id, msg) => `${id} 出错:${msg}`,\n footer: '↑/↓ 选择 · Enter 操作 · Ctrl+K 更多 · Esc 返回',\n action: {\n title: '当前词典',\n setDefault: '设为默认',\n practice: '立即练习',\n delete: '删除本地',\n },\n command: {\n title: '更多功能',\n pull: '拉取选中',\n import: '导入 .json',\n refreshList: '更新词典列表',\n },\n },\n config: {\n title: '设置',\n fields: {\n defaultDict: '默认词典',\n defaultMode: '默认模式',\n accent: '发音',\n mirror: '词典镜像源',\n chapterSize: '章节单词数',\n autoplayPronunciation: '自动播放发音',\n soundsMaster: '音效总开关',\n soundsKeystroke: '按键音',\n soundsFeedback: '反馈音',\n soundsPronunciationRate: '发音速率',\n soundsPronunciationSource: '发音音源',\n language: '语言',\n stealth: '神隐模式',\n },\n enumValues: {\n stealth: { off: '关闭', menu: '主菜单显示', default: '默认练习模式' },\n soundsPronunciationSource: { youdao: '有道词典', dictapi: '维基词典' },\n defaultMode: { order: '顺序', dictation: '默写', review: '复习', random: '乱序', loop: '循环' },\n },\n hints: {\n editing: '输入修改 · Enter 保存 · Esc 取消',\n bool: '空格切换 · ↑/↓ 移动 · Esc 返回',\n enum: '←/→ 切换 · ↑/↓ 移动 · Esc 返回',\n dictRef: 'Enter 选词典 · ↑/↓ 移动 · Esc 返回',\n stringOrInt: 'Enter 编辑 · ↑/↓ 移动 · Esc 返回',\n },\n },\n stats: {\n title: '统计 · 概览',\n loading: '加载统计中…',\n none: '还没有练习记录。',\n nonePractice: '先来一次练习吧。',\n lifetime: '累计',\n sessions: '会话',\n words: '词数',\n errors: '错误',\n wpm: '速度',\n accuracy: '准确率',\n streak: '连续天数',\n last: (n) => `最近 ${n} 天 (←/→ 切换窗口)`,\n cycleWindow: '←/→ 切换窗口 · Esc 返回',\n recent: '最近会话',\n topMistakes: '高频错词',\n footer: '←/→ 切换窗口 · Esc 返回',\n maxLabel: '最大',\n recentUnits: { words: '词', errors: '错', wpm: '速' },\n multiDictSuffix: (n) => ` 等 ${n} 部`,\n bars: { speed: '速度', accuracy: '准确率', sessions: '会话' },\n },\n word: {\n title: '查词',\n indexing: '索引本地词典中…',\n none: '没有本地词典。',\n pullFirst: '先在「词典」中拉取一部。',\n countAcross: (n) => `本地词典共 ${n} 词`,\n noMatches: (q) => `没有匹配「${q}」的词`,\n inDict: (name) => `来源:${name}`,\n mistakes: (n, date) => `错过 ${n} 次 (最近 ${date})`,\n footer: '输入过滤 · ↑/↓ 选择 · Esc 返回',\n },\n practice: {\n loading: '加载中…',\n paused: '已暂停',\n chapterComplete: '本章完成',\n chapterLabel: (c, t) => `第 ${c}/${t} 章`,\n reviewLabel: '复习',\n statusBar: {\n mode: '模式',\n accent: '发音',\n },\n modes: {\n order: '顺序',\n dictation: '默写',\n review: '复习',\n random: '乱序',\n loop: '循环',\n },\n accents: {\n us: '美',\n uk: '英',\n },\n statCards: {\n words: '词数',\n errors: '错误',\n wpm: '速度',\n accuracy: '准确率',\n elapsed: (t) => `耗时 ${t}`,\n },\n pause: {\n title: '已暂停',\n chapter: (c, t) => `第 ${c}/${t} 章`,\n progress: (completed, total) => `${completed}/${total}`,\n hint: 'Enter 继续 · Esc 返回菜单',\n },\n summary: {\n loopAgain: '再来一遍',\n nextChapter: '下一章',\n reviewMistakes: '复习错词',\n backMenu: '返回菜单',\n },\n footers: {\n typing: 'Ctrl+N 跳过 · Esc 暂停 · Tab 重播',\n },\n errors: {\n noMistakes: '错词本是空的。先练习几章吧。',\n dictEmpty: (id) => `词典 ${id} 是空的。`,\n unknown: '未知错误',\n },\n imeWarning: '! 检测到中文/日文/韩文输入,请切换到英文输入法',\n imeWarningShort: '! 中文输入',\n audioWarningShort: '! 无音效',\n },\n audio: {\n noPlayer: '! 未在 PATH 中找到音频播放器(尝试 afplay/ffplay/mpg123/paplay/aplay/powershell)。音效已禁用。',\n },\n report: {\n title: '本次会话',\n duration: '总时长',\n practiced: '练习用时',\n chapters: '完成章节',\n words: '词数',\n accuracy: '准确率',\n wpm: '速度',\n newMistakes: '新错词',\n farewell: '下次见。',\n },\n help: {\n title: '帮助',\n subtitle: '全部快捷键',\n sections: {\n main: '主菜单',\n practice: '练习',\n dict: '词典',\n config: '设置',\n stats: '统计',\n word: '查词',\n global: '全局',\n },\n keys: {\n navigate: '↑/↓ 移动选项',\n select: 'Enter 确认 / 继续',\n letterJump: '字母键 直达菜单项',\n pause: 'Esc 暂停练习',\n skip: 'Ctrl+N 跳过当前词(不计错)',\n replay: 'Tab 重播发音',\n resume: 'Enter 继续练习',\n backMenu: 'Esc 返回上一屏',\n backScreen: 'Esc 关闭面板 / 返回',\n nextChapter: 'Enter 下一章',\n reviewMistakes: 'm 复习错词',\n filter: '输入 过滤列表',\n itemActions: 'Enter 弹出动作面板',\n moreActions: 'Ctrl+K 弹出更多功能',\n cycleWindow: '←/→ 切换日窗口',\n stealthToggle: 'Ctrl+I 切换神隐信息行',\n helpScreen: '? 打开本帮助页',\n quit: 'Ctrl+C 立即退出',\n },\n footer: 'Esc 返回',\n },\n stealth: {\n paused: 'paused',\n chapterDone: 'chapter done',\n resumeHint: 'Enter resume · Esc menu',\n nextHint: 'Enter next · Esc menu',\n pausedHintRight: 'Enter 继续',\n nextHintRight: 'Enter 下一章',\n infoChipLabel: '信息',\n infoFmt: (dict, chapter, completed, total, wpm, accPct) =>\n `${dict} · ${chapter} · ${completed}/${total} · ${wpm} wpm · ${accPct}%`,\n },\n};\n","export type Lang = 'zh' | 'en';\nexport type LangPref = 'auto' | Lang;\n\nfunction pickFromString(s: string | undefined): Lang | null {\n if (!s) return null;\n const lower = s.toLowerCase();\n if (lower.startsWith('zh')) return 'zh';\n if (lower.startsWith('en')) return 'en';\n return null;\n}\n\nexport function detectLocale(pref: LangPref): Lang {\n if (pref === 'zh' || pref === 'en') return pref;\n const env =\n process.env.LC_ALL ||\n process.env.LC_MESSAGES ||\n process.env.LANG ||\n process.env.LANGUAGE;\n const fromEnv = pickFromString(env);\n if (fromEnv) return fromEnv;\n try {\n const intlLocale = Intl.DateTimeFormat().resolvedOptions().locale;\n const fromIntl = pickFromString(intlLocale);\n if (fromIntl) return fromIntl;\n } catch {\n // ignore\n }\n return 'en';\n}\n","import { Box, Text } from 'ink';\n\nexport const PALETTE = {\n accent: '#5eead4',\n muted: '#6b7280',\n text: '#e5e7eb',\n primary: '#7dcfff',\n success: '#86efac',\n warning: '#fbbf24',\n error: '#f87171',\n} as const;\n\ntype Props = {\n target: string;\n typed: string;\n error?: boolean;\n hideTarget?: boolean;\n};\n\nexport function BigWord({ target, typed, error = false, hideTarget = false }: Props) {\n const chars = [...target];\n const typedChars = [...typed];\n\n return (\n <Box paddingY={4} justifyContent=\"center\">\n {chars.map((ch, i) => {\n const isTyped = i < typedChars.length;\n const display = hideTarget && !isTyped ? '_' : isTyped ? typedChars[i]! : ch;\n const color = error\n ? PALETTE.error\n : isTyped\n ? PALETTE.accent\n : PALETTE.muted;\n return (\n <Text key={i} bold color={color}>\n {display}\n </Text>\n );\n })}\n </Box>\n );\n}\n"],"mappings":"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","names":["createContext","useContext","useState","useCallback","jsx","NavContext","NavProvider","initial","children","stack","setStack","navigate","frame","s","replace","back","reset","current","useNav","ctx","createContext","useContext","useMemo","en","name","n","tags","id","msg","q","date","c","t","completed","total","dict","chapter","wpm","accPct","zh","pickFromString","s","lower","detectLocale","pref","env","fromEnv","intlLocale","fromIntl","jsx","StringsContext","createContext","StringsProvider","pref","children","value","useMemo","lang","detectLocale","zh","en","useStrings","ctx","useContext","pickStrings","pref","lang","detectLocale","zh","en","Box","Text","jsx","PALETTE","BigWord","target","typed","error","hideTarget","chars","typedChars","ch","i","isTyped","display","color"]}
@@ -1,2 +0,0 @@
1
- import{a as o,b as s,c as p,d as c}from"./chunk-4EJYJITR.js";import{d as n,g as a,i as m}from"./chunk-6ROGUGNX.js";import"./chunk-BIBS2Q3E.js";import"./chunk-CQRKGMPU.js";import{a as i}from"./chunk-7LTZGB7F.js";import"./chunk-QG7ZTS2G.js";import{e}from"./chunk-UWTJMVJ6.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-OLNLJLIA.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-4C7HT3VD.js.map
@@ -1,2 +0,0 @@
1
- import{a as c,b as l,c as u,d as g}from"./chunk-4EJYJITR.js";import{f as m,g as f,i as p}from"./chunk-6ROGUGNX.js";import"./chunk-BIBS2Q3E.js";import"./chunk-CQRKGMPU.js";import{a}from"./chunk-7LTZGB7F.js";import"./chunk-QG7ZTS2G.js";import{e as d}from"./chunk-UWTJMVJ6.js";import"./chunk-NA5UNUVL.js";import"./chunk-E6BBQALJ.js";import n from"chalk";import{render as b}from"ink";import{createElement as v}from"react";var h=["order","dictation","review","random","loop"];function w(t){return h.includes(t)}async function O(t,o){if(!process.stdout.isTTY){console.error(n.red("Practice requires an interactive TTY.")),process.exitCode=1;return}let e=await a(),s=t??e.defaultDict;if(!s){console.error(n.red("No dictionary specified. Pass an id or set config.defaultDict.")),process.exitCode=1;return}let i=o.mode??e.defaultMode;if(!w(i)){console.error(n.red(`Invalid mode "${i}". Valid: ${h.join(", ")}`)),process.exitCode=1;return}let x=Math.max(0,Number(o.chapter??1)-1),r=o.stealth===!0||e.stealth==="default";r||c(),f();let{waitUntilExit:C}=b(v(l,{initial:{name:"practice",params:{dictId:s,chapterIndex:x,mode:i,stealth:r}},initialCfg:e,inline:r}),{patchConsole:!1,exitOnCtrlC:!1});if(await C(),r||u(),m()){process.stdout.write("\x1B[3F\x1B[0J");return}if(r)return;let{lang:M,t:S}=d(e.language);g(p(),S,M)}export{O as runPractice};
2
- //# sourceMappingURL=practice.impl-OLNLJLIA.js.map