@rse/ase 2.0.3 → 2.0.5

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 (198) hide show
  1. package/dst/ase-hook.js +13 -4
  2. package/dst/ase-lib-version.js +14 -5
  3. package/dst/ase-man-catalog.js +229 -0
  4. package/dst/ase-man-render.js +278 -0
  5. package/dst/ase-man-tui.js +636 -0
  6. package/dst/ase-man.js +33 -27
  7. package/dst/ase-setup.js +2 -2
  8. package/dst/ase-spec.js +103 -20
  9. package/dst/ase-task-board-filter.js +5 -4
  10. package/dst/ase-task-board-web-client.js +26 -26
  11. package/dst/man/ase-agent-adhoc.md +1165 -0
  12. package/dst/man/ase-agent-condenser.md +383 -0
  13. package/dst/man/ase-agent-editor.md +441 -0
  14. package/dst/man/ase-agent-estimator.md +477 -0
  15. package/dst/man/ase-agent-fleet.md +358 -0
  16. package/dst/man/ase-agent-implementer.md +521 -0
  17. package/dst/man/ase-agent-integrator.md +449 -0
  18. package/dst/man/ase-agent-qualifier.md +426 -0
  19. package/dst/man/ase-agent-rebooter.md +384 -0
  20. package/dst/man/ase-agent-relay.md +262 -0
  21. package/dst/man/ase-agent-researcher.md +478 -0
  22. package/dst/man/ase-agent.md +628 -0
  23. package/dst/man/ase-artifact-list.md +57 -0
  24. package/dst/man/ase-artifact-name.md +56 -0
  25. package/dst/man/ase-artifact.md +92 -0
  26. package/dst/man/ase-config-delete.md +74 -0
  27. package/dst/man/ase-config-edit.md +83 -0
  28. package/dst/man/ase-config-get.md +70 -0
  29. package/dst/man/ase-config-init.md +110 -0
  30. package/dst/man/ase-config-list.md +68 -0
  31. package/dst/man/ase-config-set.md +92 -0
  32. package/dst/man/ase-config.md +426 -0
  33. package/dst/man/ase-hook-permission-request.md +61 -0
  34. package/dst/man/ase-hook-pre-tool-use.md +96 -0
  35. package/dst/man/ase-hook-session-end.md +71 -0
  36. package/dst/man/ase-hook-session-persona.md +65 -0
  37. package/dst/man/ase-hook-session-start.md +184 -0
  38. package/dst/man/ase-hook-stop.md +52 -0
  39. package/dst/man/ase-hook-user-prompt-submit.md +54 -0
  40. package/dst/man/ase-hook.md +93 -0
  41. package/dst/man/ase-man.md +195 -0
  42. package/dst/man/ase-mcp.md +102 -0
  43. package/dst/man/ase-service-log.md +59 -0
  44. package/dst/man/ase-service-send.md +60 -0
  45. package/dst/man/ase-service-start.md +93 -0
  46. package/dst/man/ase-service-status.md +58 -0
  47. package/dst/man/ase-service-stop.md +52 -0
  48. package/dst/man/ase-service.md +124 -0
  49. package/dst/man/ase-setup-disable.md +67 -0
  50. package/dst/man/ase-setup-enable.md +66 -0
  51. package/dst/man/ase-setup-install.md +116 -0
  52. package/dst/man/ase-setup-mcp.md +213 -0
  53. package/dst/man/ase-setup-status.md +101 -0
  54. package/dst/man/ase-setup-statusline.md +162 -0
  55. package/dst/man/ase-setup-telemetry.md +166 -0
  56. package/dst/man/ase-setup-uninstall.md +85 -0
  57. package/dst/man/ase-setup-update.md +117 -0
  58. package/dst/man/ase-setup.md +156 -0
  59. package/dst/man/ase-spec-export.md +99 -0
  60. package/dst/man/ase-spec-init.md +57 -0
  61. package/dst/man/ase-spec-lint.md +74 -0
  62. package/dst/man/ase-spec-preview.md +57 -0
  63. package/dst/man/ase-spec.md +85 -0
  64. package/dst/man/ase-statusline.md +330 -0
  65. package/dst/man/ase-task-assign.md +138 -0
  66. package/dst/man/ase-task-board.md +651 -0
  67. package/dst/man/ase-task-delete.md +46 -0
  68. package/dst/man/ase-task-edit.md +73 -0
  69. package/dst/man/ase-task-idscheme.md +92 -0
  70. package/dst/man/ase-task-lifecycle.md +92 -0
  71. package/dst/man/ase-task-list.md +86 -0
  72. package/dst/man/ase-task-load.md +48 -0
  73. package/dst/man/ase-task-newid.md +100 -0
  74. package/dst/man/ase-task-purge.md +49 -0
  75. package/dst/man/ase-task-rename.md +69 -0
  76. package/dst/man/ase-task-request.md +108 -0
  77. package/dst/man/ase-task-save.md +78 -0
  78. package/dst/man/ase-task-status.md +79 -0
  79. package/dst/man/ase-task-store.md +377 -0
  80. package/dst/man/ase-task-view.md +50 -0
  81. package/dst/man/ase-task.md +335 -0
  82. package/dst/man/ase-util-compat.md +38 -0
  83. package/dst/man/ase-util-diagram.md +140 -0
  84. package/dst/man/ase-util-meta.md +68 -0
  85. package/dst/man/ase-util-metric.md +74 -0
  86. package/dst/man/ase-util-mint.md +85 -0
  87. package/dst/man/ase-util-worktree.md +109 -0
  88. package/dst/man/ase-util.md +81 -0
  89. package/dst/man/ase.md +228 -0
  90. package/package.json +16 -21
  91. package/plugin/.claude-plugin/plugin.json +1 -1
  92. package/plugin/.codex-plugin/plugin.json +1 -1
  93. package/plugin/.github/plugin/plugin.json +1 -1
  94. package/plugin/agents/ase-code-lint.md +3 -3
  95. package/plugin/agents/ase-docs-proofread.md +3 -3
  96. package/plugin/agents/ase-docs-refine.md +3 -3
  97. package/plugin/agents/ase-spec-lint.md +338 -0
  98. package/plugin/mods/ase-mods-common.ts +444 -0
  99. package/plugin/mods/ase-mods-grill.ts +312 -0
  100. package/plugin/mods/ase-mods-help-browse.ts +405 -0
  101. package/plugin/mods/ase-mods-help-compose.ts +561 -0
  102. package/plugin/mods/ase-mods-help-find.ts +426 -0
  103. package/plugin/mods/ase-mods-help-hint.ts +134 -0
  104. package/plugin/mods/ase-mods-help-show.ts +142 -0
  105. package/plugin/mods/ase-mods.ts +214 -381
  106. package/plugin/mods/claude-code.d.ts +26 -3
  107. package/plugin/skills/ase-code-lint/SKILL.md +15 -15
  108. package/plugin/skills/ase-code-lint/help.md +21 -8
  109. package/plugin/skills/ase-help-intent/SKILL.md +2 -1
  110. package/plugin/skills/ase-help-intent/data.md +214 -9
  111. package/plugin/skills/ase-help-skill/catalog.md +1 -0
  112. package/plugin/skills/ase-meta-workflow/workflow.txt +2 -0
  113. package/plugin/skills/ase-repo-commit/SKILL.md +1 -0
  114. package/plugin/skills/ase-repo-merge/SKILL.md +1 -0
  115. package/plugin/skills/ase-spec-edit/SKILL.md +3 -3
  116. package/plugin/skills/ase-spec-edit/help.md +1 -1
  117. package/plugin/skills/ase-spec-lint/SKILL.md +695 -0
  118. package/plugin/skills/ase-spec-lint/help.md +184 -0
  119. package/plugin/skills/ase-task-implement/SKILL.md +24 -6
  120. package/plugin/skills/ase-task-implement/help.md +11 -1
  121. package/dst/man/ase-agent-adhoc.1 +0 -515
  122. package/dst/man/ase-agent-condenser.1 +0 -193
  123. package/dst/man/ase-agent-editor.1 +0 -212
  124. package/dst/man/ase-agent-estimator.1 +0 -214
  125. package/dst/man/ase-agent-fleet.1 +0 -185
  126. package/dst/man/ase-agent-implementer.1 +0 -233
  127. package/dst/man/ase-agent-integrator.1 +0 -213
  128. package/dst/man/ase-agent-qualifier.1 +0 -214
  129. package/dst/man/ase-agent-rebooter.1 +0 -193
  130. package/dst/man/ase-agent-relay.1 +0 -128
  131. package/dst/man/ase-agent-researcher.1 +0 -217
  132. package/dst/man/ase-agent.1 +0 -264
  133. package/dst/man/ase-artifact-list.1 +0 -38
  134. package/dst/man/ase-artifact-name.1 +0 -39
  135. package/dst/man/ase-artifact.1 +0 -56
  136. package/dst/man/ase-config-delete.1 +0 -45
  137. package/dst/man/ase-config-edit.1 +0 -48
  138. package/dst/man/ase-config-get.1 +0 -46
  139. package/dst/man/ase-config-init.1 +0 -60
  140. package/dst/man/ase-config-list.1 +0 -42
  141. package/dst/man/ase-config-set.1 +0 -52
  142. package/dst/man/ase-config.1 +0 -231
  143. package/dst/man/ase-hook-permission-request.1 +0 -36
  144. package/dst/man/ase-hook-pre-tool-use.1 +0 -59
  145. package/dst/man/ase-hook-session-end.1 +0 -47
  146. package/dst/man/ase-hook-session-persona.1 +0 -37
  147. package/dst/man/ase-hook-session-start.1 +0 -81
  148. package/dst/man/ase-hook-stop.1 +0 -34
  149. package/dst/man/ase-hook-user-prompt-submit.1 +0 -34
  150. package/dst/man/ase-hook.1 +0 -51
  151. package/dst/man/ase-man.1 +0 -67
  152. package/dst/man/ase-mcp.1 +0 -61
  153. package/dst/man/ase-service-log.1 +0 -43
  154. package/dst/man/ase-service-send.1 +0 -47
  155. package/dst/man/ase-service-start.1 +0 -54
  156. package/dst/man/ase-service-status.1 +0 -46
  157. package/dst/man/ase-service-stop.1 +0 -37
  158. package/dst/man/ase-service.1 +0 -85
  159. package/dst/man/ase-setup-disable.1 +0 -44
  160. package/dst/man/ase-setup-enable.1 +0 -45
  161. package/dst/man/ase-setup-install.1 +0 -73
  162. package/dst/man/ase-setup-mcp.1 +0 -124
  163. package/dst/man/ase-setup-status.1 +0 -60
  164. package/dst/man/ase-setup-statusline.1 +0 -92
  165. package/dst/man/ase-setup-telemetry.1 +0 -92
  166. package/dst/man/ase-setup-uninstall.1 +0 -59
  167. package/dst/man/ase-setup-update.1 +0 -64
  168. package/dst/man/ase-setup.1 +0 -93
  169. package/dst/man/ase-spec-export.1 +0 -62
  170. package/dst/man/ase-spec-lint.1 +0 -40
  171. package/dst/man/ase-spec-preview.1 +0 -40
  172. package/dst/man/ase-spec.1 +0 -46
  173. package/dst/man/ase-statusline.1 +0 -189
  174. package/dst/man/ase-task-assign.1 +0 -71
  175. package/dst/man/ase-task-board.1 +0 -384
  176. package/dst/man/ase-task-delete.1 +0 -35
  177. package/dst/man/ase-task-edit.1 +0 -46
  178. package/dst/man/ase-task-idscheme.1 +0 -65
  179. package/dst/man/ase-task-lifecycle.1 +0 -57
  180. package/dst/man/ase-task-list.1 +0 -47
  181. package/dst/man/ase-task-load.1 +0 -36
  182. package/dst/man/ase-task-newid.1 +0 -61
  183. package/dst/man/ase-task-purge.1 +0 -37
  184. package/dst/man/ase-task-rename.1 +0 -44
  185. package/dst/man/ase-task-request.1 +0 -61
  186. package/dst/man/ase-task-save.1 +0 -48
  187. package/dst/man/ase-task-status.1 +0 -55
  188. package/dst/man/ase-task-store.1 +0 -194
  189. package/dst/man/ase-task-view.1 +0 -40
  190. package/dst/man/ase-task.1 +0 -160
  191. package/dst/man/ase-util-compat.1 +0 -25
  192. package/dst/man/ase-util-diagram.1 +0 -76
  193. package/dst/man/ase-util-meta.1 +0 -45
  194. package/dst/man/ase-util-metric.1 +0 -54
  195. package/dst/man/ase-util-mint.1 +0 -59
  196. package/dst/man/ase-util-worktree.1 +0 -72
  197. package/dst/man/ase-util.1 +0 -50
  198. package/dst/man/ase.1 +0 -124
@@ -0,0 +1,636 @@
1
+ /*
2
+ ** Agentic Software Engineering (ASE)
3
+ ** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
+ ** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
5
+ */
6
+ import React from "react";
7
+ import { render, Box, Text, useApp, useInput, useWindowSize } from "ink";
8
+ import pkg from "../package.json" with { type: "json" };
9
+ import { loadCatalog, searchPages, findPage } from "./ase-man-catalog.js";
10
+ import { renderPage, markMatches, codeLoad } from "./ase-man-render.js";
11
+ import { useMouseReports } from "./ase-task-board-tui.js";
12
+ import { mouseReporting } from "./ase-task-board-tui-model.js";
13
+ import { h } from "./ase-task-board-tui-view.js";
14
+ import { palette, loadPalette, cx } from "./ase-task-board-tui-style.js";
15
+ import { sanitize } from "./ase-task-board-tui-popup.js";
16
+ /* layout constants: the screen rows above the content (header, rule, top of
17
+ the panel, the three rows of the search field, and a blank line) and below
18
+ it (bottom of the panel, two key hint lines, rule, and status line), the
19
+ screen columns left of the content (margin, side, and padding of the panel),
20
+ the left and right padding of the rendered page within the detail, the
21
+ minimum width, the maximum count, and the gap of the catalog columns, and
22
+ the minimum window size */
23
+ const TOP = 7;
24
+ const BOTTOM = 5;
25
+ const LEFT = 3;
26
+ const PAD = 2;
27
+ const COL_MIN = 24;
28
+ const COL_MAX = 5;
29
+ const GAP = 2;
30
+ const MIN_W = 60;
31
+ const MIN_H = 20;
32
+ /* the text properties of the tones of the rendered manual pages */
33
+ const toneProps = (tone) => ({
34
+ text: { color: palette.normal },
35
+ bold: { color: palette.normal, bold: true },
36
+ italic: { color: palette.normal, italic: true },
37
+ code: { color: palette.accent },
38
+ link: { color: palette.accent, underline: true },
39
+ title: { color: palette.accent, bold: true },
40
+ head: { color: palette.accent, bold: true },
41
+ sub: { color: palette.normal, bold: true },
42
+ dim: { color: palette.dim },
43
+ bullet: { color: palette.accent },
44
+ cmt: { color: palette.dim, italic: true },
45
+ str: { color: palette.accent },
46
+ num: { color: palette.signal },
47
+ opt: { color: palette.signal },
48
+ var: { color: palette.signal },
49
+ key: { color: palette.accent, bold: true },
50
+ cmd: { color: palette.normal, bold: true },
51
+ kw: { color: palette.normal, bold: true },
52
+ punct: { color: palette.dim }
53
+ }[tone]);
54
+ /* reduce groups to the matching pages of a search (if any), dropping the empty groups */
55
+ const filterGroups = (groups, hits) => hits === null ? groups : groups
56
+ .map((group) => ({ ...group, pages: group.pages.filter((page) => hits.has(page.id)) }))
57
+ .filter((group) => group.pages.length > 0);
58
+ /* lay out the catalog grid onto a width (with 1-5 columns, like the
59
+ catalog of the website), reduced to the matching pages of a search (if any),
60
+ where the groups of each section are distributed in their order onto the
61
+ columns, each filled up to the balanced height */
62
+ const layoutGrid = (catalog, hits, width) => {
63
+ const n = Math.max(1, Math.min(COL_MAX, Math.floor((width + GAP) / (COL_MIN + GAP))));
64
+ const colW = Math.floor((width - (n - 1) * GAP) / n);
65
+ const lines = [];
66
+ const empty = () => new Array(n).fill(null);
67
+ const section = (title, groups) => {
68
+ if (groups.length === 0)
69
+ return;
70
+ if (lines.length > 0)
71
+ lines.push(empty());
72
+ lines.push([{ kind: "section", text: title }, ...empty().slice(1)], empty());
73
+ const total = groups.reduce((sum, group) => sum + group.pages.length + 2, -1);
74
+ const target = Math.ceil(total / n);
75
+ const cols = Array.from({ length: n }, () => []);
76
+ let c = 0;
77
+ for (const group of groups) {
78
+ if (cols[c].length > 0 && cols[c].length + group.pages.length + 2 > target && c < n - 1)
79
+ c++;
80
+ if (cols[c].length > 0)
81
+ cols[c].push(null);
82
+ cols[c].push({ kind: "group", text: group.label }, ...group.pages.map((page) => ({ kind: "page", page })));
83
+ }
84
+ const height = Math.max(...cols.map((col) => col.length));
85
+ for (let l = 0; l < height; l++)
86
+ lines.push(cols.map((col) => col[l] ?? null));
87
+ };
88
+ section("AI SKILLS", filterGroups(catalog.skills, hits));
89
+ section("CLI COMMANDS", filterGroups(catalog.commands, hits));
90
+ const pos = new Map();
91
+ lines.forEach((cells, l) => {
92
+ cells.forEach((cell, c) => {
93
+ if (cell !== null && cell.kind === "page")
94
+ pos.set(cell.page.id, { c, l });
95
+ });
96
+ });
97
+ return { n, colW, lines, pos };
98
+ };
99
+ /* move the selection within the catalog grid: vertically onto the nearest page
100
+ of the same column (or, at the end of the column, onto the nearest page further
101
+ in this direction of any other column, so the entire grid stays reachable),
102
+ resp. horizontally onto the nearest page of the neighbor column */
103
+ const moveGrid = (grid, id, dc, dl) => {
104
+ const cur = grid.pos.get(id);
105
+ if (cur === undefined)
106
+ return id;
107
+ let best = id;
108
+ let dist = Infinity;
109
+ for (const [other, p] of grid.pos) {
110
+ let d = Infinity;
111
+ if (dl !== 0 && p.c === cur.c && Math.sign(p.l - cur.l) === dl)
112
+ d = Math.abs(p.l - cur.l);
113
+ else if (dc !== 0 && p.c === cur.c + dc)
114
+ d = Math.abs(p.l - cur.l) * 2 + (p.l < cur.l ? 1 : 0);
115
+ if (d < dist) {
116
+ best = other;
117
+ dist = d;
118
+ }
119
+ }
120
+ if (dist === Infinity && dl !== 0) {
121
+ for (const [other, p] of grid.pos) {
122
+ const d = Math.sign(p.l - cur.l) === dl ? Math.abs(p.l - cur.l) * grid.n + Math.abs(p.c - cur.c) : Infinity;
123
+ if (d < dist) {
124
+ best = other;
125
+ dist = d;
126
+ }
127
+ }
128
+ }
129
+ return best;
130
+ };
131
+ /* the entries of the master: the sections and groups with their pages,
132
+ reduced to the matching pages of a search (if any) */
133
+ const masterItems = (catalog, hits) => {
134
+ const items = [];
135
+ for (const [title, groups] of [["AI SKILLS", catalog.skills], ["CLI COMMANDS", catalog.commands]]) {
136
+ let head = false;
137
+ for (const group of filterGroups(groups, hits)) {
138
+ if (items.length > 0)
139
+ items.push(null);
140
+ if (!head)
141
+ items.push({ kind: "section", text: title }, null);
142
+ head = true;
143
+ items.push({ kind: "group", text: group.label }, ...group.pages.map((page) => ({ kind: "page", page })));
144
+ }
145
+ }
146
+ return items;
147
+ };
148
+ /* keep an index visible within a viewport of a height, by minimally moving its
149
+ offset, where scrolling up reveals the entries from a top index on */
150
+ const follow = (off, top, idx, height, total) => Math.max(0, Math.min(Math.max(0, total - height), top < off ? top : idx >= off + height ? idx - height + 1 : off));
151
+ /* the index of the topmost entry to reveal together with an entry of a list:
152
+ the heading of its group (and the heading of its section, if the group is
153
+ the first one of the section), as long as they fit into a viewport of a height */
154
+ const headOf = (list, idx, height) => {
155
+ let k = idx;
156
+ while (k > 0 && list[k - 1] !== null)
157
+ k--;
158
+ if (k >= 2 && list[k - 2]?.kind === "section")
159
+ k -= 2;
160
+ return idx - k < height ? k : idx;
161
+ };
162
+ /* render a cell onto a width: the label of a page with its last part
163
+ accented in bold (or, in the same style, inversely in signal color over the
164
+ entire width if selected), resp. a heading */
165
+ const renderCell = (cell, width, selected) => {
166
+ if (cell === null)
167
+ return h(Text, {}, " ");
168
+ if (cell.kind !== "page")
169
+ return h(Text, { ...(cell.kind === "group" ? cx("accent", "bold") : cx("bold")), wrap: "truncate" }, cell.text);
170
+ const label = cell.page.label.length > width ? cell.page.label.slice(0, width - 1) + "…" : cell.page.label;
171
+ const idx = cell.page.kind === "skill" ? label.lastIndexOf("-") : label.lastIndexOf(" ");
172
+ const base = selected ? { color: palette.signal, inverse: true } : cx("normal");
173
+ const last = selected ? { color: palette.signal, bold: true, inverse: true } : cx("accent", "bold");
174
+ return h(Text, { wrap: "truncate" }, h(Text, base, label.slice(0, idx + 1)), h(Text, last, label.slice(idx + 1)), ...(selected ? [h(Text, base, " ".repeat(Math.max(0, width - label.length)))] : []));
175
+ };
176
+ /* render a vertical scroll bar (like the one of the read dialog of the task board)
177
+ over the rows of a viewport of a height (at an offset into a total count of
178
+ lines) below some leading blank rows, shown only if the lines do not fit */
179
+ const scrollBar = (lead, height, total, off) => {
180
+ const max = Math.max(0, total - height);
181
+ const thumbH = Math.max(1, Math.round(height * Math.min(1, height / Math.max(1, total))));
182
+ const thumbY = max === 0 ? 0 : Math.round((height - thumbH) * Math.min(off, max) / max);
183
+ return h(Box, { flexDirection: "column", width: 1 }, ...Array.from({ length: lead + height }, (_, i) => h(Text, { key: `bar-${i}`, ...cx("signal") }, i < lead || total <= height ? " " : i - lead >= thumbY && i - lead < thumbY + thumbH ? "█" : "░")));
184
+ };
185
+ /* render a line of a page, with its search matches and its selected reference highlighted */
186
+ const renderLine = (line, refSel) => h(Text, { wrap: "truncate" }, line.length === 0 ? " " : line.map((span, i) => h(Text, {
187
+ key: i,
188
+ ...toneProps(span.tone),
189
+ ...(span.ref !== undefined ? { underline: true } : {}),
190
+ ...(span.match === true ? { color: palette.signal, inverse: true } : {}),
191
+ ...(span.ref !== undefined && span.ref === refSel ? { color: palette.signal, inverse: true } : {})
192
+ }, sanitize(span.text))));
193
+ /* the root component of the manual page browser, starting on the start page,
194
+ resp. on the browser page with an initially selected page or search */
195
+ const App = ({ catalog, initial }) => {
196
+ const { exit } = useApp();
197
+ const { columns, rows } = useWindowSize();
198
+ const first = initial?.id ?? [...catalog.pages.keys()][0] ?? "";
199
+ /* the state: the shown page (the start page with the catalog, or the
200
+ browser page with master and detail), the search query (with its
201
+ cursor position, and whether it is being typed), the selected page of
202
+ the catalog and of the master, the scroll offset of the detail, the
203
+ selected reference of the detail, and whether the syntax highlighter of
204
+ the code blocks is loaded */
205
+ const [page, setPage] = React.useState(initial !== null ? "browse" : "start");
206
+ const [query, setQuery] = React.useState(initial?.query ?? "");
207
+ const [cursor, setCursor] = React.useState(initial?.query.length ?? 0);
208
+ const [typing, setTyping] = React.useState(false);
209
+ const [startSel, setStartSel] = React.useState(first);
210
+ const [sel, setSel] = React.useState(first);
211
+ const [scroll, setScroll] = React.useState(0);
212
+ const [ref, setRef] = React.useState(-1);
213
+ const [coded, setCoded] = React.useState(false);
214
+ const gridOff = React.useRef(0);
215
+ const masterOff = React.useRef(0);
216
+ /* the cursor position within the search query, clamped as the query
217
+ can be cleared without moving the cursor */
218
+ const cur = Math.min(cursor, query.length);
219
+ /* load the syntax highlighter of the code blocks in the background */
220
+ React.useEffect(() => {
221
+ codeLoad().then(() => {
222
+ setCoded(true);
223
+ }).catch(() => {
224
+ /* keep the code blocks without syntax highlighting */
225
+ });
226
+ }, []);
227
+ /* report mouse presses while the browser runs */
228
+ React.useEffect(() => {
229
+ mouseReporting(true);
230
+ return () => {
231
+ mouseReporting(false);
232
+ };
233
+ }, []);
234
+ /* the geometry: the content width and height (without and with the
235
+ search field and its blank line within the panel), the catalog grid,
236
+ the master width (of its longest label plus its padding, capped at a
237
+ third of the content width), the detail width (besides the adjacent
238
+ borders and paddings of the master and detail panes, where the scroll bar
239
+ of the detail takes the right padding column of its pane), and the
240
+ search field width (on the start page, resp. of the master) */
241
+ const W = columns - 2 * LEFT;
242
+ const H = Math.max(1, rows - TOP - BOTTOM);
243
+ const HC = H + 4;
244
+ const MW = React.useMemo(() => Math.min(Math.floor(W / 3), Math.max(16, ...[...catalog.pages.values()].map((p) => p.label.length + 1)) + 2 * PAD), [catalog, W]);
245
+ const DW = Math.max(10, W - MW - 4);
246
+ const FW = page === "start" ? Math.min(44, W) : MW;
247
+ /* the search hits (live while typing), the catalog grid and the master
248
+ entries (both reduced to the matching pages), and the selected page of the
249
+ catalog and of the master, which fall back to the first matching page if
250
+ the selected one does not match the search */
251
+ const hits = React.useMemo(() => searchPages(catalog, query), [catalog, query]);
252
+ const grid = React.useMemo(() => layoutGrid(catalog, hits, W - 2 * PAD), [catalog, hits, W]);
253
+ const items = React.useMemo(() => masterItems(catalog, hits), [catalog, hits]);
254
+ const pageIds = items.flatMap((item) => item !== null && item.kind === "page" ? [item.page.id] : []);
255
+ const startId = hits === null || hits.has(startSel) ? startSel : pageIds[0] ?? startSel;
256
+ const selId = hits === null || hits.has(sel) ? sel : pageIds[0] ?? sel;
257
+ const current = hits === null || hits.has(selId) ? catalog.pages.get(selId) : undefined;
258
+ React.useEffect(() => {
259
+ if (startId !== startSel)
260
+ setStartSel(startId);
261
+ if (selId !== sel)
262
+ setSel(selId);
263
+ }, [startId, startSel, selId, sel]);
264
+ /* the rendered selected page (wrapped within the left and right padding of the detail),
265
+ whose detail starts at its top without any selected reference whenever
266
+ another page is selected */
267
+ const rendered = React.useMemo(() => current !== undefined ?
268
+ renderPage(current.md, DW - 2 * PAD, current.id, (id) => catalog.pages.has(id)) : { lines: [], refs: [] }, [catalog, current, DW, coded]);
269
+ React.useEffect(() => {
270
+ setScroll(0);
271
+ setRef(-1);
272
+ }, [selId]);
273
+ const maxScroll = Math.max(0, rendered.lines.length - H);
274
+ const top = Math.min(scroll, maxScroll);
275
+ /* open a page on the browser page, leaving a search not matching it */
276
+ const open = (id) => {
277
+ if (hits !== null && !hits.has(id))
278
+ setQuery("");
279
+ setTyping(false);
280
+ setSel(id);
281
+ setPage("browse");
282
+ };
283
+ /* step the selection of the master, scroll the detail, and select the
284
+ next/previous reference of the detail, starting at the shown part of
285
+ the detail (and scrolling to the reference if it is not shown) */
286
+ const selectStep = (step) => {
287
+ if (pageIds.length > 0)
288
+ setSel(pageIds[Math.max(0, Math.min(pageIds.length - 1, pageIds.indexOf(selId) + step))]);
289
+ };
290
+ const scrollBy = (delta) => {
291
+ setScroll(Math.max(0, Math.min(maxScroll, top + delta)));
292
+ };
293
+ const cycleRef = (step) => {
294
+ const refs = rendered.refs.map((r, i) => ({ ...r, i })).filter((r) => r.line >= 0);
295
+ if (refs.length === 0)
296
+ return;
297
+ const shown = (r) => r.line >= top && r.line < top + H;
298
+ const k = refs.findIndex((r) => r.i === ref);
299
+ const next = k >= 0 && shown(refs[k]) ? refs[(k + step + refs.length) % refs.length] :
300
+ step > 0 ? refs.find((r) => r.line >= top) ?? refs[0] : refs.findLast((r) => r.line < top + H) ?? refs[refs.length - 1];
301
+ setRef(next.i);
302
+ if (!shown(next))
303
+ setScroll(Math.max(0, Math.min(maxScroll, next.line - Math.floor(H / 3))));
304
+ };
305
+ /* edit the search query by a typed key at the cursor position (like the input
306
+ boxes of the task board): the left/right arrows move the cursor by one
307
+ character, HOME/END move it to the start/end, BACKSPACE removes the character
308
+ before the cursor, DELETE removes the character at the cursor, and all other
309
+ input is inserted at the cursor */
310
+ const edit = (input, key) => {
311
+ if (key.leftArrow || key.rightArrow) {
312
+ setCursor(Math.max(0, Math.min(query.length, cur + (key.leftArrow ? -1 : 1))));
313
+ return true;
314
+ }
315
+ if (key.home || key.end) {
316
+ setCursor(key.home ? 0 : query.length);
317
+ return true;
318
+ }
319
+ if (key.backspace) {
320
+ if (cur > 0) {
321
+ setQuery(query.slice(0, cur - 1) + query.slice(cur));
322
+ setCursor(cur - 1);
323
+ }
324
+ return true;
325
+ }
326
+ if (key.delete) {
327
+ if (cur < query.length)
328
+ setQuery(query.slice(0, cur) + query.slice(cur + 1));
329
+ return true;
330
+ }
331
+ if (input !== "" && !key.ctrl && !key.meta && !key.tab && !key.return && !key.escape) {
332
+ const text = sanitize(input.replace(/[\r\n]+/g, " "));
333
+ setQuery(query.slice(0, cur) + text + query.slice(cur));
334
+ setCursor(cur + text.length);
335
+ return true;
336
+ }
337
+ return false;
338
+ };
339
+ /* handle the mouse: a click onto the search field starts typing into it
340
+ (clearing the search on its clear symbol), a click onto a page of the
341
+ catalog or the master opens it, a click onto a reference of the detail
342
+ follows it, and the wheel (64/65: up/down) steps the selection of the
343
+ catalog and the master, resp. scrolls the detail */
344
+ useMouseReports((btn, mx, my) => {
345
+ const wheel = btn === 64 ? -1 : btn === 65 ? 1 : 0;
346
+ if (btn !== 0 && wheel === 0)
347
+ return;
348
+ if (btn === 0) {
349
+ const inField = my >= TOP - 4 && my < TOP - 1 && mx < LEFT + FW;
350
+ setTyping(inField);
351
+ if (inField && my === TOP - 3 && mx >= LEFT + FW - 4 && mx <= LEFT + FW - 2)
352
+ setQuery("");
353
+ if (inField) {
354
+ setRef(-1);
355
+ return;
356
+ }
357
+ }
358
+ if (page === "start") {
359
+ if (wheel !== 0) {
360
+ setStartSel(moveGrid(grid, startId, 0, wheel));
361
+ return;
362
+ }
363
+ const gx = mx - LEFT - PAD;
364
+ const c = Math.floor(gx / (grid.colW + GAP));
365
+ const cell = my >= TOP && my < TOP + H && gx >= 0 && c < grid.n && gx - c * (grid.colW + GAP) < grid.colW ?
366
+ grid.lines[gridOff.current + my - TOP]?.[c] : undefined;
367
+ if (cell !== undefined && cell !== null && cell.kind === "page") {
368
+ setStartSel(cell.page.id);
369
+ setSel(cell.page.id);
370
+ setPage("browse");
371
+ }
372
+ return;
373
+ }
374
+ const inMaster = mx <= LEFT + MW + 1;
375
+ if (wheel !== 0) {
376
+ if (inMaster)
377
+ selectStep(wheel);
378
+ else
379
+ scrollBy(3 * wheel);
380
+ return;
381
+ }
382
+ if (my < TOP || my >= TOP + H)
383
+ return;
384
+ if (inMaster) {
385
+ const item = items[masterOff.current + my - TOP];
386
+ if (item !== undefined && item !== null && item.kind === "page") {
387
+ setSel(item.page.id);
388
+ setRef(-1);
389
+ }
390
+ return;
391
+ }
392
+ const line = rendered.lines[top + my - TOP];
393
+ let x = LEFT + MW + 4 + PAD;
394
+ for (const span of line ?? []) {
395
+ if (mx >= x && mx < x + span.text.length) {
396
+ if (span.ref !== undefined)
397
+ open(rendered.refs[span.ref].id);
398
+ break;
399
+ }
400
+ x += span.text.length;
401
+ }
402
+ });
403
+ /* handle the keyboard */
404
+ useInput((input, key) => {
405
+ /* ignore key releases and mouse reports (handled by "useMouseReports" above) */
406
+ if (key.eventType === "release" || /^\[<\d+;\d+;\d+[Mm]$/.test(input))
407
+ return;
408
+ /* on the start page, typing into the search field (filtering the catalog
409
+ live) and submitting the search (opening the browser page with the
410
+ selected, i.e. initially the first, matching page) or leaving the field,
411
+ clearing the search, and selecting and opening a page of the catalog */
412
+ if (page === "start") {
413
+ if (typing) {
414
+ if (key.escape)
415
+ setTyping(false);
416
+ else if (key.return) {
417
+ setTyping(false);
418
+ if (hits !== null) {
419
+ if (grid.pos.size > 0)
420
+ setSel(startId);
421
+ setPage("browse");
422
+ }
423
+ }
424
+ if (key.escape || key.return || edit(input, key))
425
+ return;
426
+ }
427
+ else if (input === "/") {
428
+ setTyping(true);
429
+ return;
430
+ }
431
+ else if (input === "q") {
432
+ exit();
433
+ return;
434
+ }
435
+ else if (key.escape) {
436
+ setQuery("");
437
+ return;
438
+ }
439
+ else if (key.return) {
440
+ if (grid.pos.size > 0) {
441
+ setSel(startId);
442
+ setPage("browse");
443
+ }
444
+ return;
445
+ }
446
+ if (key.upArrow || key.downArrow)
447
+ setStartSel(moveGrid(grid, startId, 0, key.upArrow ? -1 : 1));
448
+ else if (key.leftArrow || key.rightArrow)
449
+ setStartSel(moveGrid(grid, startId, key.leftArrow ? -1 : 1, 0));
450
+ else if (key.home || key.end) {
451
+ const ids = [...grid.pos.keys()];
452
+ setStartSel(ids[key.home ? 0 : ids.length - 1] ?? startId);
453
+ }
454
+ return;
455
+ }
456
+ /* on the browser page, ESCAPE first clears the search query, then leaves
457
+ the search field, and finally returns to the start page */
458
+ if (key.escape) {
459
+ if (query !== "")
460
+ setQuery("");
461
+ else if (typing)
462
+ setTyping(false);
463
+ else {
464
+ setStartSel(selId);
465
+ setPage("start");
466
+ }
467
+ return;
468
+ }
469
+ /* typing into the search field (filtering the master live), resp.
470
+ starting it, quitting, selecting and following a reference, and
471
+ entering resp. leaving the references of the detail */
472
+ if (typing) {
473
+ if (key.return) {
474
+ setTyping(false);
475
+ return;
476
+ }
477
+ if (edit(input, key))
478
+ return;
479
+ }
480
+ else if (input === "/") {
481
+ setTyping(true);
482
+ setRef(-1);
483
+ return;
484
+ }
485
+ else if (input === "q") {
486
+ exit();
487
+ return;
488
+ }
489
+ else if (key.tab) {
490
+ cycleRef(key.shift ? -1 : 1);
491
+ return;
492
+ }
493
+ else if (key.rightArrow) {
494
+ if (ref < 0)
495
+ cycleRef(1);
496
+ return;
497
+ }
498
+ else if (key.leftArrow) {
499
+ setRef(-1);
500
+ return;
501
+ }
502
+ else if (key.return) {
503
+ const target = rendered.refs[ref];
504
+ if (target !== undefined)
505
+ open(target.id);
506
+ return;
507
+ }
508
+ else if (input === " ") {
509
+ scrollBy(H - 1);
510
+ return;
511
+ }
512
+ /* selecting in the master (resp. the references of the detail, while
513
+ one is selected) and scrolling the detail */
514
+ if ((key.upArrow || key.downArrow) && ref >= 0)
515
+ cycleRef(key.upArrow ? -1 : 1);
516
+ else if (key.upArrow || key.downArrow)
517
+ selectStep(key.upArrow ? -1 : 1);
518
+ else if (key.pageUp || key.pageDown)
519
+ scrollBy((key.pageUp ? -1 : 1) * (H - 1));
520
+ else if (key.home || key.end)
521
+ setScroll(key.home ? 0 : maxScroll);
522
+ });
523
+ /* refuse to draw into a too small window */
524
+ if (columns < MIN_W || rows < MIN_H)
525
+ return h(Box, { width: columns, height: rows, justifyContent: "center", alignItems: "center" }, h(Text, cx("signal"), `window too small (${columns}×${rows}) — needs at least ${MIN_W}×${MIN_H}`));
526
+ /* the header, enclosed on its left and right side by the upward lines of the rule below it,
527
+ with the page counts (as matching/total counts during a search, in signal color without any match) */
528
+ const count = (groups) => {
529
+ const pages = groups.flatMap((group) => group.pages);
530
+ return hits === null ? String(pages.length) : `${pages.filter((p) => hits.has(p.id)).length}/${pages.length}`;
531
+ };
532
+ const counts = cx("bold", hits !== null && hits.size === 0 && "signal");
533
+ const header = h(Box, { paddingX: 1 }, h(Box, cx("fixed"), h(Text, cx("dim"), "│")), h(Box, cx("grow", "bar"), h(Text, cx("normal", "truncate"), "⧉ ASE: ", h(Text, cx("bold"), "Manpage Browser"), " · skills: ", h(Text, counts, count(catalog.skills)), " · commands: ", h(Text, counts, count(catalog.commands)))), h(Box, cx("fixed"), h(Text, cx("dim"), "│")));
534
+ /* the search field in the frame of an input box (like the input boxes of the
535
+ task board, in accent color, resp. in signal color while typing): the part
536
+ of the query around the cursor (with an inverse cursor cell at the cursor,
537
+ resp. behind its end) while typing, else its tail, or a placeholder,
538
+ followed by a clickable clear symbol for a non-empty query */
539
+ const fieldW = FW;
540
+ const textW = Math.max(1, fieldW - 8);
541
+ const tone = typing ? "signal" : "accent";
542
+ const shown = sanitize(query);
543
+ const hint = "search manpages… (/)".slice(0, textW);
544
+ const cells = shown.length + (typing && cur >= shown.length ? 1 : 0);
545
+ const fieldX = Math.max(0, typing ? cur + 1 - textW : shown.length - textW);
546
+ const used = typing || query !== "" ? Math.min(cells, fieldX + textW) - fieldX : hint.length;
547
+ const text = typing ? [
548
+ h(Text, { key: "pre", ...cx(tone, "bold") }, shown.slice(fieldX, cur)),
549
+ h(Text, { key: "cur", ...cx(tone, "bold", "inverse") }, shown[cur] ?? " "),
550
+ h(Text, { key: "post", ...cx(tone, "bold") }, shown.slice(cur + 1, fieldX + textW))
551
+ ] : query !== "" ?
552
+ [h(Text, { key: "text", ...cx(tone, "bold") }, shown.slice(fieldX))] :
553
+ [h(Text, { key: "hint", ...cx("dim") }, hint)];
554
+ const field = h(Box, { flexDirection: "column", width: fieldW }, h(Text, cx(tone), `╭${"─".repeat(fieldW - 2)}╮`), h(Text, {}, h(Text, cx(tone), "│ "), h(Text, cx(typing ? "signal" : "dim"), "⌕ "), ...text, " ".repeat(Math.max(0, textW - used)), h(Text, cx(tone, "bold"), query !== "" ? " ✕" : " "), h(Text, cx(tone), " │")), h(Text, cx(tone), `╰${"─".repeat(fieldW - 2)}╯`));
555
+ /* the content of the start page: the search field, a blank line, and the
556
+ catalog grid (reduced to the matching pages of a search, and laid out
557
+ within a left and right padding, also towards its scroll bar) */
558
+ let content;
559
+ if (page === "start") {
560
+ /* the column of the selected page (with the section headings of the first column) */
561
+ const at = grid.pos.get(startId) ?? { c: 0, l: 0 };
562
+ const col = grid.lines.map((cells) => cells[at.c] ?? (cells[0]?.kind === "section" ? cells[0] : null));
563
+ gridOff.current = follow(gridOff.current, headOf(col, at.l, H), at.l, H, grid.lines.length);
564
+ content = h(Box, { height: HC }, h(Box, { flexDirection: "column", width: W }, field, h(Text, {}, " "), ...(grid.lines.length === 0 ? [h(Text, { key: "none", ...cx("dim") }, " ".repeat(PAD) + "(no matching manpages)")] :
565
+ grid.lines.slice(gridOff.current, gridOff.current + H).map((cells, l) => h(Box, { key: `line-${gridOff.current + l}`, paddingX: PAD }, ...cells.map((cell, c) => h(Box, { key: `cell-${c}`, width: grid.colW, marginRight: c < grid.n - 1 ? GAP : 0 }, renderCell(cell, grid.colW, cell !== null && cell.kind === "page" && cell.page.id === startId))))))), scrollBar(HC - H, H, grid.lines.length, gridOff.current));
566
+ }
567
+ /* the content of the browser page, in two adjacent panes (with the border
568
+ of the active one in signal color, i.e. of the detail while one of its
569
+ references is selected, else of the master): the master with the search
570
+ field, a blank line, and the entries, and the detail with the information
571
+ line in the frame of a box (aligned with the search field), a blank line,
572
+ and the rendered selected page (aligned with the entries) */
573
+ else {
574
+ const selIdx = Math.max(0, items.findIndex((item) => item !== null && item.kind === "page" && item.page.id === selId));
575
+ masterOff.current = follow(masterOff.current, headOf(items, selIdx, H), selIdx, H, items.length);
576
+ const re = hits?.get(selId);
577
+ const master = h(Box, { flexDirection: "column", width: MW }, field, h(Text, {}, " "), ...items.slice(masterOff.current, masterOff.current + H).map((item, i) => h(Box, { key: `item-${masterOff.current + i}`, width: MW, paddingX: PAD }, renderCell(item, MW - 2 * PAD, item !== null && item.kind === "page" && item.page.id === selId))));
578
+ const where = rendered.lines.length <= H ? "all" :
579
+ `${top + 1}-${Math.min(rendered.lines.length, top + H)}/${rendered.lines.length}`;
580
+ const bar = scrollBar(HC - H, H, rendered.lines.length, top);
581
+ const detail = h(Box, { flexDirection: "column", width: DW }, h(Text, cx("dim"), `╭${"─".repeat(DW - 2)}╮`), h(Box, { width: DW }, h(Box, cx("fixed"), h(Text, cx("dim"), "│ ")), h(Box, { flexGrow: 1, flexShrink: 1, minWidth: 0 }, current === undefined ? h(Text, cx("dim"), " ") :
582
+ h(Text, cx("normal", "truncate"), `${current.kind}: `, h(Text, cx("bold"), current.label), ` — ${sanitize(current.title)}`)), h(Box, cx("fixed"), h(Text, cx("dim"), ` lines: ${where} │`))), h(Text, cx("dim"), `╰${"─".repeat(DW - 2)}╯`), h(Text, {}, " "), ...(current === undefined ? [h(Text, { key: "none", ...cx("dim") }, " ".repeat(PAD) + "(no matching manpages)")] :
583
+ rendered.lines.slice(top, top + H).map((line, i) => h(Box, { key: `line-${top + i}`, width: DW, paddingX: PAD }, renderLine(re !== undefined ? markMatches(line, re) : line, ref)))));
584
+ const pane = (active) => ({ borderStyle: "round", borderColor: active ? palette.signal : palette.dim, height: HC + 2, paddingLeft: LEFT - 2 });
585
+ content = h(Box, { marginX: 1 }, h(Box, { ...pane(ref < 0), paddingRight: 1 }, master), h(Box, pane(ref >= 0), detail, bar));
586
+ }
587
+ /* the key hints and the status line in the last line of the screen, enclosed
588
+ on its left and right side by the downward lines of the rule above it */
589
+ const hints = page === "start" ? [
590
+ "↑/↓/←/→/Wheel: select manpage · ⏎/Left-Click: open manpage · ⇤/⇥: first/last manpage",
591
+ typing ? "⏎: search manpages · ESC: leave search field · ←/→/⇤/⇥: move cursor · ⌫/DEL: delete character" :
592
+ `/: search manpages${query !== "" ? " · ESC: clear search" : ""} · q: quit`
593
+ ] : [
594
+ ref >= 0 ?
595
+ "↑/↓/TAB/⇧TAB: select previous/next reference · ←: select side-bar · ⇈/⇊/SPACE/⇤/⇥/Wheel: scroll manpage" :
596
+ "↑/↓/Wheel: select manpage · →/TAB/⇧TAB: select first reference · ⇈/⇊/SPACE/⇤/⇥/Wheel: scroll manpage",
597
+ typing ? "ESC: clear search, leave search field · ⏎: leave search field · ←/→/⇤/⇥: move cursor · ⌫/DEL: delete character" :
598
+ `⏎/Left-Click: follow reference · /: search manpages · ESC: ${query !== "" ? "clear search" : "back to catalog"} · q: quit`
599
+ ];
600
+ const report = typing ? "searching all manpages for all keywords (with fuzzy matching as fallback)" : null;
601
+ /* the screen: the header, the content (of the start page in a panel, always
602
+ active and hence with its border in signal color, aligned with the rules
603
+ of the header and the status line, and without a right padding, as the
604
+ content covers it with its scroll bar), the key hints, and the status line */
605
+ const panel = { borderStyle: "round", borderColor: palette.signal, marginX: 1, paddingLeft: LEFT - 2 };
606
+ return h(Box, { width: columns, height: rows, flexDirection: "column" }, header, h(Text, cx("dim"), " ╰" + "─".repeat(Math.max(0, columns - 4)) + "╯ "), page === "start" ? h(Box, { flexDirection: "column", ...panel }, content) : content, ...hints.map((hint, i) => h(Box, { key: `keys-${i}`, ...cx("bar") }, h(Text, cx("hint"), hint))), h(Text, cx("dim"), " ╭" + "─".repeat(Math.max(0, columns - 4)) + "╮ "), h(Box, { paddingX: 1 }, h(Box, cx("fixed"), h(Text, cx("dim"), "│")), h(Box, cx("grow", "bar"), h(Text, cx("hint", report !== null && "signal"), report ?? ["⧉ ASE: ", h(Text, { key: "app", ...cx("bold") }, "Manpage Browser"),
607
+ " · version: ", h(Text, { key: "version", ...cx("bold") }, `ASE ${pkg.version}`)])), h(Box, cx("fixed"), h(Text, cx("dim"), "│"))));
608
+ };
609
+ /* run the manual page browser until the user quits, where the words of a
610
+ requested page select the page fitting exactly onto them, else are searched */
611
+ export const runManTUI = async (log, words = []) => {
612
+ const catalog = loadCatalog();
613
+ const query = words.join(" ").trim();
614
+ const found = query !== "" ? findPage(catalog, words) : undefined;
615
+ const initial = query === "" ? null : { id: found?.id ?? null, query: found !== undefined ? "" : sanitize(query) };
616
+ loadPalette(log);
617
+ /* defer stderr log output while Ink draws, as it bypasses patchConsole */
618
+ log.defer(true);
619
+ /* ensure the terminal never stays in mouse reporting mode,
620
+ even on a premature process exit (e.g. an uncaught exception) */
621
+ const reset = () => {
622
+ mouseReporting(false);
623
+ };
624
+ process.once("exit", reset);
625
+ try {
626
+ const app = render(h(App, { catalog, initial }), {
627
+ alternateScreen: true, exitOnCtrlC: true, patchConsole: true
628
+ });
629
+ await app.waitUntilExit();
630
+ }
631
+ finally {
632
+ process.off("exit", reset);
633
+ mouseReporting(false);
634
+ log.defer(false);
635
+ }
636
+ };