@qnroa/qtype 0.1.4 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +118 -0
- package/CHANGELOG.zh.md +89 -0
- package/dist/cli/commands/config/action.js +68 -0
- package/dist/cli/commands/config/i18n.js +8 -0
- package/dist/cli/commands/config/index.js +50 -0
- package/dist/cli/registry/help.js +6 -0
- package/dist/core/schemas/both.js +15 -13
- package/dist/core/schemas/repo.js +8 -15
- package/dist/core/schemas/user.js +4 -0
- package/dist/core/typing/engine/TypingInput.js +18 -0
- package/dist/view/assets/index-B8531CqV.css +1 -0
- package/dist/view/assets/{index-BOawXuCn.js → index-B_HnQEsb.js} +2 -2
- package/dist/view/assets/{index-Qldz13H1.js → index-BbG90BH8.js} +1 -1
- package/dist/view/assets/index-eEQRUOZD.js +194 -0
- package/dist/view/index.html +2 -2
- package/package.json +4 -2
- package/dist/view/assets/index-DYR47C2S.css +0 -1
- package/dist/view/assets/index-U1REj2X4.js +0 -192
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,124 @@ can land in any minor bump (`0.x.0`).
|
|
|
13
13
|
|
|
14
14
|
## [Unreleased]
|
|
15
15
|
|
|
16
|
+
## [0.2.1] — 2026-09-01
|
|
17
|
+
|
|
18
|
+
Discoverability pass for `qtype config`. Users no longer have to grep
|
|
19
|
+
the source (or the wiki) to find out what keys they can set.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- **`qtype config --help` now lists every configurable key** at the
|
|
23
|
+
bottom of the help output — one line per key with its default, scope,
|
|
24
|
+
description, and enum options where applicable. Same source of truth
|
|
25
|
+
as the actual schema, so it stays honest.
|
|
26
|
+
- **`qtype config list --all`** — verbose per-key view: current value
|
|
27
|
+
(or default if unset), source (which file / default), scope, and
|
|
28
|
+
enum options. Complements `list` (only shows keys the user has
|
|
29
|
+
actually set).
|
|
30
|
+
- **`SchemaEntry.description` + `SchemaEntry.options`** fields, both
|
|
31
|
+
optional. Populated for every currently-registered key so the two
|
|
32
|
+
new help surfaces have something to show.
|
|
33
|
+
- **`CommandModule.afterHelp`** hook — commands can append reference
|
|
34
|
+
material after the standard help sections. `config` uses it; other
|
|
35
|
+
commands can add their own without touching the registry.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
- Cleaned up in-line doc comments in the schema files (they've been
|
|
39
|
+
moved into the `description` field where possible, so they're
|
|
40
|
+
reachable at runtime).
|
|
41
|
+
|
|
42
|
+
## [0.2.0] — 2026-08-31
|
|
43
|
+
|
|
44
|
+
Substantial rework of the typing page. The screen-flow is now a real
|
|
45
|
+
finite state machine (xstate), the ad-hoc `paused / stampKind /
|
|
46
|
+
awaitingAdvance / finished / isReview` booleans are gone, and every
|
|
47
|
+
screen state's UI is derived from one place rather than pieced together
|
|
48
|
+
by view code. Behaviour visible to the user shifts in a few spots —
|
|
49
|
+
see below.
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
- **Session persistence for done state.** Marking a card done writes
|
|
53
|
+
to the persistent kv (`useDoneState`), so the completion streak
|
|
54
|
+
survives page reloads and shows up on Home right next to the
|
|
55
|
+
material title as `3 / 12`. Openng a material now lands on the
|
|
56
|
+
first card the user hasn't finished, not always card 1.
|
|
57
|
+
- **Completed-card review mode.** Coming back to a card you've already
|
|
58
|
+
finished shows the answer painted in green (via `engine.markAllHit`)
|
|
59
|
+
with the HUD still available; pressing ▶ / Alt+Space wipes the done
|
|
60
|
+
flag and lets you retype from scratch.
|
|
61
|
+
- **Card-jump list panel.** The quick-input's card submode now lists
|
|
62
|
+
every card in the queue with its title, done state, and a "current"
|
|
63
|
+
marker. Type text to filter by title, digits to jump by number.
|
|
64
|
+
- **After-pass advance mode.** New setting `advanceMode` — the default
|
|
65
|
+
is now **Confirm** (stay on the finished card until the user hits
|
|
66
|
+
the confirm key) rather than auto-advance after 700ms. Users who
|
|
67
|
+
prefer the old behaviour can flip it back to Auto in settings.
|
|
68
|
+
- **Confirm key setting.** Choose Enter or Space as the "next" key.
|
|
69
|
+
The chosen key is highlighted on the on-screen keyboard during the
|
|
70
|
+
confirmation window so mobile users can see where to tap.
|
|
71
|
+
- **Global keyboard channel.** Typing keeps working after clicks that
|
|
72
|
+
move focus out of the arena — a window-level `keydown` listener
|
|
73
|
+
forwards keystrokes to the engine so users don't have to click back
|
|
74
|
+
into the (invisible) editor after every misplaced tap.
|
|
75
|
+
- **Mobile virtual keyboard inset.** The layout reserves space at the
|
|
76
|
+
bottom equal to the soft-keyboard height (`visualViewport` API), so
|
|
77
|
+
the answer text no longer disappears under the keyboard on iOS /
|
|
78
|
+
Android.
|
|
79
|
+
- **Region-aware TTS default.** New `auto` TTS provider picks Youdao
|
|
80
|
+
for `zh-*` browsers and Google elsewhere, with the "browser" engine
|
|
81
|
+
as fallback. Explicit picks still take precedence.
|
|
82
|
+
- **`?path=` search param on the typing route.** The HUD "back to
|
|
83
|
+
library" button returns to the exact folder the user came from
|
|
84
|
+
instead of the library root.
|
|
85
|
+
|
|
86
|
+
### Changed
|
|
87
|
+
- **Typing page UI reimplemented as an xstate machine** (`idle` /
|
|
88
|
+
`typing` / `paused` / `passed` / `completed` / `finished`), replacing
|
|
89
|
+
five ad-hoc `useState` booleans. Every screen state's mask / stamp /
|
|
90
|
+
pill / keyboard-hint / HUD-play-icon is derived from one `derived`
|
|
91
|
+
struct rather than assembled by the view. Fixes a class of races
|
|
92
|
+
where two effects would fight over the same visual (e.g. the pass
|
|
93
|
+
stamp being covered by the pause mask because a review-restore
|
|
94
|
+
effect fired half a frame too early).
|
|
95
|
+
- **`useTypingEngine` is now keyed on card id, not answer text.** Two
|
|
96
|
+
cards whose answers happened to be identical used to share one
|
|
97
|
+
engine, leaking cursor / hit-miss marks between them. Every card
|
|
98
|
+
now gets a fresh engine on entry.
|
|
99
|
+
- **First character of each card is now highlighted on the on-screen
|
|
100
|
+
keyboard.** `useHudBridge` derives the first keystroke from the
|
|
101
|
+
engine at mount instead of waiting for the arena to push one.
|
|
102
|
+
- **Backspaced characters render as untyped.** The engine still tags
|
|
103
|
+
them as `Recover` for downstream logic, but the dotted underline
|
|
104
|
+
UI decoration is removed — visually the slot goes back to "pending".
|
|
105
|
+
- **HUD ▶ / Alt+Space in `completed` state means "restart this card"**,
|
|
106
|
+
not "start typing". It clears the done marker + resets the engine +
|
|
107
|
+
moves the machine to typing.
|
|
108
|
+
- **Paused → next card lands in idle, not typing.** Advancing while
|
|
109
|
+
paused now shows the pause mask on the new card so the user still
|
|
110
|
+
has to explicitly start.
|
|
111
|
+
- **Backspace / stamp / advance cleanup are co-located with the user
|
|
112
|
+
action that triggers them**, not smuggled through state-transition
|
|
113
|
+
effects that could fire on incidental transitions (e.g. advancing
|
|
114
|
+
to an un-done card would previously have clobbered the destination's
|
|
115
|
+
done state).
|
|
116
|
+
|
|
117
|
+
### Removed
|
|
118
|
+
- **`sessionPassed` snapshot store.** Reviewing a passed card no
|
|
119
|
+
longer replays the exact keystroke-by-keystroke record from
|
|
120
|
+
sessionStorage — a static "all hit" view is used instead. That
|
|
121
|
+
removed a whole storage layer and closed the quota concern for
|
|
122
|
+
users with thousands of cards.
|
|
123
|
+
- **`TypingInput.snapshot` / `restore`** (and the `TypingInputSnapshot`
|
|
124
|
+
type). Superseded by `markAllHit`.
|
|
125
|
+
|
|
126
|
+
### Fixed
|
|
127
|
+
- **Focus loss no longer stops typing.** Clicking HUD buttons or
|
|
128
|
+
outside the arena used to silently drop keystrokes; the global
|
|
129
|
+
keydown fallback catches them now.
|
|
130
|
+
- **Mobile answer text no longer covered by the soft keyboard.**
|
|
131
|
+
- **HUD "back" button returns to origin folder** (see `?path=`
|
|
132
|
+
above), instead of always the library root.
|
|
133
|
+
|
|
16
134
|
## [0.1.4] — 2026-08-29
|
|
17
135
|
|
|
18
136
|
Two small UX changes: a tappable resume overlay and a completion
|
package/CHANGELOG.zh.md
CHANGED
|
@@ -12,6 +12,95 @@ English: [CHANGELOG.md](https://www.npmjs.com/package/@qnroa/qtype?activeTab=cod
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.2.1] — 2026-09-01
|
|
16
|
+
|
|
17
|
+
`qtype config` 的可发现性升级。用户不再需要翻源码或 wiki 才知道能设
|
|
18
|
+
置哪些 key。
|
|
19
|
+
|
|
20
|
+
### 新增
|
|
21
|
+
- **`qtype config --help` 现在会在底部列出所有可配置的 key**,一行一个,
|
|
22
|
+
含默认值、作用域、描述、枚举选项(如果有)。数据源就是 schema 本身,
|
|
23
|
+
不会脱节。
|
|
24
|
+
- **`qtype config list --all`** —— 详版:每个 key 显示当前值(或未设置
|
|
25
|
+
时的默认)、来源(哪个文件 / 默认)、作用域、枚举选项。和 `list`(只
|
|
26
|
+
显示用户设过的)互补。
|
|
27
|
+
- **`SchemaEntry.description` + `SchemaEntry.options`** 两个可选字段。
|
|
28
|
+
现在每个已注册 key 都填了,给上面两个新的 help 出口提供内容。
|
|
29
|
+
- **`CommandModule.afterHelp` 钩子** —— 命令可以在标准 help 段落后追加
|
|
30
|
+
参考内容。`config` 用它,其它命令想加也行,不用改 registry。
|
|
31
|
+
|
|
32
|
+
### 变更
|
|
33
|
+
- 清理了 schema 文件里的行内文档注释(能放进 `description` 的都挪进
|
|
34
|
+
去了,这样运行时就能读到)。
|
|
35
|
+
|
|
36
|
+
## [0.2.0] — 2026-08-31
|
|
37
|
+
|
|
38
|
+
打字页面的整体架构重写。屏幕流程改成了真正的有限状态机(xstate),
|
|
39
|
+
之前散在各处的 `paused / stampKind / awaitingAdvance / finished /
|
|
40
|
+
isReview` 布尔位全部替换成一个状态机,每种状态下的 UI 都从一个统一
|
|
41
|
+
的派生里读,而不是各处自己拼。用户可感知的行为变化见下面。
|
|
42
|
+
|
|
43
|
+
### 新增
|
|
44
|
+
- **完成状态持久化。** 通过卡片会写到本地存储(`useDoneState`),
|
|
45
|
+
刷新页面 / 重启浏览器都保留,Home 里的材料卡上会显示 `3 / 12`
|
|
46
|
+
完成进度。打开材料时会跳到第一张未完成的卡,不再永远从第一张开始。
|
|
47
|
+
- **已完成卡片的回看模式。** 切回已通过的卡时,答案会整体显示为绿色
|
|
48
|
+
(`engine.markAllHit`),HUD 仍可用;按 ▶ / Alt+Space 会清掉这张
|
|
49
|
+
卡的完成标记并重置引擎,回到从头开始的可打状态。
|
|
50
|
+
- **卡片跳转列表面板。** 命令面板的卡片跳转模式现在会列出 queue 中
|
|
51
|
+
每一张卡的标题、完成状态和当前位置。输入文本按标题模糊过滤,输入
|
|
52
|
+
数字按序号直接跳转。
|
|
53
|
+
- **通过后推进模式。** 新增设置 `advanceMode`,默认改为 **确认**(打
|
|
54
|
+
完卡等待用户按确认键才进入下一张),不再是原本的 700ms 自动跳。
|
|
55
|
+
想要旧行为的用户可以在设置里切回自动。
|
|
56
|
+
- **确认键设置。** 可选 Enter 或 Space 作为「进入下一张」的键。选中
|
|
57
|
+
的键在等待确认时会在虚拟键盘上高亮,方便移动端用户看到点哪。
|
|
58
|
+
- **全局键盘通道。** 点击了 HUD 按钮 / 卡片之外的区域之后依然可以继续
|
|
59
|
+
打字 —— 一个 window 级别的 keydown 监听会兜底转发到引擎,用户不用
|
|
60
|
+
再回点隐形编辑器。
|
|
61
|
+
- **移动端虚拟键盘避让。** 布局底部会预留和虚拟键盘等高的空间
|
|
62
|
+
(`visualViewport` API),答题区不再被系统键盘挡住。
|
|
63
|
+
- **区域感知的 TTS 默认值。** 新增 `auto` 语音源,浏览器语言 `zh-*`
|
|
64
|
+
用有道,其他用 Google,都失败降级到浏览器自带。用户手动选的会覆盖。
|
|
65
|
+
- **typing route 支持 `?path=` search 参数。** HUD 上的「返回」按钮
|
|
66
|
+
会回到用户进入该材料前所在的文件夹,而不是永远回到根目录。
|
|
67
|
+
|
|
68
|
+
### 变更
|
|
69
|
+
- **打字页 UI 用 xstate 状态机重写**(`idle` / `typing` / `paused` /
|
|
70
|
+
`passed` / `completed` / `finished` 六态),替换了原本的 5 个散乱
|
|
71
|
+
`useState` 布尔。所有状态的遮罩 / 笑脸 / pill / 键盘高亮 / HUD 播
|
|
72
|
+
放按钮图标都从一个 `derived` 结构派生。修复了一类由两个副作用抢
|
|
73
|
+
同一个视觉状态引起的 bug(比如笑脸被 review restore 副作用早触发
|
|
74
|
+
盖住)。
|
|
75
|
+
- **`useTypingEngine` 现在按 cardId 缓存,不再按答案文本。** 之前两
|
|
76
|
+
张答案文字相同的卡会共用一个引擎,光标 / 对错标记会串。现在每张
|
|
77
|
+
卡进入时都是一个干净的新引擎。
|
|
78
|
+
- **每张卡的第一个字符现在会在键盘上高亮。** `useHudBridge` 在挂载
|
|
79
|
+
时就从引擎派生出第一个 keystroke,不再等 arena push。
|
|
80
|
+
- **删除的字符按待打字状态渲染。** 引擎仍然会打 `Recover` 标记(下
|
|
81
|
+
游还在用),但视觉上不再显示原本的点状下划线。
|
|
82
|
+
- **completed 状态下按 ▶ / Alt+Space 表示「重打这张卡」**,不是「开始
|
|
83
|
+
打字」。会清完成标记 + 重置引擎 + 转到 typing。
|
|
84
|
+
- **暂停状态下切下一张会到 idle,不到 typing。** 新卡进去仍显示暂停
|
|
85
|
+
遮罩,等用户主动开始。
|
|
86
|
+
- **Backspace / stamp / advance 的清理逻辑放到触发它们的用户动作里**,
|
|
87
|
+
不再依赖状态过渡副作用。避免了「切到未通过的卡时误清目标卡完成状
|
|
88
|
+
态」这类 bug。
|
|
89
|
+
|
|
90
|
+
### 删除
|
|
91
|
+
- **`sessionPassed` 快照存储。** 回看已通过的卡不再从 sessionStorage
|
|
92
|
+
回放当时逐键的完整状态,直接使用「全绿」静态视图。移除了一整层存
|
|
93
|
+
储,消除了对几千张卡容量上限的顾虑。
|
|
94
|
+
- **`TypingInput.snapshot` / `restore` 方法**(以及 `TypingInputSnapshot`
|
|
95
|
+
类型)。被 `markAllHit` 取代。
|
|
96
|
+
|
|
97
|
+
### 修复
|
|
98
|
+
- **焦点丢失不再停止打字。** 点击 HUD 按钮 / 卡片之外会让隐形编辑器
|
|
99
|
+
失焦,原来会静默吞掉按键。现在有全局 keydown 兜底。
|
|
100
|
+
- **移动端答题区不再被虚拟键盘遮挡。**
|
|
101
|
+
- **HUD「返回」按钮回到进入时的目录**(见上面 `?path=`),不再永远回
|
|
102
|
+
到根目录。
|
|
103
|
+
|
|
15
104
|
## [0.1.4] — 2026-08-29
|
|
16
105
|
|
|
17
106
|
两个小 UX 改动:暂停遮罩可以点击继续,卡片打完时会有一个短暂的
|
|
@@ -178,6 +178,12 @@ export async function configUnsetAction(key, opts) {
|
|
|
178
178
|
/** `qtype config list [--global|--local] [--show-origin]` */
|
|
179
179
|
export async function configListAction(opts) {
|
|
180
180
|
const scope = pickScope(opts);
|
|
181
|
+
// --all: enumerate every schema-known key, filling in defaults for
|
|
182
|
+
// ones the user hasn't set. Useful for discovering what can be
|
|
183
|
+
// configured.
|
|
184
|
+
if (opts.all) {
|
|
185
|
+
return listAllKnown(scope);
|
|
186
|
+
}
|
|
181
187
|
const userEntries = scope === 'repo' ? [] : await userKv.list();
|
|
182
188
|
const repoEntries = scope === 'user' ? [] : (await repoKv.available()) ? await repoKv.list() : [];
|
|
183
189
|
const rows = [];
|
|
@@ -220,6 +226,68 @@ export async function configListAction(opts) {
|
|
|
220
226
|
// ═══════════════════════════════════════════════════════════════
|
|
221
227
|
// helpers
|
|
222
228
|
// ═══════════════════════════════════════════════════════════════
|
|
229
|
+
/**
|
|
230
|
+
* List every key known to the schema in the given scope (all if scope
|
|
231
|
+
* is undefined). For each key: current value + source, or default +
|
|
232
|
+
* `(default)` marker if unset, plus type / options / description.
|
|
233
|
+
*/
|
|
234
|
+
async function listAllKnown(scope) {
|
|
235
|
+
const rows = [];
|
|
236
|
+
const seen = new Set();
|
|
237
|
+
if (scope !== 'repo') {
|
|
238
|
+
for (const k of userKv.knownKeys()) {
|
|
239
|
+
if (seen.has(k))
|
|
240
|
+
continue;
|
|
241
|
+
seen.add(k);
|
|
242
|
+
rows.push({ key: k, kv: userKv, scope: 'user' });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (scope !== 'user') {
|
|
246
|
+
for (const k of repoKv.knownKeys()) {
|
|
247
|
+
if (seen.has(k))
|
|
248
|
+
continue;
|
|
249
|
+
seen.add(k);
|
|
250
|
+
rows.push({ key: k, kv: repoKv, scope: 'repo' });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
rows.sort((a, b) => a.key.localeCompare(b.key));
|
|
254
|
+
const repoAvailable = scope === 'user' ? false : await repoKv.available();
|
|
255
|
+
for (const r of rows) {
|
|
256
|
+
const entry = r.kv.getSchema(r.key);
|
|
257
|
+
// Prefer the stored value (from user or repo scope); fall back to
|
|
258
|
+
// the schema default. `get` returns default when unset, so guard
|
|
259
|
+
// with `has` first to know whether the value is user-set.
|
|
260
|
+
let currentDisplay = '';
|
|
261
|
+
let sourceLabel = '';
|
|
262
|
+
const canRead = r.scope === 'user' || repoAvailable;
|
|
263
|
+
if (canRead && (await r.kv.has(r.key))) {
|
|
264
|
+
const raw = await r.kv.get(r.key);
|
|
265
|
+
currentDisplay = formatValue(raw);
|
|
266
|
+
sourceLabel = `${r.scope}:${r.kv.origin}`;
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
currentDisplay = formatValue(entry.default);
|
|
270
|
+
sourceLabel = 'default';
|
|
271
|
+
}
|
|
272
|
+
const parts = [`${r.key}=${currentDisplay}`];
|
|
273
|
+
if (entry.description)
|
|
274
|
+
parts.push(dim(entry.description));
|
|
275
|
+
console.log(parts.join(' '));
|
|
276
|
+
const meta = [];
|
|
277
|
+
if (entry.options && entry.options.length > 0) {
|
|
278
|
+
meta.push(`options: ${entry.options.join(' | ')}`);
|
|
279
|
+
}
|
|
280
|
+
if (entry.default !== undefined && sourceLabel === 'default') {
|
|
281
|
+
// already shown as current; keep meta uncluttered
|
|
282
|
+
}
|
|
283
|
+
else if (entry.default !== undefined) {
|
|
284
|
+
meta.push(`default: ${formatValue(entry.default)}`);
|
|
285
|
+
}
|
|
286
|
+
meta.push(`scope: ${r.scope}`);
|
|
287
|
+
meta.push(`source: ${sourceLabel}`);
|
|
288
|
+
console.log(' ' + dim(meta.join(' · ')));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
223
291
|
/** Format a value as a single-line string. Objects / arrays use JSON. */
|
|
224
292
|
function formatValue(v) {
|
|
225
293
|
if (v == null)
|
|
@@ -7,8 +7,10 @@ export const messages = {
|
|
|
7
7
|
'config.opt.global': '操作 user 层(~/.config/qtype/config.json)',
|
|
8
8
|
'config.opt.local': '操作 repo 层(<repo>/.qtype/config.json)',
|
|
9
9
|
'config.opt.showOrigin': '显示配置项来源(文件路径 / 哪一层)',
|
|
10
|
+
'config.opt.all': '列出所有可配置项(含 schema 里未设置的默认值)',
|
|
10
11
|
'config.example.setLang': '设置 CLI 语言',
|
|
11
12
|
'config.example.list': '列出所有生效配置',
|
|
13
|
+
'config.list.example.all': '列出所有可配置项及默认值',
|
|
12
14
|
'config.get.summary': '读配置',
|
|
13
15
|
'config.get.description': '读取指定 key 的值(默认合并两层;key 不存在 exit 1)',
|
|
14
16
|
'config.get.example.basic': '读 CLI 语言偏好',
|
|
@@ -29,6 +31,8 @@ export const messages = {
|
|
|
29
31
|
'config.list.example.global': '只看 user 层',
|
|
30
32
|
'config.list.msg.empty': '配置为空',
|
|
31
33
|
'config.list.msg.emptyScoped': '配置为空 ({scope})',
|
|
34
|
+
'config.help.keysHeader': '可配置项:',
|
|
35
|
+
'config.help.keysFooter': '用 `qtype config set <key> <value>` 修改;`qtype config list --all` 查看完整状态。',
|
|
32
36
|
},
|
|
33
37
|
'en-US': {
|
|
34
38
|
'config.summary': 'Manage config',
|
|
@@ -38,8 +42,10 @@ export const messages = {
|
|
|
38
42
|
'config.opt.global': 'Operate on user scope (~/.config/qtype/config.json)',
|
|
39
43
|
'config.opt.local': 'Operate on repo scope (<repo>/.qtype/config.json)',
|
|
40
44
|
'config.opt.showOrigin': 'Show where each entry comes from (file path and scope)',
|
|
45
|
+
'config.opt.all': 'List every configurable key, including schema defaults for unset keys',
|
|
41
46
|
'config.example.setLang': 'Set CLI language',
|
|
42
47
|
'config.example.list': 'List all effective config',
|
|
48
|
+
'config.list.example.all': 'List every configurable key with defaults',
|
|
43
49
|
'config.get.summary': 'Get value',
|
|
44
50
|
'config.get.description': 'Get value for a key (merged by default; exit 1 if key not set)',
|
|
45
51
|
'config.get.example.basic': 'Read CLI language preference',
|
|
@@ -60,5 +66,7 @@ export const messages = {
|
|
|
60
66
|
'config.list.example.global': 'Only user scope',
|
|
61
67
|
'config.list.msg.empty': 'Config is empty',
|
|
62
68
|
'config.list.msg.emptyScoped': 'Config is empty ({scope})',
|
|
69
|
+
'config.help.keysHeader': 'Available keys:',
|
|
70
|
+
'config.help.keysFooter': 'Use `qtype config set <key> <value>` to change one; `qtype config list --all` to see the full state.',
|
|
63
71
|
},
|
|
64
72
|
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { t } from '../../i18n/index.js';
|
|
2
2
|
import { messages } from './i18n.js';
|
|
3
|
+
import { userKv, repoKv } from '../../bootstrap.js';
|
|
4
|
+
import { bold, dim } from '../../utils/output.js';
|
|
3
5
|
import { configGetAction, configSetAction, configUnsetAction, configListAction, } from './action.js';
|
|
4
6
|
/** Shared --global / --local / --show-origin options. */
|
|
5
7
|
const scopeOptions = [
|
|
@@ -52,13 +54,59 @@ const listCmd = {
|
|
|
52
54
|
options: [
|
|
53
55
|
...scopeOptions,
|
|
54
56
|
{ flags: '--show-origin', describe: () => t('config.opt.showOrigin') },
|
|
57
|
+
{ flags: '--all', describe: () => t('config.opt.all') },
|
|
55
58
|
],
|
|
56
59
|
examples: () => [
|
|
57
60
|
{ command: 'qtype config list', describe: () => t('config.list.example.merged') },
|
|
58
61
|
{ command: 'qtype config list --global', describe: () => t('config.list.example.global') },
|
|
62
|
+
{ command: 'qtype config list --all', describe: () => t('config.list.example.all') },
|
|
59
63
|
],
|
|
60
64
|
action: async (opts) => configListAction(opts),
|
|
61
65
|
};
|
|
66
|
+
/**
|
|
67
|
+
* Render every configurable key (from both user and repo schemas)
|
|
68
|
+
* as a `key = <default> — description` list. Used by `qtype
|
|
69
|
+
* config --help` so users can discover what can be tuned without
|
|
70
|
+
* hunting through docs.
|
|
71
|
+
*/
|
|
72
|
+
function renderAvailableKeys() {
|
|
73
|
+
const rows = [];
|
|
74
|
+
const seen = new Set();
|
|
75
|
+
for (const k of userKv.knownKeys()) {
|
|
76
|
+
if (seen.has(k))
|
|
77
|
+
continue;
|
|
78
|
+
seen.add(k);
|
|
79
|
+
const e = userKv.getSchema(k);
|
|
80
|
+
rows.push({ key: k, scope: 'user', default: e.default, description: e.description, options: e.options });
|
|
81
|
+
}
|
|
82
|
+
for (const k of repoKv.knownKeys()) {
|
|
83
|
+
if (seen.has(k))
|
|
84
|
+
continue;
|
|
85
|
+
seen.add(k);
|
|
86
|
+
const e = repoKv.getSchema(k);
|
|
87
|
+
rows.push({ key: k, scope: 'repo', default: e.default, description: e.description, options: e.options });
|
|
88
|
+
}
|
|
89
|
+
rows.sort((a, b) => a.key.localeCompare(b.key));
|
|
90
|
+
const fmtDefault = (v) => {
|
|
91
|
+
if (v === undefined)
|
|
92
|
+
return '(no default)';
|
|
93
|
+
if (typeof v === 'string')
|
|
94
|
+
return v || '""';
|
|
95
|
+
return JSON.stringify(v);
|
|
96
|
+
};
|
|
97
|
+
const lines = [bold(t('config.help.keysHeader'))];
|
|
98
|
+
for (const r of rows) {
|
|
99
|
+
lines.push(` ${r.key} ${dim('=')} ${fmtDefault(r.default)} ${dim(`(${r.scope})`)}`);
|
|
100
|
+
if (r.description)
|
|
101
|
+
lines.push(` ${dim(r.description)}`);
|
|
102
|
+
if (r.options && r.options.length > 0) {
|
|
103
|
+
lines.push(` ${dim(`options: ${r.options.join(' | ')}`)}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
lines.push('');
|
|
107
|
+
lines.push(dim(t('config.help.keysFooter')));
|
|
108
|
+
return lines.join('\n');
|
|
109
|
+
}
|
|
62
110
|
export const configCmd = {
|
|
63
111
|
name: 'config',
|
|
64
112
|
messages,
|
|
@@ -67,6 +115,8 @@ export const configCmd = {
|
|
|
67
115
|
examples: () => [
|
|
68
116
|
{ command: 'qtype config set cli.lang zh', describe: () => t('config.example.setLang') },
|
|
69
117
|
{ command: 'qtype config list', describe: () => t('config.example.list') },
|
|
118
|
+
{ command: 'qtype config list --all', describe: () => t('config.list.example.all') },
|
|
70
119
|
],
|
|
120
|
+
afterHelp: () => renderAvailableKeys(),
|
|
71
121
|
subcommands: [getCmd, setCmd, unsetCmd, listCmd],
|
|
72
122
|
};
|
|
@@ -66,6 +66,12 @@ export function applyHelpMetadata(cmd, mod) {
|
|
|
66
66
|
if (mod.seeAlso && mod.seeAlso.length > 0) {
|
|
67
67
|
cmd.addHelpText('after', renderSeeAlso(mod.seeAlso));
|
|
68
68
|
}
|
|
69
|
+
// Free-form afterHelp — printed last, useful for reference lists.
|
|
70
|
+
if (mod.afterHelp) {
|
|
71
|
+
const extra = mod.afterHelp();
|
|
72
|
+
if (extra && extra.trim())
|
|
73
|
+
cmd.addHelpText('after', '\n' + extra);
|
|
74
|
+
}
|
|
69
75
|
// Aliases
|
|
70
76
|
if (mod.aliases && mod.aliases.length > 0) {
|
|
71
77
|
for (const alias of mod.aliases)
|
|
@@ -5,48 +5,50 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { enumValidator, boolValidator, positiveIntValidator, } from '../../store/index.js';
|
|
7
7
|
export const bothSchema = {
|
|
8
|
-
// Repo language. Drives:
|
|
9
|
-
// • which template locale (examples/zh vs examples/en) new repos are
|
|
10
|
-
// scaffolded from
|
|
11
|
-
// • the web UI language — the reader is locked to repo.lang so the UI
|
|
12
|
-
// matches the material's own language
|
|
13
|
-
//
|
|
14
|
-
// Values are BCP 47 codes (`zh-CN` / `en-US`); the short forms `zh` /
|
|
15
|
-
// `en` are accepted for backwards compatibility with older `.qtype/
|
|
16
|
-
// config.json` files and normalised at read time.
|
|
17
8
|
'repo.lang': {
|
|
18
9
|
validator: enumValidator(['zh-CN', 'en-US', 'zh', 'en']),
|
|
19
10
|
default: 'en-US',
|
|
11
|
+
description: 'Repo (and its published web UI) language.',
|
|
12
|
+
options: ['zh-CN', 'en-US'],
|
|
20
13
|
},
|
|
21
14
|
'typing.theme': {
|
|
22
15
|
validator: enumValidator(['mocha', 'latte']),
|
|
23
16
|
default: 'mocha',
|
|
17
|
+
description: 'Web reader theme.',
|
|
18
|
+
options: ['mocha', 'latte'],
|
|
24
19
|
},
|
|
25
20
|
'typing.targetWpm': {
|
|
26
21
|
validator: positiveIntValidator(),
|
|
22
|
+
description: 'Ghost-pacer target speed in WPM. 0 = disabled.',
|
|
27
23
|
},
|
|
28
24
|
'typing.tabBehavior': {
|
|
29
25
|
validator: enumValidator(['reset', 'input', 'disabled']),
|
|
30
26
|
default: 'reset',
|
|
27
|
+
description: 'What pressing Tab in the arena does.',
|
|
28
|
+
options: ['reset', 'input', 'disabled'],
|
|
31
29
|
},
|
|
32
30
|
'typing.hint': {
|
|
33
31
|
validator: enumValidator(['none', 'ghost']),
|
|
34
32
|
default: 'none',
|
|
33
|
+
description: 'Whether to render a faint ghost hint under the caret.',
|
|
34
|
+
options: ['none', 'ghost'],
|
|
35
35
|
},
|
|
36
36
|
'typing.showKeyboard': {
|
|
37
37
|
validator: boolValidator(),
|
|
38
38
|
default: true,
|
|
39
|
+
description: 'Show the on-screen keyboard beneath the arena.',
|
|
40
|
+
options: ['true', 'false'],
|
|
39
41
|
},
|
|
40
42
|
'typing.showHands': {
|
|
41
43
|
validator: boolValidator(),
|
|
42
44
|
default: true,
|
|
45
|
+
description: 'Show the hand overlay on the on-screen keyboard.',
|
|
46
|
+
options: ['true', 'false'],
|
|
43
47
|
},
|
|
44
|
-
// Mermaid pre-render mode. `prerender` (default) uses puppeteer at build
|
|
45
|
-
// time to bake SVGs into dist/material/mermaid.json; `client` ships raw
|
|
46
|
-
// ```mermaid fences and lets the browser render them; `off` treats mermaid
|
|
47
|
-
// as plain code blocks.
|
|
48
48
|
'publish.mermaid': {
|
|
49
49
|
validator: enumValidator(['prerender', 'client', 'off']),
|
|
50
50
|
default: 'prerender',
|
|
51
|
+
description: 'Mermaid diagram handling at `publish build` time.',
|
|
52
|
+
options: ['prerender', 'client', 'off'],
|
|
51
53
|
},
|
|
52
54
|
};
|
|
@@ -6,43 +6,35 @@ import { enumValidator, nonEmptyStringValidator, boolValidator, } from '../../st
|
|
|
6
6
|
export const repoOnlySchema = {
|
|
7
7
|
'repo.name': {
|
|
8
8
|
validator: nonEmptyStringValidator(),
|
|
9
|
+
description: 'Repo display name (folder title, package.json name).',
|
|
9
10
|
},
|
|
10
11
|
// ── Password protection for `publish build` ─────────────
|
|
11
|
-
// On/off toggle. When true, `publish build` reads the other three fields
|
|
12
|
-
// and encrypts every material/*.md before writing to dist.
|
|
13
12
|
'publish.password.enabled': {
|
|
14
13
|
validator: boolValidator(),
|
|
15
14
|
default: false,
|
|
15
|
+
description: 'Encrypt materials at `publish build` time.',
|
|
16
|
+
options: ['true', 'false'],
|
|
16
17
|
},
|
|
17
|
-
// Argon2id PHC-style hash of the plaintext password, used to verify the
|
|
18
|
-
// password entered at build time. NOT the AES key — that's derived
|
|
19
|
-
// separately via `publish.password.aesSalt`. Empty string when protection
|
|
20
|
-
// is off; shape enforced by `publish password set` writing a well-formed
|
|
21
|
-
// PHC string.
|
|
22
18
|
'publish.password.hash': {
|
|
23
19
|
validator: (v) => typeof v === 'string'
|
|
24
20
|
? null
|
|
25
21
|
: { code: 'err.schema.nonEmptyString', fallback: 'must be a string' },
|
|
26
22
|
default: '',
|
|
23
|
+
description: 'Argon2id PHC hash of the publish password (managed by `qtype publish password`).',
|
|
27
24
|
},
|
|
28
|
-
// Base64 salt used with argon2id (password → AES-256 key). Rotated only
|
|
29
|
-
// when the user calls `publish password set` — stable across rebuilds so
|
|
30
|
-
// the same password unlocks previously published sites.
|
|
31
25
|
'publish.password.aesSalt': {
|
|
32
26
|
validator: (v) => typeof v === 'string'
|
|
33
27
|
? null
|
|
34
28
|
: { code: 'err.schema.nonEmptyString', fallback: 'must be a string' },
|
|
35
29
|
default: '',
|
|
30
|
+
description: 'Base64 salt used to derive the AES key (managed by `qtype publish password`).',
|
|
36
31
|
},
|
|
37
|
-
// What to encrypt. `all` covers material/ + any future encrypted assets;
|
|
38
|
-
// `material-only` limits encryption to material/*.md (same behavior in
|
|
39
|
-
// v0 since qtype has no other content streams).
|
|
40
32
|
'publish.password.scope': {
|
|
41
33
|
validator: enumValidator(['all', 'material-only', 'selected']),
|
|
42
34
|
default: 'all',
|
|
35
|
+
description: 'What to encrypt when password protection is on.',
|
|
36
|
+
options: ['all', 'material-only', 'selected'],
|
|
43
37
|
},
|
|
44
|
-
// When scope='selected', the list of file names (relative to material/)
|
|
45
|
-
// to encrypt. Ignored otherwise.
|
|
46
38
|
'publish.password.selected': {
|
|
47
39
|
validator: (v) => Array.isArray(v) && v.every((x) => typeof x === 'string')
|
|
48
40
|
? null
|
|
@@ -51,5 +43,6 @@ export const repoOnlySchema = {
|
|
|
51
43
|
fallback: 'must be an array of strings',
|
|
52
44
|
},
|
|
53
45
|
default: [],
|
|
46
|
+
description: 'File names to encrypt when scope is `selected` (JSON array).',
|
|
54
47
|
},
|
|
55
48
|
};
|
|
@@ -11,9 +11,13 @@ export const userOnlySchema = {
|
|
|
11
11
|
'cli.lang': {
|
|
12
12
|
validator: enumValidator(['zh-CN', 'en-US', 'zh', 'en']),
|
|
13
13
|
default: 'en-US',
|
|
14
|
+
description: 'CLI display language.',
|
|
15
|
+
options: ['zh-CN', 'en-US'],
|
|
14
16
|
},
|
|
15
17
|
'cli.color': {
|
|
16
18
|
validator: enumValidator(['always', 'never', 'auto']),
|
|
17
19
|
default: 'auto',
|
|
20
|
+
description: 'Color output policy for CLI messages.',
|
|
21
|
+
options: ['always', 'never', 'auto'],
|
|
18
22
|
},
|
|
19
23
|
};
|
|
@@ -140,6 +140,24 @@ export class TypingInput {
|
|
|
140
140
|
this.#position = 0;
|
|
141
141
|
this.#notify();
|
|
142
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Mark every character as Hit — used by the route's `completed`
|
|
145
|
+
* state (a card the user finished earlier) so downstream renderers
|
|
146
|
+
* paint the whole answer in green without needing a full snapshot
|
|
147
|
+
* of the original keystroke stream. Purely cosmetic: stats derived
|
|
148
|
+
* from timestamps stay zero.
|
|
149
|
+
*/
|
|
150
|
+
markAllHit() {
|
|
151
|
+
const next = this.#chars.map((c) => ({
|
|
152
|
+
target: c.target,
|
|
153
|
+
typed: c.target,
|
|
154
|
+
attrs: (c.attrs & ~(CharAttr.Miss | CharAttr.Cursor)) | CharAttr.Hit,
|
|
155
|
+
timestamp: 0,
|
|
156
|
+
}));
|
|
157
|
+
this.#chars = next;
|
|
158
|
+
this.#position = this.#length;
|
|
159
|
+
this.#notify();
|
|
160
|
+
}
|
|
143
161
|
get position() {
|
|
144
162
|
return this.#position;
|
|
145
163
|
}
|