qwerty-cli 0.0.1-alpha.8 → 0.0.1-alpha.9

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 (49) hide show
  1. package/dist/ConfigEditor-GXFVIJP3.js +2 -0
  2. package/dist/ConfigEditor-GXFVIJP3.js.map +1 -0
  3. package/dist/DictBrowser-SZVB5W25.js +2 -0
  4. package/dist/DictBrowser-SZVB5W25.js.map +1 -0
  5. package/dist/HelpScreen-OUP5G5UG.js +2 -0
  6. package/dist/HelpScreen-OUP5G5UG.js.map +1 -0
  7. package/dist/PracticeScreen-LLUTKFXL.js +2 -0
  8. package/dist/PracticeScreen-LLUTKFXL.js.map +1 -0
  9. package/dist/StatsViewer-EY2N2LP3.js +2 -0
  10. package/dist/StatsViewer-EY2N2LP3.js.map +1 -0
  11. package/dist/WordLookup-UPEDLVKF.js +2 -0
  12. package/dist/WordLookup-UPEDLVKF.js.map +1 -0
  13. package/dist/chunk-2GTGXODM.js +2 -0
  14. package/dist/chunk-2GTGXODM.js.map +1 -0
  15. package/dist/chunk-2MRNI465.js +2 -0
  16. package/dist/chunk-2MRNI465.js.map +1 -0
  17. package/dist/chunk-6KRVNT2S.js +4 -0
  18. package/dist/chunk-6KRVNT2S.js.map +1 -0
  19. package/dist/chunk-6QICLHIY.js +2 -0
  20. package/dist/chunk-6QICLHIY.js.map +1 -0
  21. package/dist/chunk-ELWVQGDK.js +2 -0
  22. package/dist/chunk-ELWVQGDK.js.map +1 -0
  23. package/dist/chunk-MPE25TTQ.js +2 -0
  24. package/dist/chunk-MPE25TTQ.js.map +1 -0
  25. package/dist/chunk-QEX27D7F.js +2 -0
  26. package/dist/chunk-QEX27D7F.js.map +1 -0
  27. package/dist/chunk-RF5SVFBO.js +3 -0
  28. package/dist/chunk-RF5SVFBO.js.map +1 -0
  29. package/dist/chunk-TP77EGJ2.js +2 -0
  30. package/dist/chunk-TP77EGJ2.js.map +1 -0
  31. package/dist/chunk-UPA4JFCH.js +2 -0
  32. package/dist/chunk-UPA4JFCH.js.map +1 -0
  33. package/dist/chunk-UPYHZMDS.js +2 -0
  34. package/dist/chunk-UPYHZMDS.js.map +1 -0
  35. package/dist/cli.js +1 -3733
  36. package/dist/cli.js.map +1 -1
  37. package/dist/config.impl-IYJ4ZUPE.js +2 -0
  38. package/dist/config.impl-IYJ4ZUPE.js.map +1 -0
  39. package/dist/dict.impl-Y66SRRZL.js +4 -0
  40. package/dist/dict.impl-Y66SRRZL.js.map +1 -0
  41. package/dist/menu.impl-L5KAWNMC.js +2 -0
  42. package/dist/menu.impl-L5KAWNMC.js.map +1 -0
  43. package/dist/practice.impl-NYUJO5ER.js +2 -0
  44. package/dist/practice.impl-NYUJO5ER.js.map +1 -0
  45. package/dist/stats.impl-IXVF3Q5Y.js +7 -0
  46. package/dist/stats.impl-IXVF3Q5Y.js.map +1 -0
  47. package/dist/word.impl-C4AYZ3NC.js +2 -0
  48. package/dist/word.impl-C4AYZ3NC.js.map +1 -0
  49. package/package.json +2 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/infra/config-store.ts"],"sourcesContent":["import { z } from 'zod';\nimport { paths } from './paths.js';\nimport { readJson, writeJsonAtomic } from './fs-store.js';\n\nexport const ConfigSchema = z.object({\n mirror: z.enum(['jsdelivr', 'github']).default('jsdelivr'),\n accent: z.enum(['us', 'uk']).default('us'),\n chapterSize: z.number().int().positive().max(200).default(20),\n sounds: z\n .object({\n master: z.boolean().default(true),\n keystroke: z.boolean().default(true),\n feedback: z.boolean().default(true),\n keySoundName: z.string().default('default'),\n })\n .default({ master: true, keystroke: true, feedback: true, keySoundName: 'default' }),\n autoplayPronunciation: z.boolean().default(true),\n defaultMode: z.enum(['order', 'dictation', 'review', 'random', 'loop']).default('order'),\n defaultDict: z.string().optional(),\n language: z.enum(['auto', 'zh', 'en']).default('auto'),\n stealth: z.enum(['off', 'menu', 'default']).default('off'),\n});\n\nexport type Config = z.infer<typeof ConfigSchema>;\n\n// Defaults are computed lazily at first loadConfig() call to keep zod default\n// synthesis (~5-10ms) out of the boot module-graph evaluation.\nlet cachedDefaults: Config | null = null;\nfunction getDefaults(): Config {\n if (cachedDefaults) return cachedDefaults;\n cachedDefaults = ConfigSchema.parse({});\n return cachedDefaults;\n}\n\nexport async function loadConfig(): Promise<Config> {\n const raw = await readJson<unknown>(paths.config);\n if (!raw) return getDefaults();\n const result = ConfigSchema.safeParse(raw);\n if (!result.success) {\n throw new Error(`Invalid config at ${paths.config}: ${result.error.message}`);\n }\n return result.data;\n}\n\nexport async function saveConfig(cfg: Config): Promise<void> {\n await writeJsonAtomic(paths.config, cfg);\n}\n\nexport function getByPath(cfg: Config, path: string): unknown {\n const parts = path.split('.');\n let cur: unknown = cfg;\n for (const p of parts) {\n if (cur === null || typeof cur !== 'object') return undefined;\n cur = (cur as Record<string, unknown>)[p];\n }\n return cur;\n}\n\nexport function setByPath(cfg: Config, path: string, rawValue: string): Config {\n const parts = path.split('.');\n if (parts.length === 0) throw new Error('Empty config key');\n const clone: Record<string, unknown> = JSON.parse(JSON.stringify(cfg));\n let cur: Record<string, unknown> = clone;\n for (let i = 0; i < parts.length - 1; i++) {\n const k = parts[i]!;\n const next = cur[k];\n if (typeof next !== 'object' || next === null) {\n throw new Error(`Cannot set ${path}: ${parts.slice(0, i + 1).join('.')} is not an object`);\n }\n cur = next as Record<string, unknown>;\n }\n const leaf = parts[parts.length - 1]!;\n cur[leaf] = coerce(rawValue);\n const validated = ConfigSchema.safeParse(clone);\n if (!validated.success) {\n throw new Error(`Invalid value for ${path}: ${validated.error.issues[0]?.message ?? 'unknown'}`);\n }\n return validated.data;\n}\n\nfunction coerce(v: string): unknown {\n if (v === 'true') return true;\n if (v === 'false') return false;\n if (v === 'null') return null;\n if (/^-?\\d+$/.test(v)) return Number(v);\n if (/^-?\\d+\\.\\d+$/.test(v)) return Number(v);\n return v;\n}\n"],"mappings":"iDAAA,OAAS,KAAAA,MAAS,MAIX,IAAMC,EAAeC,EAAE,OAAO,CACnC,OAAQA,EAAE,KAAK,CAAC,WAAY,QAAQ,CAAC,EAAE,QAAQ,UAAU,EACzD,OAAQA,EAAE,KAAK,CAAC,KAAM,IAAI,CAAC,EAAE,QAAQ,IAAI,EACzC,YAAaA,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,GAAG,EAAE,QAAQ,EAAE,EAC5D,OAAQA,EACL,OAAO,CACN,OAAQA,EAAE,QAAQ,EAAE,QAAQ,EAAI,EAChC,UAAWA,EAAE,QAAQ,EAAE,QAAQ,EAAI,EACnC,SAAUA,EAAE,QAAQ,EAAE,QAAQ,EAAI,EAClC,aAAcA,EAAE,OAAO,EAAE,QAAQ,SAAS,CAC5C,CAAC,EACA,QAAQ,CAAE,OAAQ,GAAM,UAAW,GAAM,SAAU,GAAM,aAAc,SAAU,CAAC,EACrF,sBAAuBA,EAAE,QAAQ,EAAE,QAAQ,EAAI,EAC/C,YAAaA,EAAE,KAAK,CAAC,QAAS,YAAa,SAAU,SAAU,MAAM,CAAC,EAAE,QAAQ,OAAO,EACvF,YAAaA,EAAE,OAAO,EAAE,SAAS,EACjC,SAAUA,EAAE,KAAK,CAAC,OAAQ,KAAM,IAAI,CAAC,EAAE,QAAQ,MAAM,EACrD,QAASA,EAAE,KAAK,CAAC,MAAO,OAAQ,SAAS,CAAC,EAAE,QAAQ,KAAK,CAC3D,CAAC,EAMGC,EAAgC,KACpC,SAASC,GAAsB,CAC7B,OAAID,IACJA,EAAiBF,EAAa,MAAM,CAAC,CAAC,EAC/BE,EACT,CAEA,eAAsBE,GAA8B,CAClD,IAAMC,EAAM,MAAMC,EAAkBC,EAAM,MAAM,EAChD,GAAI,CAACF,EAAK,OAAOF,EAAY,EAC7B,IAAMK,EAASR,EAAa,UAAUK,CAAG,EACzC,GAAI,CAACG,EAAO,QACV,MAAM,IAAI,MAAM,qBAAqBD,EAAM,MAAM,KAAKC,EAAO,MAAM,OAAO,EAAE,EAE9E,OAAOA,EAAO,IAChB,CAEA,eAAsBC,EAAWC,EAA4B,CAC3D,MAAMC,EAAgBJ,EAAM,OAAQG,CAAG,CACzC,CAEO,SAASE,EAAUF,EAAaG,EAAuB,CAC5D,IAAMC,EAAQD,EAAK,MAAM,GAAG,EACxBE,EAAeL,EACnB,QAAWM,KAAKF,EAAO,CACrB,GAAIC,IAAQ,MAAQ,OAAOA,GAAQ,SAAU,OAC7CA,EAAOA,EAAgCC,CAAC,CAC1C,CACA,OAAOD,CACT,CAEO,SAASE,EAAUP,EAAaG,EAAcK,EAA0B,CAC7E,IAAMJ,EAAQD,EAAK,MAAM,GAAG,EAC5B,GAAIC,EAAM,SAAW,EAAG,MAAM,IAAI,MAAM,kBAAkB,EAC1D,IAAMK,EAAiC,KAAK,MAAM,KAAK,UAAUT,CAAG,CAAC,EACjEK,EAA+BI,EACnC,QAASC,EAAI,EAAGA,EAAIN,EAAM,OAAS,EAAGM,IAAK,CACzC,IAAMC,EAAIP,EAAMM,CAAC,EACXE,EAAOP,EAAIM,CAAC,EAClB,GAAI,OAAOC,GAAS,UAAYA,IAAS,KACvC,MAAM,IAAI,MAAM,cAAcT,CAAI,KAAKC,EAAM,MAAM,EAAGM,EAAI,CAAC,EAAE,KAAK,GAAG,CAAC,mBAAmB,EAE3FL,EAAMO,CACR,CACA,IAAMC,EAAOT,EAAMA,EAAM,OAAS,CAAC,EACnCC,EAAIQ,CAAI,EAAIC,EAAON,CAAQ,EAC3B,IAAMO,EAAYzB,EAAa,UAAUmB,CAAK,EAC9C,GAAI,CAACM,EAAU,QACb,MAAM,IAAI,MAAM,qBAAqBZ,CAAI,KAAKY,EAAU,MAAM,OAAO,CAAC,GAAG,SAAW,SAAS,EAAE,EAEjG,OAAOA,EAAU,IACnB,CAEA,SAASD,EAAOE,EAAoB,CAClC,OAAIA,IAAM,OAAe,GACrBA,IAAM,QAAgB,GACtBA,IAAM,OAAe,KACrB,UAAU,KAAKA,CAAC,GAChB,eAAe,KAAKA,CAAC,EAAU,OAAOA,CAAC,EACpCA,CACT","names":["z","ConfigSchema","z","cachedDefaults","getDefaults","loadConfig","raw","readJson","paths","result","saveConfig","cfg","writeJsonAtomic","getByPath","path","parts","cur","p","setByPath","rawValue","clone","i","k","next","leaf","coerce","validated","v"]}
@@ -0,0 +1,2 @@
1
+ import{a as w,g as h,h as S}from"./chunk-6KRVNT2S.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-MPE25TTQ.js.map
@@ -0,0 +1 @@
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":"sDAAA,OAAS,KAAAA,MAAS,MAIX,IAAMC,EAAsBC,EAAE,OAAO,CAC1C,GAAIA,EAAE,OAAO,EACb,OAAQA,EAAE,OAAO,EACjB,QAASA,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EACtC,KAAMA,EAAE,OAAO,EACf,UAAWA,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EACxC,OAAQA,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EACrC,WAAYA,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EACzC,cAAeA,EAAE,OAAOA,EAAE,OAAO,EAAGA,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,CAAC,EAAE,QAAQ,CAAC,CAAC,CAChF,CAAC,EAID,eAAsBC,EAAcC,EAAsC,CACxE,MAAMC,EAAYC,EAAM,MAAOF,CAAM,CACvC,CAEA,eAAsBG,GAAyC,CAE7D,OADa,MAAMC,EAAmBF,EAAM,KAAK,GAE9C,IAAKG,GAAMR,EAAoB,UAAUQ,CAAC,CAAC,EAC3C,OAAQA,GAAmDA,EAAE,OAAO,EACpE,IAAKA,GAAMA,EAAE,IAAI,CACtB,CAEO,SAASC,EAAWN,EAA+B,CACxD,GAAIA,EAAO,aAAe,EAAG,MAAO,GACpC,IAAMO,EAAUP,EAAO,WAAa,IACpC,OAAO,KAAK,MAAOA,EAAO,UAAYO,EAAW,EAAE,EAAI,EACzD,CAEO,SAASC,EAASR,EAA+B,CAEtD,GAAIA,EAAO,YAAc,EAAG,MAAO,GACnC,IAAMS,EAAkB,OAAO,KAAKT,EAAO,aAAa,EAAE,OAC1D,OAAO,KAAK,IAAI,EAAG,KAAK,IAAI,GAAIA,EAAO,UAAYS,GAAmBT,EAAO,SAAS,CAAC,CACzF,CAEO,SAASU,EAAYC,EAA2BC,EAAM,IAAI,KAAgB,CAC/E,GAAID,EAAS,SAAW,EAAG,MAAO,GAClC,IAAME,EAAO,IAAI,IACjB,QAAWC,KAAKH,EAAUE,EAAK,IAAIC,EAAE,GAAG,MAAM,EAAG,EAAE,CAAC,EACpD,IAAIC,EAAS,EACPC,EAAM,IAAI,KAAKJ,CAAG,EACxB,OAAa,CACX,IAAMK,EAAMD,EAAI,YAAY,EAAE,MAAM,EAAG,EAAE,EACzC,GAAI,CAACH,EAAK,IAAII,CAAG,EAAG,MACpBF,IACAC,EAAI,WAAWA,EAAI,WAAW,EAAI,CAAC,CACrC,CACA,OAAOD,CACT,CAEA,IAAMG,EAAQ,CAAC,SAAK,SAAK,SAAK,SAAK,SAAK,SAAK,SAAK,QAAG,EAE9C,SAASC,EAAUC,EAA0B,CAClD,GAAIA,EAAO,SAAW,EAAG,MAAO,GAChC,IAAMC,EAAM,KAAK,IAAI,GAAGD,EAAQ,CAAC,EAC3BE,EAAM,KAAK,IAAI,GAAGF,EAAQ,CAAC,EAC3BG,EAAQ,KAAK,IAAI,EAAGF,EAAMC,CAAG,EACnC,OAAOF,EACJ,IAAKI,GAAM,CACV,IAAMC,EAAM,KAAK,OAAQD,EAAIF,GAAOC,GAAUL,EAAM,OAAS,EAAE,EAC/D,OAAOA,EAAM,KAAK,IAAI,EAAG,KAAK,IAAIA,EAAM,OAAS,EAAGO,CAAG,CAAC,CAAC,CAC3D,CAAC,EACA,KAAK,EAAE,CACZ,CAwBO,SAASC,EACdC,EACAC,EACAC,EAAM,IAAI,KACO,CACjB,GAAIF,EAAS,SAAW,GAAKC,GAAQ,EACnC,MAAO,CAAE,OAAQ,EAAG,gBAAiB,EAAG,UAAW,EAAG,iBAAkB,EAAG,KAAAA,CAAK,EAElF,IAAME,EAAS,IAAI,KAAKD,CAAG,EAC3BC,EAAO,WAAWA,EAAO,WAAW,GAAKF,EAAO,EAAE,EAClD,IAAMG,EAAYD,EAAO,YAAY,EAAE,MAAM,EAAG,EAAE,EAE9CE,EAAgB,EAChBC,EAAiB,EACjBC,EAAiB,EACjBC,EAAmB,EACnBC,EAAkB,EACtB,QAAWC,KAAKV,EAAU,CACxB,IAAMW,EAAMC,EAAWF,CAAC,EACpBC,EAAMF,IAAiBA,EAAkBE,GACzC,EAAAD,EAAE,GAAG,MAAM,EAAG,EAAE,EAAIN,KACxBI,IACAH,GAAiBK,EAAE,UACnBJ,GAAkBK,EAAMD,EAAE,UAC1BH,GAAkBM,EAASH,CAAC,EAAIA,EAAE,UACpC,CACA,GAAIL,IAAkB,EACpB,MAAO,CAAE,OAAQ,EAAG,gBAAAI,EAAiB,UAAW,EAAG,iBAAAD,EAAkB,KAAAP,CAAK,EAE5E,IAAMa,EAAS,KAAK,MAAOR,EAAiBD,EAAiB,EAAE,EAAI,GAC7DU,EAAY,KAAK,MAAOR,EAAiBF,EAAiB,GAAI,EAAI,GACxE,MAAO,CAAE,OAAAS,EAAQ,gBAAAL,EAAiB,UAAAM,EAAW,iBAAAP,EAAkB,KAAAP,CAAK,CACtE,CAIO,SAASe,EAAahB,EAA2BC,EAAcC,EAAM,IAAI,KAAuB,CACrG,IAAMe,EAAqB,CAAC,EACtBC,EAAQ,IAAI,IAClB,QAAWR,KAAKV,EAAU,CACxB,IAAMmB,EAAMT,EAAE,GAAG,MAAM,EAAG,EAAE,EACtBU,EAAMF,EAAM,IAAIC,CAAG,GAAK,CAAC,EAC/BC,EAAI,KAAKV,CAAC,EACVQ,EAAM,IAAIC,EAAKC,CAAG,CACpB,CACA,IAAMC,EAAM,IAAI,KAAKnB,CAAG,EACxBmB,EAAI,WAAWA,EAAI,WAAW,GAAKpB,EAAO,EAAE,EAC5C,QAASqB,EAAI,EAAGA,EAAIrB,EAAMqB,IAAK,CAC7B,IAAMH,EAAME,EAAI,YAAY,EAAE,MAAM,EAAG,EAAE,EACnCE,EAASL,EAAM,IAAIC,CAAG,GAAK,CAAC,EAClC,GAAII,EAAO,SAAW,EACpBN,EAAI,KAAK,CAAE,KAAME,EAAK,IAAK,EAAG,SAAU,EAAG,SAAU,CAAE,CAAC,MACnD,CACL,IAAMR,EAAMY,EAAO,OAAO,CAACC,EAAGd,IAAMc,EAAIZ,EAAWF,CAAC,EAAG,CAAC,EAAIa,EAAO,OAC7DE,EAAMF,EAAO,OAAO,CAACC,EAAGd,IAAMc,EAAIX,EAASH,CAAC,EAAG,CAAC,EAAIa,EAAO,OACjEN,EAAI,KAAK,CAAE,KAAME,EAAK,IAAAR,EAAK,SAAUc,EAAK,SAAUF,EAAO,MAAO,CAAC,CACrE,CACAF,EAAI,WAAWA,EAAI,WAAW,EAAI,CAAC,CACrC,CACA,OAAOJ,CACT","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"]}
@@ -0,0 +1,2 @@
1
+ import{createContext as k,useContext as E,useState as L,useCallback as g}from"react";import{jsx as v}from"react/jsx-runtime";var h=k(null);function N({initial:t,children:e}){let[n,r]=L([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 v(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 $,useMemo as S}from"react";var p={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",soundsKeySound:"sounds key sound",language:"language",stealth:"stealth mode"},enumValues:{stealth:{off:"off",menu:"show in menu",default:"default practice"}},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"}},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.",notPracticed:"no practice this run"},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}%`}},d={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:"\u9690\u8EAB",stealthHint:"\u9690\u8EAB\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",soundsKeySound:"\u6309\u952E\u97F3\u8272",language:"\u8BED\u8A00",stealth:"\u9690\u8EAB\u6A21\u5F0F"},enumValues:{stealth:{off:"\u5173\u95ED",menu:"\u4E3B\u83DC\u5355\u663E\u793A",default:"\u9ED8\u8BA4\u7EC3\u4E60\u6A21\u5F0F"}},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"}},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",notPracticed:"\u672C\u6B21\u672A\u7EC3\u4E60"},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\u9690\u8EAB\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 K({pref:t,children:e}){let n=S(()=>{let r=m(t);return{lang:r,t:r==="zh"?d:p}},[t]);return C(w.Provider,{value:n,children:e})}function I(){let t=$(w);if(!t)throw new Error("useStrings must be used inside StringsProvider");return t.t}function O(t){let e=m(t);return{lang:e,t:e==="zh"?d:p}}import{Box as P,Text as H}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(H,{bold:!0,color:o,children:i},c)})})}export{N as a,D as b,K as c,I as d,O as e,f,G as g};
2
+ //# sourceMappingURL=chunk-QEX27D7F.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/ui/nav.tsx","../src/i18n/context.tsx","../src/i18n/strings.ts","../src/i18n/locale.ts","../src/ui/components/BigWord.tsx"],"sourcesContent":["import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';\n\nexport type ScreenName =\n | 'main'\n | 'practice'\n | 'dict'\n | 'config'\n | 'stats'\n | 'word'\n | 'help';\n\nexport type PracticeParams = {\n dictId: string;\n chapterIndex: number;\n mode: 'order' | 'dictation' | 'review' | 'random' | 'loop';\n stealth?: boolean;\n};\n\nexport type DictParams = {\n pickerMode?: 'set-default' | 'choose-then-practice';\n};\n\nexport type ScreenFrame =\n | { name: 'main' }\n | { name: 'practice'; params: PracticeParams }\n | { name: 'dict'; params?: DictParams }\n | { name: 'config' }\n | { name: 'stats' }\n | { name: 'word' }\n | { name: 'help' };\n\ntype NavContextValue = {\n current: ScreenFrame;\n stack: ScreenFrame[];\n navigate: (frame: ScreenFrame) => void;\n replace: (frame: ScreenFrame) => void;\n back: () => void;\n reset: (frame: ScreenFrame) => void;\n};\n\nconst NavContext = createContext<NavContextValue | null>(null);\n\nexport function NavProvider({ initial, children }: { initial: ScreenFrame; children: ReactNode }) {\n const [stack, setStack] = useState<ScreenFrame[]>([initial]);\n\n const navigate = useCallback((frame: ScreenFrame) => {\n setStack((s) => [...s, frame]);\n }, []);\n const replace = useCallback((frame: ScreenFrame) => {\n setStack((s) => (s.length === 0 ? [frame] : [...s.slice(0, -1), frame]));\n }, []);\n const back = useCallback(() => {\n setStack((s) => (s.length > 1 ? s.slice(0, -1) : s));\n }, []);\n const reset = useCallback((frame: ScreenFrame) => {\n setStack([frame]);\n }, []);\n\n const current = stack[stack.length - 1]!;\n return (\n <NavContext.Provider value={{ current, stack, navigate, replace, back, reset }}>\n {children}\n </NavContext.Provider>\n );\n}\n\nexport function useNav(): NavContextValue {\n const ctx = useContext(NavContext);\n if (!ctx) throw new Error('useNav must be used inside NavProvider');\n return ctx;\n}\n","import { createContext, useContext, useMemo, type ReactNode } from 'react';\nimport { en, zh, type Strings } from './strings.js';\nimport { detectLocale, type Lang, type LangPref } from './locale.js';\n\nconst StringsContext = createContext<{ lang: Lang; t: Strings } | null>(null);\n\nexport function StringsProvider({\n pref,\n children,\n}: {\n pref: LangPref;\n children: ReactNode;\n}) {\n const value = useMemo(() => {\n const lang = detectLocale(pref);\n return { lang, t: lang === 'zh' ? zh : en };\n }, [pref]);\n return <StringsContext.Provider value={value}>{children}</StringsContext.Provider>;\n}\n\nexport function useStrings(): Strings {\n const ctx = useContext(StringsContext);\n if (!ctx) throw new Error('useStrings must be used inside StringsProvider');\n return ctx.t;\n}\n\nexport function useLang(): Lang {\n const ctx = useContext(StringsContext);\n if (!ctx) throw new Error('useLang must be used inside StringsProvider');\n return ctx.lang;\n}\n\nexport function pickStrings(pref: LangPref): { lang: Lang; t: Strings } {\n const lang = detectLocale(pref);\n return { lang, t: lang === 'zh' ? zh : en };\n}\n","// Centralized string table for the qwerty-cli TUI.\n//\n// Print-only subcommands (qwerty config list, qwerty dict list, etc.) keep\n// English output regardless of locale so shell scripts stay predictable.\n// Only the interactive TUI is localized via this table.\n\nexport type Strings = {\n app: {\n title: string;\n subtitle: string;\n };\n common: {\n back: string;\n quit: string;\n on: string;\n off: string;\n cancel: string;\n };\n mainMenu: {\n items: {\n practiceLabel: string;\n practiceHintWith: (name: string) => string;\n practiceHintNone: string;\n dictLabel: string;\n dictHint: string;\n wordLabel: string;\n wordHint: string;\n statsLabel: string;\n statsHint: string;\n configLabel: string;\n configHint: string;\n stealthLabel: string;\n stealthHint: string;\n quitLabel: string;\n quitHint: string;\n };\n hint: string;\n helpHint: string;\n };\n dict: {\n title: string;\n loading: string;\n entries: (n: number) => string;\n filterPlaceholder: string;\n local: string;\n notLocal: string;\n defaultMark: string;\n tagsLabel: (tags: string) => string;\n wordsLabel: (n: number) => string;\n pulling: (id: string) => string;\n removing: (id: string) => string;\n errorOn: (id: string, msg: string) => string;\n footer: string;\n action: {\n title: string;\n setDefault: string;\n practice: string;\n delete: string;\n };\n command: {\n title: string;\n pull: string;\n import: string;\n refreshList: string;\n };\n };\n config: {\n title: string;\n fields: {\n defaultDict: string;\n defaultMode: string;\n accent: string;\n mirror: string;\n chapterSize: string;\n autoplayPronunciation: string;\n soundsMaster: string;\n soundsKeystroke: string;\n soundsFeedback: string;\n soundsKeySound: string;\n language: string;\n stealth: string;\n };\n enumValues: {\n stealth: { off: string; menu: string; default: 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 };\n audio: {\n noPlayer: string;\n };\n report: {\n title: string;\n duration: string;\n practiced: string;\n chapters: string;\n words: string;\n accuracy: string;\n wpm: string;\n newMistakes: string;\n farewell: string;\n notPracticed: string;\n };\n help: {\n title: string;\n subtitle: string;\n sections: {\n main: string;\n practice: string;\n dict: string;\n config: string;\n stats: string;\n word: string;\n global: string;\n };\n keys: {\n navigate: string;\n select: string;\n letterJump: string;\n pause: string;\n skip: string;\n replay: string;\n resume: string;\n backMenu: string;\n backScreen: string;\n nextChapter: string;\n reviewMistakes: string;\n filter: string;\n itemActions: string;\n moreActions: string;\n cycleWindow: string;\n stealthToggle: string;\n helpScreen: string;\n quit: string;\n };\n footer: string;\n };\n stealth: {\n paused: string;\n chapterDone: string;\n resumeHint: string;\n nextHint: string;\n pausedHintRight: string;\n nextHintRight: string;\n infoChipLabel: string;\n infoFmt: (\n dict: string,\n chapter: string,\n completed: number,\n total: number,\n wpm: number,\n accPct: number,\n ) => string;\n };\n};\n\nexport const en: Strings = {\n app: {\n title: 'qwerty',\n subtitle: 'typing practice for the terminal',\n },\n common: {\n back: 'back',\n quit: 'quit',\n on: 'on',\n off: 'off',\n cancel: 'cancel',\n },\n mainMenu: {\n items: {\n practiceLabel: 'Practice',\n practiceHintWith: (name) => `start ${name}`,\n practiceHintNone: 'pick a dictionary',\n dictLabel: 'Dictionaries',\n dictHint: 'browse, pull, set default',\n wordLabel: 'Word lookup',\n wordHint: 'search local dicts',\n statsLabel: 'Stats',\n statsHint: 'history & trends',\n configLabel: 'Config',\n configHint: 'edit preferences',\n stealthLabel: 'Stealth',\n stealthHint: 'quiet practice mode',\n quitLabel: 'Quit',\n quitHint: 'Esc or Ctrl+C also exits',\n },\n hint: '↑/↓ navigate · Enter select · letters jump',\n helpHint: '? help',\n },\n dict: {\n title: 'Dictionaries',\n loading: 'loading dictionaries…',\n entries: (n) => `${n} entries`,\n filterPlaceholder: 'type to filter',\n local: 'local ✓',\n notLocal: 'not local',\n defaultMark: 'default ★',\n tagsLabel: (tags) => `tags: ${tags}`,\n wordsLabel: (n) => `${n} words`,\n pulling: (id) => `pulling ${id}…`,\n removing: (id) => `removing ${id}…`,\n errorOn: (id, msg) => `error on ${id}: ${msg}`,\n footer: '↑/↓ select · Enter actions · Ctrl+K more · Esc back',\n action: {\n title: 'current dictionary',\n setDefault: 'set as default',\n practice: 'practice now',\n delete: 'delete local',\n },\n command: {\n title: 'more actions',\n pull: 'pull selected',\n import: 'import .json',\n refreshList: 'update dictionary list',\n },\n },\n config: {\n title: 'Config',\n fields: {\n defaultDict: 'default dict',\n defaultMode: 'default mode',\n accent: 'accent',\n mirror: 'dict mirror',\n chapterSize: 'chapter size',\n autoplayPronunciation: 'autoplay pronunciation',\n soundsMaster: 'sounds master',\n soundsKeystroke: 'sounds keystroke',\n soundsFeedback: 'sounds feedback',\n soundsKeySound: 'sounds key sound',\n language: 'language',\n stealth: 'stealth mode',\n },\n enumValues: {\n stealth: { off: 'off', menu: 'show in menu', default: 'default practice' },\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 },\n audio: {\n noPlayer: '! No audio player found on PATH (looked for afplay/ffplay/mpg123/paplay/aplay/powershell). Sounds disabled.',\n },\n report: {\n title: 'Session summary',\n duration: 'duration',\n practiced: 'practiced',\n chapters: 'chapters',\n words: 'words',\n accuracy: 'accuracy',\n wpm: 'wpm',\n newMistakes: 'new mistakes',\n farewell: 'see you next time.',\n notPracticed: 'no practice this run',\n },\n help: {\n title: 'Help',\n subtitle: 'all shortcuts',\n sections: {\n main: 'main menu',\n practice: 'practice',\n dict: 'dictionaries',\n config: 'config',\n stats: 'stats',\n word: 'word lookup',\n global: 'global',\n },\n keys: {\n navigate: '↑/↓ navigate items',\n select: 'Enter confirm / continue',\n letterJump: 'letter jump to menu item',\n pause: 'Esc pause practice',\n skip: 'Ctrl+N skip current word (neutral)',\n replay: 'Tab replay pronunciation',\n resume: 'Enter resume from pause',\n backMenu: 'Esc back to previous screen',\n backScreen: 'Esc close panel or back',\n nextChapter: 'Enter next chapter',\n reviewMistakes: 'm review mistakes',\n filter: 'type to filter list',\n itemActions: 'Enter open actions panel',\n moreActions: 'Ctrl+K more actions panel',\n cycleWindow: '←/→ cycle day window',\n stealthToggle: 'Ctrl+I toggle stealth info row',\n helpScreen: '? open this help screen',\n quit: 'Ctrl+C quit immediately',\n },\n footer: 'Esc back',\n },\n stealth: {\n paused: 'paused',\n chapterDone: 'chapter done',\n resumeHint: 'Enter resume · Esc menu',\n nextHint: 'Enter next · Esc menu',\n pausedHintRight: 'Enter resume',\n nextHintRight: 'Enter next',\n infoChipLabel: 'info',\n infoFmt: (dict, chapter, completed, total, wpm, accPct) =>\n `${dict} · ${chapter} · ${completed}/${total} · ${wpm} wpm · ${accPct}%`,\n },\n};\n\nexport const zh: Strings = {\n app: {\n title: 'qwerty',\n subtitle: '终端键盘练习',\n },\n common: {\n back: '返回',\n quit: '退出',\n on: '开',\n off: '关',\n cancel: '取消',\n },\n mainMenu: {\n items: {\n practiceLabel: '练习',\n practiceHintWith: (name) => `开始 ${name}`,\n practiceHintNone: '请先选词典',\n dictLabel: '词典',\n dictHint: '浏览、下载、设为默认',\n wordLabel: '查词',\n wordHint: '在本地词典中搜索',\n statsLabel: '统计',\n statsHint: '历史与趋势',\n configLabel: '设置',\n configHint: '修改偏好',\n stealthLabel: '隐身',\n stealthHint: '隐身练习模式',\n quitLabel: '退出',\n quitHint: 'Esc 或 Ctrl+C 退出',\n },\n hint: '↑/↓ 移动 · Enter 确认 · 字母直达',\n helpHint: '? 帮助',\n },\n dict: {\n title: '词典',\n loading: '加载词典中…',\n entries: (n) => `${n} 部词典`,\n filterPlaceholder: '输入过滤',\n local: '已下载 ✓',\n notLocal: '未下载',\n defaultMark: '默认 ★',\n tagsLabel: (tags) => `标签:${tags}`,\n wordsLabel: (n) => `${n} 词`,\n pulling: (id) => `拉取 ${id} 中…`,\n removing: (id) => `删除 ${id} 中…`,\n errorOn: (id, msg) => `${id} 出错:${msg}`,\n footer: '↑/↓ 选择 · Enter 操作 · Ctrl+K 更多 · Esc 返回',\n action: {\n title: '当前词典',\n setDefault: '设为默认',\n practice: '立即练习',\n delete: '删除本地',\n },\n command: {\n title: '更多功能',\n pull: '拉取选中',\n import: '导入 .json',\n refreshList: '更新词典列表',\n },\n },\n config: {\n title: '设置',\n fields: {\n defaultDict: '默认词典',\n defaultMode: '默认模式',\n accent: '发音',\n mirror: '词典镜像源',\n chapterSize: '章节单词数',\n autoplayPronunciation: '自动播放发音',\n soundsMaster: '音效总开关',\n soundsKeystroke: '按键音',\n soundsFeedback: '反馈音',\n soundsKeySound: '按键音色',\n language: '语言',\n stealth: '隐身模式',\n },\n enumValues: {\n stealth: { off: '关闭', menu: '主菜单显示', default: '默认练习模式' },\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 },\n audio: {\n noPlayer: '! 未在 PATH 中找到音频播放器(尝试 afplay/ffplay/mpg123/paplay/aplay/powershell)。音效已禁用。',\n },\n report: {\n title: '本次会话',\n duration: '总时长',\n practiced: '练习用时',\n chapters: '完成章节',\n words: '词数',\n accuracy: '准确率',\n wpm: '速度',\n newMistakes: '新错词',\n farewell: '下次见。',\n notPracticed: '本次未练习',\n },\n help: {\n title: '帮助',\n subtitle: '全部快捷键',\n sections: {\n main: '主菜单',\n practice: '练习',\n dict: '词典',\n config: '设置',\n stats: '统计',\n word: '查词',\n global: '全局',\n },\n keys: {\n navigate: '↑/↓ 移动选项',\n select: 'Enter 确认 / 继续',\n letterJump: '字母键 直达菜单项',\n pause: 'Esc 暂停练习',\n skip: 'Ctrl+N 跳过当前词(不计错)',\n replay: 'Tab 重播发音',\n resume: 'Enter 继续练习',\n backMenu: 'Esc 返回上一屏',\n backScreen: 'Esc 关闭面板 / 返回',\n nextChapter: 'Enter 下一章',\n reviewMistakes: 'm 复习错词',\n filter: '输入 过滤列表',\n itemActions: 'Enter 弹出动作面板',\n moreActions: 'Ctrl+K 弹出更多功能',\n cycleWindow: '←/→ 切换日窗口',\n stealthToggle: 'Ctrl+I 切换隐身信息行',\n helpScreen: '? 打开本帮助页',\n quit: 'Ctrl+C 立即退出',\n },\n footer: 'Esc 返回',\n },\n stealth: {\n paused: 'paused',\n chapterDone: 'chapter done',\n resumeHint: 'Enter resume · Esc menu',\n nextHint: 'Enter next · Esc menu',\n pausedHintRight: 'Enter 继续',\n nextHintRight: 'Enter 下一章',\n infoChipLabel: '信息',\n infoFmt: (dict, chapter, completed, total, wpm, accPct) =>\n `${dict} · ${chapter} · ${completed}/${total} · ${wpm} wpm · ${accPct}%`,\n },\n};\n","export type Lang = 'zh' | 'en';\nexport type LangPref = 'auto' | Lang;\n\nfunction pickFromString(s: string | undefined): Lang | null {\n if (!s) return null;\n const lower = s.toLowerCase();\n if (lower.startsWith('zh')) return 'zh';\n if (lower.startsWith('en')) return 'en';\n return null;\n}\n\nexport function detectLocale(pref: LangPref): Lang {\n if (pref === 'zh' || pref === 'en') return pref;\n const env =\n process.env.LC_ALL ||\n process.env.LC_MESSAGES ||\n process.env.LANG ||\n process.env.LANGUAGE;\n const fromEnv = pickFromString(env);\n if (fromEnv) return fromEnv;\n try {\n const intlLocale = Intl.DateTimeFormat().resolvedOptions().locale;\n const fromIntl = pickFromString(intlLocale);\n if (fromIntl) return fromIntl;\n } catch {\n // ignore\n }\n return 'en';\n}\n","import { Box, Text } from 'ink';\n\nexport const PALETTE = {\n accent: '#5eead4',\n muted: '#6b7280',\n text: '#e5e7eb',\n primary: '#7dcfff',\n success: '#86efac',\n warning: '#fbbf24',\n error: '#f87171',\n} as const;\n\ntype Props = {\n target: string;\n typed: string;\n error?: boolean;\n hideTarget?: boolean;\n};\n\nexport function BigWord({ target, typed, error = false, hideTarget = false }: Props) {\n const chars = [...target];\n const typedChars = [...typed];\n\n return (\n <Box paddingY={4} justifyContent=\"center\">\n {chars.map((ch, i) => {\n const isTyped = i < typedChars.length;\n const display = hideTarget && !isTyped ? '_' : isTyped ? typedChars[i]! : ch;\n const color = error\n ? PALETTE.error\n : isTyped\n ? PALETTE.accent\n : PALETTE.muted;\n return (\n <Text key={i} bold color={color}>\n {display}\n </Text>\n );\n })}\n </Box>\n );\n}\n"],"mappings":"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","names":["createContext","useContext","useState","useCallback","jsx","NavContext","NavProvider","initial","children","stack","setStack","navigate","frame","s","replace","back","reset","current","useNav","ctx","createContext","useContext","useMemo","en","name","n","tags","id","msg","q","date","c","t","completed","total","dict","chapter","wpm","accPct","zh","pickFromString","s","lower","detectLocale","pref","env","fromEnv","intlLocale","fromIntl","jsx","StringsContext","createContext","StringsProvider","pref","children","value","useMemo","lang","detectLocale","zh","en","useStrings","ctx","useContext","pickStrings","pref","lang","detectLocale","zh","en","Box","Text","jsx","PALETTE","BigWord","target","typed","error","hideTarget","chars","typedChars","ch","i","isTyped","display","color"]}
@@ -0,0 +1,3 @@
1
+ import{a as R,g as C}from"./chunk-UPYHZMDS.js";import{a as L,b as k}from"./chunk-2GTGXODM.js";import{a as B,b as D,c as w,d as H}from"./chunk-2MRNI465.js";import{a as P,b as T,c as N,d as E,f as a}from"./chunk-QEX27D7F.js";var _="\x1B[?1049h\x1B[?25l\x1B[2J\x1B[H",I="\x1B[?25h\x1B[?1049l",v=!1,$=!1;function O(){if($)return;$=!0;let e=()=>{if(v){try{process.stdout.write(I)}catch{}v=!1}};process.once("exit",e),process.once("SIGINT",()=>{e(),process.exit(130)}),process.once("SIGTERM",()=>{e(),process.exit(143)})}function W(){v||process.stdout.isTTY&&process.env.QWERTY_NO_ALTSCREEN!=="1"&&(O(),process.stdout.write(_),v=!0)}function M(){if(v){try{process.stdout.write(I)}catch{}v=!1}}import{Suspense as ue,lazy as A,useRef as le}from"react";import{Box as pe,Text as de,useApp as me,useInput as fe}from"ink";import{useEffect as Q,useState as X}from"react";import{Box as U,useStdout as Z}from"ink";import{jsx as ee}from"react/jsx-runtime";function z({children:e}){let{stdout:t}=Z(),[s,r]=X(()=>({rows:t?.rows??24,cols:t?.columns??80}));return Q(()=>{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),M()}},[]),ee(U,{width:s.cols,height:s.rows,flexDirection:"column",children:e})}import{createContext as te,useContext as re,useEffect as oe,useState as ne}from"react";import{jsx as ie}from"react/jsx-runtime";var F=te({warning:null,ready:!1});function Y({disabled:e,children:t}){let[s,r]=ne({warning:null,ready:!1});return oe(()=>{let o=!1;return R(e).then(()=>{o||r({warning:C(),ready:!0})}).catch(()=>{o||r({warning:null,ready:!0})}),()=>{o=!0}},[e]),ie(F.Provider,{value:s,children:t})}function q(){return re(F)}import{useState as se}from"react";import{Box as y,Text as m,useApp as ae,useInput as ce}from"ink";import{jsx as h,jsxs as g}from"react/jsx-runtime";function V({cfg:e}){let[t,s]=se(0),{exit:r}=ae(),o=T(),f=q(),l=E(),p=D(e.defaultDict),n=l.mainMenu.items,x=c=>{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"}})},d=[{key:"p",label:n.practiceLabel,hint:e.defaultDict?n.practiceHintWith(H(p,24)):n.practiceHintNone,run:()=>x(e.stealth==="default")}];(e.stealth==="menu"||e.stealth==="default")&&d.push({key:"b",label:n.stealthLabel,hint:n.stealthHint,run:()=>x(!0)}),d.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 J=Math.max(...d.map(c=>w(c.label)))+4;return ce((c,b)=>{if(b.escape){r();return}if(b.upArrow&&s(u=>(u-1+d.length)%d.length),b.downArrow&&s(u=>(u+1)%d.length),b.return){d[t].run();return}if(c==="?"){o.navigate({name:"help"});return}for(let u of d)if(c===u.key){u.run();return}}),g(y,{flexDirection:"column",paddingX:2,paddingY:1,width:"100%",children:[g(y,{children:[h(m,{bold:!0,color:a.accent,children:l.app.title}),g(m,{color:a.muted,children:[" \xB7 ",l.app.subtitle]})]}),h(y,{marginTop:2,flexDirection:"column",children:d.map((c,b)=>{let u=b===t,K=" ".repeat(Math.max(0,J-w(c.label)));return g(y,{children:[h(m,{color:u?a.accent:a.muted,children:u?"\u258C ":" "}),g(m,{color:u?a.accent:a.muted,children:["[",c.key,"]"]}),h(m,{children:" "}),g(m,{bold:u,color:u?a.text:a.muted,children:[c.label,K]}),h(m,{color:a.muted,children:c.hint})]},c.key)})}),h(y,{marginTop:2,children:g(m,{color:a.muted,children:[l.mainMenu.hint," \xB7 ",l.mainMenu.helpHint]})}),f.warning&&h(y,{marginTop:1,children:h(m,{color:a.warning,children:l.audio.noPlayer})})]})}import{jsx as i}from"react/jsx-runtime";var he=A(()=>import("./PracticeScreen-LLUTKFXL.js").then(e=>({default:e.PracticeScreen}))),ge=A(()=>import("./DictBrowser-SZVB5W25.js").then(e=>({default:e.DictBrowser}))),Se=A(()=>import("./ConfigEditor-GXFVIJP3.js").then(e=>({default:e.ConfigEditor}))),xe=A(()=>import("./StatsViewer-EY2N2LP3.js").then(e=>({default:e.StatsViewer}))),be=A(()=>import("./WordLookup-UPEDLVKF.js").then(e=>({default:e.WordLookup}))),we=A(()=>import("./HelpScreen-OUP5G5UG.js").then(e=>({default:e.HelpScreen})));function ve(){return i(pe,{alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:i(de,{color:a.muted,children:"\u2026"})})}function nt({initial:e,initialCfg:t,inline:s=!1}){return i(L,{initialCfg:t,children:i(ye,{children:i(B,{children:i(Y,{disabled:!t.sounds.master,children:i(P,{initial:e,children:s?i(j,{inline:!0}):i(z,{children:i(j,{})})})})})})})}function ye({children:e}){let{cfg:t}=k();return i(N,{pref:t.language,children:e})}function Ae(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=T(),{cfg:s}=k(),{exit:r}=me(),o=le(null);fe((n,x)=>{x.ctrl&&n==="c"&&r()});let f=t.current,l=Ae(f);o.current!==l&&(!e&&process.stdout.isTTY&&process.stdout.write("\x1B[2J\x1B[H"),o.current=l);let p=(()=>{switch(f.name){case"main":return i(V,{cfg:s});case"practice":return i(he,{params:f.params});case"dict":return i(ge,{params:f.params});case"config":return i(Se,{});case"stats":return i(xe,{});case"word":return i(be,{});case"help":return i(we,{})}})();return i(ue,{fallback:i(ve,{}),children:p})}import S from"chalk";import Te from"boxen";function pt(){M()}function G(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 Me(e){return e>=90?S.green:e<75?S.dim:t=>t}function ke(e,t,s){let r=[],o=[t.report.duration];e.chaptersCompleted===0?o.push(t.report.notPracticed):(o.push(t.report.practiced,t.report.chapters,t.report.words,t.report.accuracy,t.report.wpm),e.newMistakeWords>0&&o.push(t.report.newMistakes));let f=Math.max(...o.map(w)),l=n=>n+" ".repeat(Math.max(0,f-w(n))),p=(n,x)=>`${S.dim(l(n))} ${x}`;if(r.push(p(t.report.duration,G(e.totalDurationMs,s))),e.chaptersCompleted===0)r.push(S.dim(t.report.notPracticed));else{r.push(p(t.report.practiced,G(e.practiceMs,s))),r.push(p(t.report.chapters,String(e.chaptersCompleted))),r.push(p(t.report.words,String(e.wordCount)));let n=Math.round(e.accuracy*1e3)/10;r.push(p(t.report.accuracy,Me(n)(`${n}%`))),r.push(p(t.report.wpm,String(e.wpm))),e.newMistakeWords>0&&r.push(p(t.report.newMistakes,S.red(String(e.newMistakeWords))))}return r.push(""),r.push(S.dim.italic(t.report.farewell)),r}function dt(e,t,s){if(e.startedAt===null&&e.chaptersCompleted===0)return;let r=ke(e,t,s).join(`
2
+ `);console.log(Te(r,{title:S.bold.cyan(t.report.title),titleAlignment:"left",borderStyle:"round",borderColor:"gray",padding:{top:1,bottom:1,left:3,right:3},margin:{top:1,bottom:1,left:2,right:0}}))}export{W as a,nt as b,pt as c,dt as d};
3
+ //# sourceMappingURL=chunk-RF5SVFBO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/util/altscreen.ts","../src/ui/App.tsx","../src/ui/Fullscreen.tsx","../src/ui/audio-context.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 { createContext, useContext, useEffect, useState, type ReactNode } from 'react';\nimport { initAudio, audioWarning } from '../infra/audio.js';\n\ntype AudioStatus = { warning: string | null; ready: boolean };\n\nconst AudioStatusContext = createContext<AudioStatus>({ warning: null, ready: false });\n\nexport function AudioStatusProvider({\n disabled,\n children,\n}: {\n disabled: boolean;\n children: ReactNode;\n}) {\n const [status, setStatus] = useState<AudioStatus>({ warning: null, ready: false });\n\n useEffect(() => {\n let cancelled = false;\n initAudio(disabled)\n .then(() => {\n if (cancelled) return;\n setStatus({ warning: audioWarning(), ready: true });\n })\n .catch(() => {\n if (cancelled) return;\n setStatus({ warning: null, ready: true });\n });\n return () => {\n cancelled = true;\n };\n }, [disabled]);\n\n return <AudioStatusContext.Provider value={status}>{children}</AudioStatusContext.Provider>;\n}\n\nexport function useAudioStatus(): AudioStatus {\n return useContext(AudioStatusContext);\n}\n","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 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 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 <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 <Box marginTop={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: 1, bottom: 1, 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","createContext","useContext","useEffect","useState","jsx","AudioStatusContext","createContext","AudioStatusProvider","disabled","children","status","setStatus","useState","useEffect","cancelled","initAudio","audioWarning","useAudioStatus","useContext","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","items","truncateName","labelW","it","visibleWidth","useInput","input","key","i","Box","Text","PALETTE","active","pad","jsx","PracticeScreen","lazy","m","DictBrowser","ConfigEditor","StatsViewer","WordLookup","HelpScreen","LazyFallback","Box","Text","PALETTE","App","initial","initialCfg","inline","AppStateProvider","LangBridge","RegistryProvider","AudioStatusProvider","NavProvider","Router","Fullscreen","children","cfg","useAppState","StringsProvider","screenKey","frame","p","nav","useNav","exit","useApp","lastKeyRef","useRef","useInput","input","key","screen","MainMenu","Suspense","chalk","boxen","ensureMainScreen","leaveAltScreen","fmtDuration","ms","lang","total","m","s","accColor","pct","chalk","buildLines","r","lines","allLabels","labelW","visibleWidth","padLabel","row","label","value","accPct","printSessionReport","content","boxen"]}
@@ -0,0 +1,2 @@
1
+ import{a as y}from"./chunk-ELWVQGDK.js";import{a as w,d as p}from"./chunk-6QICLHIY.js";import{a as s,b as m,d as f,e as h,f as u}from"./chunk-6KRVNT2S.js";import{createHash as g}from"crypto";import{stat as P,copyFile as S}from"fs/promises";var d=null;async function D(){return d||(d=(await import("undici")).request,d)}var F="https://cdn.jsdelivr.net/gh/RealKai42/qwerty-learner@master",x="https://raw.githubusercontent.com/RealKai42/qwerty-learner/master";async function q(t,r=2){let n=await D(),o;for(let e=0;e<=r;e++)try{let a=await n(t,{headersTimeout:1e4,bodyTimeout:3e4});if(a.statusCode>=400)throw new Error(`HTTP ${a.statusCode}`);return await a.body.text()}catch(a){o=a,e<r&&await new Promise(i=>setTimeout(i,500*(e+1)))}throw o instanceof Error?o:new Error(String(o))}function R(t,r){let o=`/public${r.startsWith("/")?r:`/${r}`}`,e=`${F}${o}`,a=`${x}${o}`;return t==="jsdelivr"?[e,a]:[a,e]}async function v(t){let r=await p(t);if(!r)throw new Error(`Unknown dictionary id: ${t}`);let n=await y();await m();let o=R(n.mirror,r.url),e=null,a;for(let l of o)try{e=await q(l);break}catch(b){a=b}if(!e)throw new Error(`Failed to download dictionary ${t} from any mirror: ${a instanceof Error?a.message:a}`);let i;try{i=JSON.parse(e)}catch(l){throw new Error(`Dictionary ${t} is not valid JSON: ${l.message}`)}let c=w.safeParse(i);if(!c.success)throw new Error(`Dictionary ${t} failed schema validation: ${c.error.issues[0]?.message}`);let E=g("sha256").update(e).digest("hex");await u(s.dictFile(t),c.data);let $={sha256:E,size:e.length,fetchedAt:new Date().toISOString(),id:t};return await u(s.dictMeta(t),$),{words:c.data,size:e.length}}async function I(t,r){if(!/^[a-z0-9-]+$/.test(r))throw new Error(`Invalid id "${r}". Must match /^[a-z0-9-]+$/`);if(await m(),!await f(t))throw new Error(`File not found: ${t}`);let n=await h(t),o=w.safeParse(n);if(!o.success)throw new Error(`Import rejected: ${o.error.issues[0]?.message}`);await S(t,s.dictFile(r));let e=(await P(s.dictFile(r))).size,i={sha256:g("sha256").update(JSON.stringify(o.data)).digest("hex"),size:e,fetchedAt:new Date().toISOString(),id:r};return await u(s.dictMeta(r),i),{words:o.data,size:e}}async function z(t){let r=await h(s.dictFile(t));if(!r)return null;let n=w.safeParse(r);if(!n.success)throw new Error(`Local dictionary ${t} corrupt: ${n.error.message}`);return n.data}async function N(t){return f(s.dictFile(t))}async function O(t){let r=await z(t);if(r)return r;let{words:n}=await v(t);return n}async function L(t){let{unlink:r}=await import("fs/promises"),n=!1;for(let o of[s.dictFile(t),s.dictMeta(t)])try{await r(o),n=!0}catch(e){if(e.code!=="ENOENT")throw e}return n}export{v as a,I as b,z as c,N as d,O as e,L as f};
2
+ //# sourceMappingURL=chunk-TP77EGJ2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/infra/dict-downloader.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { stat, copyFile } from 'node:fs/promises';\nimport { WordArraySchema, type Word, type DictionaryEntry } from '../domain/dictionary.js';\nimport { paths, ensureDirs } from './paths.js';\nimport { exists, readJson, writeJsonAtomic } from './fs-store.js';\nimport { findEntry } from './registry-store.js';\nimport { loadConfig } from './config-store.js';\n\n// undici is only needed when actually pulling a dictionary from the network.\n// Deferring its import keeps it out of the boot module graph (~47ms saved).\ntype RequestFn = typeof import('undici').request;\nlet cachedRequest: RequestFn | null = null;\nasync function loadRequest(): Promise<RequestFn> {\n if (cachedRequest) return cachedRequest;\n const mod = await import('undici');\n cachedRequest = mod.request;\n return cachedRequest;\n}\n\nconst JSDELIVR_BASE = 'https://cdn.jsdelivr.net/gh/RealKai42/qwerty-learner@master';\nconst GITHUB_RAW_BASE = 'https://raw.githubusercontent.com/RealKai42/qwerty-learner/master';\n\ntype Meta = { sha256: string; size: number; fetchedAt: string; id: string };\n\nasync function fetchWithRetry(url: string, retries = 2): Promise<string> {\n const request = await loadRequest();\n let lastErr: unknown;\n for (let i = 0; i <= retries; i++) {\n try {\n const res = await request(url, { headersTimeout: 10000, bodyTimeout: 30000 });\n if (res.statusCode >= 400) throw new Error(`HTTP ${res.statusCode}`);\n return await res.body.text();\n } catch (err) {\n lastErr = err;\n if (i < retries) await new Promise((r) => setTimeout(r, 500 * (i + 1)));\n }\n }\n throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));\n}\n\nfunction buildUrls(mirror: 'jsdelivr' | 'github', relUrl: string): string[] {\n // Registry urls are web-relative (\"/dicts/X.json\"); upstream repo serves them\n // from /public/, so prepend that prefix when hitting source mirrors.\n const path = relUrl.startsWith('/') ? relUrl : `/${relUrl}`;\n const repoPath = `/public${path}`;\n const jsd = `${JSDELIVR_BASE}${repoPath}`;\n const gh = `${GITHUB_RAW_BASE}${repoPath}`;\n return mirror === 'jsdelivr' ? [jsd, gh] : [gh, jsd];\n}\n\nexport async function pullDictionary(id: string): Promise<{ words: Word[]; size: number }> {\n const entry = await findEntry(id);\n if (!entry) throw new Error(`Unknown dictionary id: ${id}`);\n const cfg = await loadConfig();\n await ensureDirs();\n\n const urls = buildUrls(cfg.mirror, entry.url);\n let body: string | null = null;\n let lastErr: unknown;\n for (const u of urls) {\n try {\n body = await fetchWithRetry(u);\n break;\n } catch (err) {\n lastErr = err;\n }\n }\n if (!body) {\n throw new Error(\n `Failed to download dictionary ${id} from any mirror: ${lastErr instanceof Error ? lastErr.message : lastErr}`,\n );\n }\n\n let json: unknown;\n try {\n json = JSON.parse(body);\n } catch (err) {\n throw new Error(`Dictionary ${id} is not valid JSON: ${(err as Error).message}`);\n }\n\n const parsed = WordArraySchema.safeParse(json);\n if (!parsed.success) {\n throw new Error(`Dictionary ${id} failed schema validation: ${parsed.error.issues[0]?.message}`);\n }\n\n const sha = createHash('sha256').update(body).digest('hex');\n await writeJsonAtomic(paths.dictFile(id), parsed.data);\n const meta: Meta = { sha256: sha, size: body.length, fetchedAt: new Date().toISOString(), id };\n await writeJsonAtomic(paths.dictMeta(id), meta);\n return { words: parsed.data, size: body.length };\n}\n\nexport async function importDictionary(file: string, id: string): Promise<{ words: Word[]; size: number }> {\n if (!/^[a-z0-9-]+$/.test(id)) {\n throw new Error(`Invalid id \"${id}\". Must match /^[a-z0-9-]+$/`);\n }\n await ensureDirs();\n if (!(await exists(file))) throw new Error(`File not found: ${file}`);\n\n const raw = await readJson<unknown>(file);\n const parsed = WordArraySchema.safeParse(raw);\n if (!parsed.success) {\n throw new Error(`Import rejected: ${parsed.error.issues[0]?.message}`);\n }\n await copyFile(file, paths.dictFile(id));\n const size = (await stat(paths.dictFile(id))).size;\n const sha = createHash('sha256').update(JSON.stringify(parsed.data)).digest('hex');\n const meta: Meta = { sha256: sha, size, fetchedAt: new Date().toISOString(), id };\n await writeJsonAtomic(paths.dictMeta(id), meta);\n return { words: parsed.data, size };\n}\n\nexport async function loadLocalDictionary(id: string): Promise<Word[] | null> {\n const data = await readJson<unknown>(paths.dictFile(id));\n if (!data) return null;\n const parsed = WordArraySchema.safeParse(data);\n if (!parsed.success) throw new Error(`Local dictionary ${id} corrupt: ${parsed.error.message}`);\n return parsed.data;\n}\n\nexport async function isLocallyAvailable(id: string): Promise<boolean> {\n return exists(paths.dictFile(id));\n}\n\nexport async function ensureDictionary(id: string): Promise<Word[]> {\n const local = await loadLocalDictionary(id);\n if (local) return local;\n const { words } = await pullDictionary(id);\n return words;\n}\n\nexport async function removeDictionary(id: string): Promise<boolean> {\n const { unlink } = await import('node:fs/promises');\n let removed = false;\n for (const p of [paths.dictFile(id), paths.dictMeta(id)]) {\n try {\n await unlink(p);\n removed = true;\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;\n }\n }\n return removed;\n}\n"],"mappings":"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","names":["createHash","stat","copyFile","cachedRequest","loadRequest","JSDELIVR_BASE","GITHUB_RAW_BASE","fetchWithRetry","url","retries","request","lastErr","i","res","err","r","buildUrls","mirror","relUrl","repoPath","jsd","gh","pullDictionary","id","entry","findEntry","cfg","loadConfig","ensureDirs","urls","body","u","json","parsed","WordArraySchema","sha","createHash","writeJsonAtomic","paths","meta","importDictionary","file","exists","raw","readJson","copyFile","size","stat","loadLocalDictionary","data","isLocallyAvailable","ensureDictionary","local","words","removeDictionary","unlink","removed","p"]}
@@ -0,0 +1,2 @@
1
+ import{a as i,e as a,f as c}from"./chunk-6KRVNT2S.js";import{z as o}from"zod";var u=o.record(o.string(),o.object({count:o.number().int().nonnegative(),lastSeen:o.string(),dictIds:o.array(o.string()).default([])}));async function f(){let t=await a(i.mistakes);if(!t)return{};let e=u.safeParse(t);return e.success?e.data:(console.warn("Mistake book is corrupt; starting fresh"),{})}async function M(t){await c(i.mistakes,t)}function g(t,e,s,r=1){let n=t[e]??{count:0,lastSeen:new Date(0).toISOString(),dictIds:[]},k=n.dictIds.includes(s)?n.dictIds:[...n.dictIds,s];return{...t,[e]:{count:n.count+r,lastSeen:new Date().toISOString(),dictIds:k}}}function B(t,e){return Object.entries(t).sort((s,r)=>r[1].count-s[1].count).slice(0,e)}export{f as a,M as b,g as c,B as d};
2
+ //# sourceMappingURL=chunk-UPA4JFCH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/domain/mistakes.ts"],"sourcesContent":["import { z } from 'zod';\nimport { paths } from '../infra/paths.js';\nimport { readJson, writeJsonAtomic } from '../infra/fs-store.js';\n\nexport const MistakeBookSchema = z.record(\n z.string(),\n z.object({\n count: z.number().int().nonnegative(),\n lastSeen: z.string(),\n dictIds: z.array(z.string()).default([]),\n }),\n);\n\nexport type MistakeBook = z.infer<typeof MistakeBookSchema>;\n\nexport async function loadMistakes(): Promise<MistakeBook> {\n const raw = await readJson<unknown>(paths.mistakes);\n if (!raw) return {};\n const parsed = MistakeBookSchema.safeParse(raw);\n if (!parsed.success) {\n console.warn('Mistake book is corrupt; starting fresh');\n return {};\n }\n return parsed.data;\n}\n\nexport async function saveMistakes(book: MistakeBook): Promise<void> {\n await writeJsonAtomic(paths.mistakes, book);\n}\n\nexport function bump(book: MistakeBook, word: string, dictId: string, delta = 1): MistakeBook {\n const prev = book[word] ?? { count: 0, lastSeen: new Date(0).toISOString(), dictIds: [] };\n const dictIds = prev.dictIds.includes(dictId) ? prev.dictIds : [...prev.dictIds, dictId];\n return {\n ...book,\n [word]: {\n count: prev.count + delta,\n lastSeen: new Date().toISOString(),\n dictIds,\n },\n };\n}\n\nexport function topN(book: MistakeBook, n: number): Array<[string, MistakeBook[string]]> {\n return Object.entries(book)\n .sort((a, b) => b[1].count - a[1].count)\n .slice(0, n);\n}\n"],"mappings":"sDAAA,OAAS,KAAAA,MAAS,MAIX,IAAMC,EAAoBC,EAAE,OACjCA,EAAE,OAAO,EACTA,EAAE,OAAO,CACP,MAAOA,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EACpC,SAAUA,EAAE,OAAO,EACnB,QAASA,EAAE,MAAMA,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,CAAC,CACzC,CAAC,CACH,EAIA,eAAsBC,GAAqC,CACzD,IAAMC,EAAM,MAAMC,EAAkBC,EAAM,QAAQ,EAClD,GAAI,CAACF,EAAK,MAAO,CAAC,EAClB,IAAMG,EAASN,EAAkB,UAAUG,CAAG,EAC9C,OAAKG,EAAO,QAILA,EAAO,MAHZ,QAAQ,KAAK,yCAAyC,EAC/C,CAAC,EAGZ,CAEA,eAAsBC,EAAaC,EAAkC,CACnE,MAAMC,EAAgBJ,EAAM,SAAUG,CAAI,CAC5C,CAEO,SAASE,EAAKF,EAAmBG,EAAcC,EAAgBC,EAAQ,EAAgB,CAC5F,IAAMC,EAAON,EAAKG,CAAI,GAAK,CAAE,MAAO,EAAG,SAAU,IAAI,KAAK,CAAC,EAAE,YAAY,EAAG,QAAS,CAAC,CAAE,EAClFI,EAAUD,EAAK,QAAQ,SAASF,CAAM,EAAIE,EAAK,QAAU,CAAC,GAAGA,EAAK,QAASF,CAAM,EACvF,MAAO,CACL,GAAGJ,EACH,CAACG,CAAI,EAAG,CACN,MAAOG,EAAK,MAAQD,EACpB,SAAU,IAAI,KAAK,EAAE,YAAY,EACjC,QAAAE,CACF,CACF,CACF,CAEO,SAASC,EAAKR,EAAmBS,EAAiD,CACvF,OAAO,OAAO,QAAQT,CAAI,EACvB,KAAK,CAACU,EAAGC,IAAMA,EAAE,CAAC,EAAE,MAAQD,EAAE,CAAC,EAAE,KAAK,EACtC,MAAM,EAAGD,CAAC,CACf","names":["z","MistakeBookSchema","z","loadMistakes","raw","readJson","paths","parsed","saveMistakes","book","writeJsonAtomic","bump","word","dictId","delta","prev","dictIds","topN","n","a","b"]}
@@ -0,0 +1,2 @@
1
+ import{a as b,b as w,c as p,d as g}from"./chunk-6KRVNT2S.js";var u={startedAt:null,chapters:[]};function I(e=Date.now()){u.startedAt===null&&(u.startedAt=e)}function E(e){u.startedAt===null&&(u.startedAt=Date.now()),u.chapters.push(e)}function W(e=Date.now()){let r=u.chapters,t=r.reduce((s,i)=>s+i.wordCount,0),o=r.reduce((s,i)=>s+i.errors,0),a=r.reduce((s,i)=>s+i.durationMs,0),f=a/6e4,d=f>0?Math.round(t/f*10)/10:0,l=new Set;for(let s of r)for(let i of Object.keys(s.perWordErrors))l.add(i);let c=t===0?1:Math.max(0,(t-l.size)/t);return{startedAt:u.startedAt,totalDurationMs:u.startedAt===null?0:e-u.startedAt,chaptersCompleted:r.length,wordCount:t,errors:o,wpm:d,accuracy:c,newMistakeWords:l.size,practiceMs:a}}import{spawn as h}from"child_process";import{mkdir as k,rename as Q,writeFile as A}from"fs/promises";import{join as P,dirname as C}from"path";var x=[{kind:"afplay",cmd:"afplay",args:e=>[e],supports:"both"},{kind:"ffplay",cmd:"ffplay",args:e=>["-nodisp","-autoexit","-loglevel","quiet",e],supports:"both"},{kind:"mpg123",cmd:"mpg123",args:e=>["-q",e],supports:"mp3"},{kind:"paplay",cmd:"paplay",args:e=>[e],supports:"wav"},{kind:"aplay",cmd:"aplay",args:e=>["-q",e],supports:"wav"},{kind:"powershell",cmd:"powershell",args:e=>["-NoProfile","-Command",`(New-Object Media.SoundPlayer '${e}').PlaySync();`],supports:"wav"}],q="https://dict.youdao.com/dictvoice?audio=",n=null,y=null;async function R(e){return new Promise(r=>{let t=h(e,["--version"],{stdio:"ignore"});t.on("error",()=>r(!1)),t.on("exit",()=>r(!0)),setTimeout(()=>{t.kill(),r(!1)},150)})}async function M(){let e=await Promise.all(x.map(async o=>[o,await R(o.cmd)])),r=null,t=null;for(let[o,a]of e)if(a&&(!r&&(o.supports==="wav"||o.supports==="both")&&(r=o),!t&&(o.supports==="mp3"||o.supports==="both")&&(t=o),r&&t))break;return{wav:r,mp3:t}}async function T(){return(await import("p-queue")).default}async function O(e){if(n)return n;let r=await T();if(e)return n={disabled:!0,wavPlayer:null,mp3Player:null,warning:null,keyQueue:new r({concurrency:1}),feedbackQueue:new r({concurrency:1}),pronQueue:new r({concurrency:1})},n;let{wav:t,mp3:o}=await M(),a=null;return!t&&!o?a="No audio player found on PATH (looked for afplay/ffplay/mpg123/paplay/aplay/powershell). Sounds disabled.":o||(a="No MP3 player found; word pronunciations will be skipped."),n={disabled:!t&&!o,wavPlayer:t,mp3Player:o,warning:a,keyQueue:new r({concurrency:2}),feedbackQueue:new r({concurrency:1}),pronQueue:new r({concurrency:1})},n}function D(e,r){try{let t=h(e.cmd,e.args(r),{detached:!0,stdio:"ignore"});t.on("error",()=>{}),t.unref()}catch{}}function m(e,r){if(!n||n.disabled)return;let t=r==="wav"?n.wavPlayer:n.mp3Player;t&&D(t,e)}function B(){!n||n.disabled||n.keyQueue.size>=2||n.keyQueue.add(async()=>{m(P(p(),"sounds","key-default.wav"),"wav"),await new Promise(e=>setTimeout(e,30))})}function H(){!n||n.disabled||n.feedbackQueue.add(async()=>{m(P(p(),"sounds","correct.wav"),"wav"),await new Promise(e=>setTimeout(e,50))})}function U(){!n||n.disabled||n.feedbackQueue.add(async()=>{m(P(p(),"sounds","wrong.wav"),"wav"),await new Promise(e=>setTimeout(e,50))})}async function S(){return y||(y=(await import("undici")).request,y)}async function v(e,r){let t=b.audioCache(e,r);if(await g(t))return t;await k(C(t),{recursive:!0});let o=r==="us"?2:1,a=`${q}${encodeURIComponent(e)}&type=${o}`;try{let d=await(await S())(a,{headersTimeout:8e3,bodyTimeout:2e4});if(d.statusCode>=400)return null;let l=Buffer.from(await d.body.arrayBuffer());if(l.length<1024)return null;let c=`${t}.tmp`;return await A(c,l),await Q(c,t),t}catch{return null}}async function _(e,r){!n||n.disabled||!n.mp3Player||(await w(),await n.pronQueue.add(async()=>{let t=await v(e,r);t&&m(t,"mp3")}))}async function G(e,r){!n||n.disabled||!n.mp3Player||(await w(),n.pronQueue.add(async()=>{await v(e,r)}))}function J(){return n?.warning??null}export{O as a,B as b,H as c,U as d,_ as e,G as f,J as g,I as h,E as i,W as j};
2
+ //# sourceMappingURL=chunk-UPYHZMDS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/infra/session-tracker.ts","../src/infra/audio.ts"],"sourcesContent":["type ChapterEntry = {\n dictId: string;\n chapterIndex: number;\n mode: string;\n wordCount: number;\n errors: number;\n durationMs: number;\n perWordErrors: Record<string, number>;\n};\n\ntype State = {\n startedAt: number | null;\n chapters: ChapterEntry[];\n};\n\nconst state: State = {\n startedAt: null,\n chapters: [],\n};\n\nexport function start(now = Date.now()): void {\n if (state.startedAt === null) state.startedAt = now;\n}\n\nexport function addChapter(entry: ChapterEntry): void {\n if (state.startedAt === null) state.startedAt = Date.now();\n state.chapters.push(entry);\n}\n\nexport type SessionReport = {\n startedAt: number | null;\n totalDurationMs: number;\n chaptersCompleted: number;\n wordCount: number;\n errors: number;\n wpm: number;\n accuracy: number;\n newMistakeWords: number;\n practiceMs: number;\n};\n\nexport function report(now = Date.now()): SessionReport {\n const chapters = state.chapters;\n const wordCount = chapters.reduce((a, c) => a + c.wordCount, 0);\n const errors = chapters.reduce((a, c) => a + c.errors, 0);\n const practiceMs = chapters.reduce((a, c) => a + c.durationMs, 0);\n const minutes = practiceMs / 60000;\n const wpm = minutes > 0 ? Math.round((wordCount / minutes) * 10) / 10 : 0;\n const errorWordSet = new Set<string>();\n for (const c of chapters) {\n for (const w of Object.keys(c.perWordErrors)) errorWordSet.add(w);\n }\n const accuracy = wordCount === 0 ? 1 : Math.max(0, (wordCount - errorWordSet.size) / wordCount);\n return {\n startedAt: state.startedAt,\n totalDurationMs: state.startedAt === null ? 0 : now - state.startedAt,\n chaptersCompleted: chapters.length,\n wordCount,\n errors,\n wpm,\n accuracy,\n newMistakeWords: errorWordSet.size,\n practiceMs,\n };\n}\n\nexport function reset(): void {\n state.startedAt = null;\n state.chapters = [];\n}\n","import { spawn } from 'node:child_process';\nimport { mkdir, rename, writeFile } from 'node:fs/promises';\nimport { join, dirname } from 'node:path';\nimport { paths, packageAssetsDir, ensureDirs } from './paths.js';\nimport { exists } from './fs-store.js';\n\n// undici and p-queue are dynamically imported at first use to keep them out\n// of the boot module graph — saves ~50ms at startup for the menu/--version\n// paths that don't touch audio at all.\ntype RequestFn = typeof import('undici').request;\ntype PQueueCtor = typeof import('p-queue').default;\n\ntype PlayerKind = 'afplay' | 'paplay' | 'aplay' | 'mpg123' | 'ffplay' | 'powershell';\n\ntype Player = {\n kind: PlayerKind;\n cmd: string;\n args: (file: string) => string[];\n supports: 'wav' | 'mp3' | 'both';\n};\n\nconst CANDIDATES: Player[] = [\n { kind: 'afplay', cmd: 'afplay', args: (f) => [f], supports: 'both' },\n { kind: 'ffplay', cmd: 'ffplay', args: (f) => ['-nodisp', '-autoexit', '-loglevel', 'quiet', f], supports: 'both' },\n { kind: 'mpg123', cmd: 'mpg123', args: (f) => ['-q', f], supports: 'mp3' },\n { kind: 'paplay', cmd: 'paplay', args: (f) => [f], supports: 'wav' },\n { kind: 'aplay', cmd: 'aplay', args: (f) => ['-q', f], supports: 'wav' },\n {\n kind: 'powershell',\n cmd: 'powershell',\n args: (f) => ['-NoProfile', '-Command', `(New-Object Media.SoundPlayer '${f}').PlaySync();`],\n supports: 'wav',\n },\n];\n\nconst PRON_API = 'https://dict.youdao.com/dictvoice?audio=';\n\ntype AudioRuntime = {\n disabled: boolean;\n wavPlayer: Player | null;\n mp3Player: Player | null;\n warning: string | null;\n keyQueue: InstanceType<PQueueCtor>;\n feedbackQueue: InstanceType<PQueueCtor>;\n pronQueue: InstanceType<PQueueCtor>;\n};\n\nlet runtime: AudioRuntime | null = null;\nlet cachedRequest: RequestFn | null = null;\n\nasync function isExecutable(cmd: string): Promise<boolean> {\n return new Promise((resolve) => {\n const probe = spawn(cmd, ['--version'], { stdio: 'ignore' });\n probe.on('error', () => resolve(false));\n probe.on('exit', () => resolve(true));\n // 150ms is plenty for `--version` (typical < 20ms); was 500ms × 6 serial\n // candidates = up to 3s of useless waiting at boot.\n setTimeout(() => {\n probe.kill();\n resolve(false);\n }, 150);\n });\n}\n\nasync function detect(): Promise<{ wav: Player | null; mp3: Player | null }> {\n // Parallel probe — all 6 candidates run simultaneously, total time ≈\n // single isExecutable timeout (150ms) instead of 6× that serially.\n const checks = await Promise.all(\n CANDIDATES.map(async (p) => [p, await isExecutable(p.cmd)] as const),\n );\n let wav: Player | null = null;\n let mp3: Player | null = null;\n for (const [p, ok] of checks) {\n if (!ok) continue;\n if (!wav && (p.supports === 'wav' || p.supports === 'both')) wav = p;\n if (!mp3 && (p.supports === 'mp3' || p.supports === 'both')) mp3 = p;\n if (wav && mp3) break;\n }\n return { wav, mp3 };\n}\n\nasync function loadPQueue(): Promise<PQueueCtor> {\n const mod = await import('p-queue');\n return mod.default;\n}\n\nexport async function initAudio(disabledByConfig: boolean): Promise<AudioRuntime> {\n if (runtime) return runtime;\n const PQueue = await loadPQueue();\n if (disabledByConfig) {\n runtime = {\n disabled: true,\n wavPlayer: null,\n mp3Player: null,\n warning: null,\n keyQueue: new PQueue({ concurrency: 1 }),\n feedbackQueue: new PQueue({ concurrency: 1 }),\n pronQueue: new PQueue({ concurrency: 1 }),\n };\n return runtime;\n }\n const { wav, mp3 } = await detect();\n let warning: string | null = null;\n if (!wav && !mp3) {\n warning = 'No audio player found on PATH (looked for afplay/ffplay/mpg123/paplay/aplay/powershell). Sounds disabled.';\n } else if (!mp3) {\n warning = 'No MP3 player found; word pronunciations will be skipped.';\n }\n runtime = {\n disabled: !wav && !mp3,\n wavPlayer: wav,\n mp3Player: mp3,\n warning,\n keyQueue: new PQueue({ concurrency: 2 }),\n feedbackQueue: new PQueue({ concurrency: 1 }),\n pronQueue: new PQueue({ concurrency: 1 }),\n };\n return runtime;\n}\n\nfunction spawnPlay(player: Player, file: string): void {\n try {\n const child = spawn(player.cmd, player.args(file), {\n detached: true,\n stdio: 'ignore',\n });\n child.on('error', () => {\n // swallow; runtime will be disabled if many failures stack up\n });\n child.unref();\n } catch {\n /* fail-soft */\n }\n}\n\nfunction playFile(file: string, kind: 'wav' | 'mp3'): void {\n if (!runtime || runtime.disabled) return;\n const player = kind === 'wav' ? runtime.wavPlayer : runtime.mp3Player;\n if (!player) return;\n spawnPlay(player, file);\n}\n\nexport function playKeystroke(): void {\n if (!runtime || runtime.disabled) return;\n // Drop if queue is saturated; we never want to delay typing.\n if (runtime.keyQueue.size >= 2) return;\n void runtime.keyQueue.add(async () => {\n playFile(join(packageAssetsDir(), 'sounds', 'key-default.wav'), 'wav');\n await new Promise((r) => setTimeout(r, 30));\n });\n}\n\nexport function playCorrect(): void {\n if (!runtime || runtime.disabled) return;\n void runtime.feedbackQueue.add(async () => {\n playFile(join(packageAssetsDir(), 'sounds', 'correct.wav'), 'wav');\n await new Promise((r) => setTimeout(r, 50));\n });\n}\n\nexport function playWrong(): void {\n if (!runtime || runtime.disabled) return;\n void runtime.feedbackQueue.add(async () => {\n playFile(join(packageAssetsDir(), 'sounds', 'wrong.wav'), 'wav');\n await new Promise((r) => setTimeout(r, 50));\n });\n}\n\nasync function loadRequest(): Promise<RequestFn> {\n if (cachedRequest) return cachedRequest;\n const mod = await import('undici');\n cachedRequest = mod.request;\n return cachedRequest;\n}\n\nasync function downloadPronunciation(word: string, accent: 'us' | 'uk'): Promise<string | null> {\n const cacheFile = paths.audioCache(word, accent);\n if (await exists(cacheFile)) return cacheFile;\n await mkdir(dirname(cacheFile), { recursive: true });\n const type = accent === 'us' ? 2 : 1;\n const url = `${PRON_API}${encodeURIComponent(word)}&type=${type}`;\n try {\n const request = await loadRequest();\n const res = await request(url, { headersTimeout: 8000, bodyTimeout: 20000 });\n if (res.statusCode >= 400) return null;\n const buf = Buffer.from(await res.body.arrayBuffer());\n if (buf.length < 1024) return null;\n const tmp = `${cacheFile}.tmp`;\n await writeFile(tmp, buf);\n await rename(tmp, cacheFile);\n return cacheFile;\n } catch {\n return null;\n }\n}\n\nexport async function playPronunciation(word: string, accent: 'us' | 'uk'): Promise<void> {\n if (!runtime || runtime.disabled || !runtime.mp3Player) return;\n await ensureDirs();\n await runtime.pronQueue.add(async () => {\n const file = await downloadPronunciation(word, accent);\n if (file) playFile(file, 'mp3');\n });\n}\n\nexport async function prefetchPronunciation(word: string, accent: 'us' | 'uk'): Promise<void> {\n if (!runtime || runtime.disabled || !runtime.mp3Player) return;\n await ensureDirs();\n void runtime.pronQueue.add(async () => {\n await downloadPronunciation(word, accent);\n });\n}\n\nexport function audioWarning(): string | null {\n return runtime?.warning ?? null;\n}\n\nexport function audioDisabled(): boolean {\n return runtime?.disabled ?? true;\n}\n"],"mappings":"6DAeA,IAAMA,EAAe,CACnB,UAAW,KACX,SAAU,CAAC,CACb,EAEO,SAASC,EAAMC,EAAM,KAAK,IAAI,EAAS,CACxCF,EAAM,YAAc,OAAMA,EAAM,UAAYE,EAClD,CAEO,SAASC,EAAWC,EAA2B,CAChDJ,EAAM,YAAc,OAAMA,EAAM,UAAY,KAAK,IAAI,GACzDA,EAAM,SAAS,KAAKI,CAAK,CAC3B,CAcO,SAASC,EAAOH,EAAM,KAAK,IAAI,EAAkB,CACtD,IAAMI,EAAWN,EAAM,SACjBO,EAAYD,EAAS,OAAO,CAACE,EAAGC,IAAMD,EAAIC,EAAE,UAAW,CAAC,EACxDC,EAASJ,EAAS,OAAO,CAACE,EAAGC,IAAMD,EAAIC,EAAE,OAAQ,CAAC,EAClDE,EAAaL,EAAS,OAAO,CAACE,EAAGC,IAAMD,EAAIC,EAAE,WAAY,CAAC,EAC1DG,EAAUD,EAAa,IACvBE,EAAMD,EAAU,EAAI,KAAK,MAAOL,EAAYK,EAAW,EAAE,EAAI,GAAK,EAClEE,EAAe,IAAI,IACzB,QAAWL,KAAKH,EACd,QAAWS,KAAK,OAAO,KAAKN,EAAE,aAAa,EAAGK,EAAa,IAAIC,CAAC,EAElE,IAAMC,EAAWT,IAAc,EAAI,EAAI,KAAK,IAAI,GAAIA,EAAYO,EAAa,MAAQP,CAAS,EAC9F,MAAO,CACL,UAAWP,EAAM,UACjB,gBAAiBA,EAAM,YAAc,KAAO,EAAIE,EAAMF,EAAM,UAC5D,kBAAmBM,EAAS,OAC5B,UAAAC,EACA,OAAAG,EACA,IAAAG,EACA,SAAAG,EACA,gBAAiBF,EAAa,KAC9B,WAAAH,CACF,CACF,CChEA,OAAS,SAAAM,MAAa,gBACtB,OAAS,SAAAC,EAAO,UAAAC,EAAQ,aAAAC,MAAiB,cACzC,OAAS,QAAAC,EAAM,WAAAC,MAAe,OAmB9B,IAAMC,EAAuB,CAC3B,CAAE,KAAM,SAAU,IAAK,SAAU,KAAOC,GAAM,CAACA,CAAC,EAAG,SAAU,MAAO,EACpE,CAAE,KAAM,SAAU,IAAK,SAAU,KAAOA,GAAM,CAAC,UAAW,YAAa,YAAa,QAASA,CAAC,EAAG,SAAU,MAAO,EAClH,CAAE,KAAM,SAAU,IAAK,SAAU,KAAOA,GAAM,CAAC,KAAMA,CAAC,EAAG,SAAU,KAAM,EACzE,CAAE,KAAM,SAAU,IAAK,SAAU,KAAOA,GAAM,CAACA,CAAC,EAAG,SAAU,KAAM,EACnE,CAAE,KAAM,QAAS,IAAK,QAAS,KAAOA,GAAM,CAAC,KAAMA,CAAC,EAAG,SAAU,KAAM,EACvE,CACE,KAAM,aACN,IAAK,aACL,KAAOA,GAAM,CAAC,aAAc,WAAY,kCAAkCA,CAAC,gBAAgB,EAC3F,SAAU,KACZ,CACF,EAEMC,EAAW,2CAYbC,EAA+B,KAC/BC,EAAkC,KAEtC,eAAeC,EAAaC,EAA+B,CACzD,OAAO,IAAI,QAASC,GAAY,CAC9B,IAAMC,EAAQC,EAAMH,EAAK,CAAC,WAAW,EAAG,CAAE,MAAO,QAAS,CAAC,EAC3DE,EAAM,GAAG,QAAS,IAAMD,EAAQ,EAAK,CAAC,EACtCC,EAAM,GAAG,OAAQ,IAAMD,EAAQ,EAAI,CAAC,EAGpC,WAAW,IAAM,CACfC,EAAM,KAAK,EACXD,EAAQ,EAAK,CACf,EAAG,GAAG,CACR,CAAC,CACH,CAEA,eAAeG,GAA8D,CAG3E,IAAMC,EAAS,MAAM,QAAQ,IAC3BX,EAAW,IAAI,MAAOY,GAAM,CAACA,EAAG,MAAMP,EAAaO,EAAE,GAAG,CAAC,CAAU,CACrE,EACIC,EAAqB,KACrBC,EAAqB,KACzB,OAAW,CAACF,EAAGG,CAAE,IAAKJ,EACpB,GAAKI,IACD,CAACF,IAAQD,EAAE,WAAa,OAASA,EAAE,WAAa,UAASC,EAAMD,GAC/D,CAACE,IAAQF,EAAE,WAAa,OAASA,EAAE,WAAa,UAASE,EAAMF,GAC/DC,GAAOC,GAAK,MAElB,MAAO,CAAE,IAAAD,EAAK,IAAAC,CAAI,CACpB,CAEA,eAAeE,GAAkC,CAE/C,OADY,KAAM,QAAO,SAAS,GACvB,OACb,CAEA,eAAsBC,EAAUC,EAAkD,CAChF,GAAIf,EAAS,OAAOA,EACpB,IAAMgB,EAAS,MAAMH,EAAW,EAChC,GAAIE,EACF,OAAAf,EAAU,CACR,SAAU,GACV,UAAW,KACX,UAAW,KACX,QAAS,KACT,SAAU,IAAIgB,EAAO,CAAE,YAAa,CAAE,CAAC,EACvC,cAAe,IAAIA,EAAO,CAAE,YAAa,CAAE,CAAC,EAC5C,UAAW,IAAIA,EAAO,CAAE,YAAa,CAAE,CAAC,CAC1C,EACOhB,EAET,GAAM,CAAE,IAAAU,EAAK,IAAAC,CAAI,EAAI,MAAMJ,EAAO,EAC9BU,EAAyB,KAC7B,MAAI,CAACP,GAAO,CAACC,EACXM,EAAU,4GACAN,IACVM,EAAU,6DAEZjB,EAAU,CACR,SAAU,CAACU,GAAO,CAACC,EACnB,UAAWD,EACX,UAAWC,EACX,QAAAM,EACA,SAAU,IAAID,EAAO,CAAE,YAAa,CAAE,CAAC,EACvC,cAAe,IAAIA,EAAO,CAAE,YAAa,CAAE,CAAC,EAC5C,UAAW,IAAIA,EAAO,CAAE,YAAa,CAAE,CAAC,CAC1C,EACOhB,CACT,CAEA,SAASkB,EAAUC,EAAgBC,EAAoB,CACrD,GAAI,CACF,IAAMC,EAAQf,EAAMa,EAAO,IAAKA,EAAO,KAAKC,CAAI,EAAG,CACjD,SAAU,GACV,MAAO,QACT,CAAC,EACDC,EAAM,GAAG,QAAS,IAAM,CAExB,CAAC,EACDA,EAAM,MAAM,CACd,MAAQ,CAER,CACF,CAEA,SAASC,EAASF,EAAcG,EAA2B,CACzD,GAAI,CAACvB,GAAWA,EAAQ,SAAU,OAClC,IAAMmB,EAASI,IAAS,MAAQvB,EAAQ,UAAYA,EAAQ,UACvDmB,GACLD,EAAUC,EAAQC,CAAI,CACxB,CAEO,SAASI,GAAsB,CAChC,CAACxB,GAAWA,EAAQ,UAEpBA,EAAQ,SAAS,MAAQ,GACxBA,EAAQ,SAAS,IAAI,SAAY,CACpCsB,EAASG,EAAKC,EAAiB,EAAG,SAAU,iBAAiB,EAAG,KAAK,EACrE,MAAM,IAAI,QAASC,GAAM,WAAWA,EAAG,EAAE,CAAC,CAC5C,CAAC,CACH,CAEO,SAASC,GAAoB,CAC9B,CAAC5B,GAAWA,EAAQ,UACnBA,EAAQ,cAAc,IAAI,SAAY,CACzCsB,EAASG,EAAKC,EAAiB,EAAG,SAAU,aAAa,EAAG,KAAK,EACjE,MAAM,IAAI,QAASC,GAAM,WAAWA,EAAG,EAAE,CAAC,CAC5C,CAAC,CACH,CAEO,SAASE,GAAkB,CAC5B,CAAC7B,GAAWA,EAAQ,UACnBA,EAAQ,cAAc,IAAI,SAAY,CACzCsB,EAASG,EAAKC,EAAiB,EAAG,SAAU,WAAW,EAAG,KAAK,EAC/D,MAAM,IAAI,QAASC,GAAM,WAAWA,EAAG,EAAE,CAAC,CAC5C,CAAC,CACH,CAEA,eAAeG,GAAkC,CAC/C,OAAI7B,IAEJA,GADY,KAAM,QAAO,QAAQ,GACb,QACbA,EACT,CAEA,eAAe8B,EAAsBC,EAAcC,EAA6C,CAC9F,IAAMC,EAAYC,EAAM,WAAWH,EAAMC,CAAM,EAC/C,GAAI,MAAMG,EAAOF,CAAS,EAAG,OAAOA,EACpC,MAAMG,EAAMC,EAAQJ,CAAS,EAAG,CAAE,UAAW,EAAK,CAAC,EACnD,IAAMK,EAAON,IAAW,KAAO,EAAI,EAC7BO,EAAM,GAAGzC,CAAQ,GAAG,mBAAmBiC,CAAI,CAAC,SAASO,CAAI,GAC/D,GAAI,CAEF,IAAME,EAAM,MADI,MAAMX,EAAY,GACRU,EAAK,CAAE,eAAgB,IAAM,YAAa,GAAM,CAAC,EAC3E,GAAIC,EAAI,YAAc,IAAK,OAAO,KAClC,IAAMC,EAAM,OAAO,KAAK,MAAMD,EAAI,KAAK,YAAY,CAAC,EACpD,GAAIC,EAAI,OAAS,KAAM,OAAO,KAC9B,IAAMC,EAAM,GAAGT,CAAS,OACxB,aAAMU,EAAUD,EAAKD,CAAG,EACxB,MAAMG,EAAOF,EAAKT,CAAS,EACpBA,CACT,MAAQ,CACN,OAAO,IACT,CACF,CAEA,eAAsBY,EAAkBd,EAAcC,EAAoC,CACpF,CAACjC,GAAWA,EAAQ,UAAY,CAACA,EAAQ,YAC7C,MAAM+C,EAAW,EACjB,MAAM/C,EAAQ,UAAU,IAAI,SAAY,CACtC,IAAMoB,EAAO,MAAMW,EAAsBC,EAAMC,CAAM,EACjDb,GAAME,EAASF,EAAM,KAAK,CAChC,CAAC,EACH,CAEA,eAAsB4B,EAAsBhB,EAAcC,EAAoC,CACxF,CAACjC,GAAWA,EAAQ,UAAY,CAACA,EAAQ,YAC7C,MAAM+C,EAAW,EACZ/C,EAAQ,UAAU,IAAI,SAAY,CACrC,MAAM+B,EAAsBC,EAAMC,CAAM,CAC1C,CAAC,EACH,CAEO,SAASgB,GAA8B,CAC5C,OAAOjD,GAAS,SAAW,IAC7B","names":["state","start","now","addChapter","entry","report","chapters","wordCount","a","c","errors","practiceMs","minutes","wpm","errorWordSet","w","accuracy","spawn","mkdir","rename","writeFile","join","dirname","CANDIDATES","f","PRON_API","runtime","cachedRequest","isExecutable","cmd","resolve","probe","spawn","detect","checks","p","wav","mp3","ok","loadPQueue","initAudio","disabledByConfig","PQueue","warning","spawnPlay","player","file","child","playFile","kind","playKeystroke","join","packageAssetsDir","r","playCorrect","playWrong","loadRequest","downloadPronunciation","word","accent","cacheFile","paths","exists","mkdir","dirname","type","url","res","buf","tmp","writeFile","rename","playPronunciation","ensureDirs","prefetchPronunciation","audioWarning"]}