revspec 0.2.2 → 0.4.0
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/CLAUDE.md +10 -2
- package/README.md +86 -29
- package/bin/revspec.ts +2 -1
- package/docs/superpowers/plans/2026-03-15-ui-refactor.md +1025 -0
- package/package.json +1 -1
- package/scripts/install-skill.sh +20 -0
- package/scripts/release.sh +5 -6
- package/skills/revspec/SKILL.md +137 -0
- package/src/protocol/live-events.ts +3 -2
- package/src/tui/app.ts +198 -310
- package/src/tui/comment-input.ts +145 -144
- package/src/tui/confirm.ts +29 -43
- package/src/tui/help.ts +33 -57
- package/src/tui/pager.ts +162 -82
- package/src/tui/search.ts +6 -6
- package/src/tui/status-bar.ts +77 -34
- package/src/tui/thread-list.ts +28 -54
- package/src/tui/ui/dialog.ts +106 -0
- package/src/tui/ui/hint-bar.ts +20 -0
- package/src/tui/ui/keybinds.ts +104 -0
- package/src/tui/ui/markdown.ts +251 -0
- package/src/tui/ui/theme.ts +49 -0
- package/test/tui/ui/keybinds.test.ts +71 -0
- package/src/tui/theme.ts +0 -34
package/src/tui/comment-input.ts
CHANGED
|
@@ -2,12 +2,12 @@ import {
|
|
|
2
2
|
BoxRenderable,
|
|
3
3
|
TextRenderable,
|
|
4
4
|
TextareaRenderable,
|
|
5
|
-
ScrollBoxRenderable,
|
|
6
5
|
type CliRenderer,
|
|
7
6
|
type KeyEvent,
|
|
8
7
|
} from "@opentui/core";
|
|
9
8
|
import type { Thread, Message } from "../protocol/types";
|
|
10
|
-
import { theme } from "./theme";
|
|
9
|
+
import { theme } from "./ui/theme";
|
|
10
|
+
import { createDialog } from "./ui/dialog";
|
|
11
11
|
|
|
12
12
|
export interface CommentInputOptions {
|
|
13
13
|
renderer: CliRenderer;
|
|
@@ -42,30 +42,139 @@ function createThreadView(
|
|
|
42
42
|
onResolve: () => void,
|
|
43
43
|
onCancel: () => void,
|
|
44
44
|
): CommentInputOverlay {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
const title = thread.id
|
|
46
|
+
? `Thread #${thread.id} (line ${line})`
|
|
47
|
+
: `New comment on line ${line}`;
|
|
48
|
+
|
|
49
|
+
const normalHints = [
|
|
50
|
+
{ key: "NORMAL", action: "" },
|
|
51
|
+
{ key: "c", action: "reply" },
|
|
52
|
+
{ key: "r", action: "resolve" },
|
|
53
|
+
{ key: "Esc/q", action: "close" },
|
|
54
|
+
];
|
|
55
|
+
const insertHints = [
|
|
56
|
+
{ key: "INSERT", action: "" },
|
|
57
|
+
{ key: "Tab", action: "send" },
|
|
58
|
+
{ key: "Esc", action: "back" },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
// --- State ---
|
|
62
|
+
let mode: "normal" | "insert" = "insert";
|
|
63
|
+
|
|
64
|
+
// Build the textarea now (we need it in the key handler closure)
|
|
65
|
+
const textarea = new TextareaRenderable(renderer, {
|
|
66
|
+
width: "100%",
|
|
67
|
+
height: 4,
|
|
68
|
+
flexGrow: 0,
|
|
69
|
+
flexShrink: 0,
|
|
70
|
+
backgroundColor: theme.backgroundElement,
|
|
71
|
+
textColor: theme.textDim,
|
|
72
|
+
focusedBackgroundColor: theme.backgroundPanel,
|
|
73
|
+
focusedTextColor: theme.text,
|
|
74
|
+
wrapMode: "word",
|
|
75
|
+
placeholder: "Press c to reply...",
|
|
76
|
+
placeholderColor: theme.textDim,
|
|
77
|
+
initialValue: "",
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Register our comprehensive key handler FIRST (before createDialog) so it
|
|
81
|
+
// fires before the dialog's Esc handler and can stopPropagation in insert mode.
|
|
82
|
+
const keyHandler = (key: KeyEvent) => {
|
|
83
|
+
if (mode === "insert") {
|
|
84
|
+
// --- INSERT MODE ---
|
|
85
|
+
if (key.name === "escape") {
|
|
86
|
+
key.preventDefault();
|
|
87
|
+
key.stopPropagation();
|
|
88
|
+
enterNormal();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (key.name === "tab") {
|
|
92
|
+
key.preventDefault();
|
|
93
|
+
key.stopPropagation();
|
|
94
|
+
const text = textarea.plainText.trim();
|
|
95
|
+
if (text.length === 0) return;
|
|
96
|
+
onSubmit(text);
|
|
97
|
+
appendToConversation({ author: "reviewer", text, ts: Date.now() });
|
|
98
|
+
textarea.selectAll();
|
|
99
|
+
textarea.deleteChar();
|
|
100
|
+
enterNormal();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// All other keys: let textarea handle them (don't preventDefault)
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --- NORMAL MODE (textarea blurred, all keys are ours) ---
|
|
108
|
+
key.preventDefault();
|
|
109
|
+
key.stopPropagation();
|
|
110
|
+
|
|
111
|
+
switch (key.name) {
|
|
112
|
+
case "escape":
|
|
113
|
+
case "q":
|
|
114
|
+
onCancel();
|
|
115
|
+
return;
|
|
116
|
+
|
|
117
|
+
case "c":
|
|
118
|
+
enterInsert();
|
|
119
|
+
return;
|
|
120
|
+
|
|
121
|
+
case "r":
|
|
122
|
+
onResolve();
|
|
123
|
+
return;
|
|
124
|
+
|
|
125
|
+
case "j":
|
|
126
|
+
case "down":
|
|
127
|
+
scrollBox.scrollBy({ x: 0, y: 1 });
|
|
128
|
+
renderer.requestRender();
|
|
129
|
+
return;
|
|
130
|
+
|
|
131
|
+
case "k":
|
|
132
|
+
case "up":
|
|
133
|
+
scrollBox.scrollBy({ x: 0, y: -1 });
|
|
134
|
+
renderer.requestRender();
|
|
135
|
+
return;
|
|
136
|
+
|
|
137
|
+
case "d":
|
|
138
|
+
if (key.ctrl) {
|
|
139
|
+
scrollBox.scrollBy({ x: 0, y: 5 });
|
|
140
|
+
renderer.requestRender();
|
|
141
|
+
}
|
|
142
|
+
return;
|
|
143
|
+
|
|
144
|
+
case "u":
|
|
145
|
+
if (key.ctrl) {
|
|
146
|
+
scrollBox.scrollBy({ x: 0, y: -5 });
|
|
147
|
+
renderer.requestRender();
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
|
|
151
|
+
case "g":
|
|
152
|
+
if (key.shift) {
|
|
153
|
+
// G = go to bottom
|
|
154
|
+
scrollBox.scrollTo(scrollBox.scrollHeight);
|
|
155
|
+
renderer.requestRender();
|
|
156
|
+
}
|
|
157
|
+
// TODO: gg = go to top (needs double-tap tracking)
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
renderer.keyInput.on("keypress", keyHandler);
|
|
163
|
+
|
|
164
|
+
// Now create the dialog (its Esc handler registers after ours)
|
|
165
|
+
// Pass no-op onDismiss — we handle all keys ourselves above.
|
|
166
|
+
const dialog = createDialog({
|
|
167
|
+
renderer,
|
|
168
|
+
title,
|
|
49
169
|
width: "80%",
|
|
50
170
|
height: "85%",
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
borderStyle: "single",
|
|
55
|
-
borderColor: theme.borderComment,
|
|
56
|
-
title: thread.id ? ` Thread #${thread.id} (line ${line}) ` : ` New comment on line ${line} `,
|
|
57
|
-
flexDirection: "column",
|
|
58
|
-
padding: 1,
|
|
171
|
+
borderColor: theme.blue,
|
|
172
|
+
onDismiss: onCancel,
|
|
173
|
+
hints: insertHints,
|
|
59
174
|
});
|
|
60
175
|
|
|
61
|
-
// ---
|
|
62
|
-
const scrollBox =
|
|
63
|
-
width: "100%",
|
|
64
|
-
flexGrow: 1,
|
|
65
|
-
flexShrink: 1,
|
|
66
|
-
scrollY: true,
|
|
67
|
-
scrollX: false,
|
|
68
|
-
});
|
|
176
|
+
// --- Scrollable conversation history ---
|
|
177
|
+
const scrollBox = dialog.content;
|
|
69
178
|
|
|
70
179
|
function renderMessage(msg: Message): BoxRenderable {
|
|
71
180
|
const isReviewer = msg.author === "reviewer";
|
|
@@ -87,7 +196,7 @@ function createThreadView(
|
|
|
87
196
|
content: tsStr ? `${label} ${tsStr}` : label,
|
|
88
197
|
width: "100%",
|
|
89
198
|
height: 1,
|
|
90
|
-
fg: theme.
|
|
199
|
+
fg: theme.textMuted,
|
|
91
200
|
wrapMode: "none",
|
|
92
201
|
});
|
|
93
202
|
msgBox.add(header);
|
|
@@ -107,64 +216,32 @@ function createThreadView(
|
|
|
107
216
|
for (const msg of thread.messages) {
|
|
108
217
|
scrollBox.add(renderMessage(msg));
|
|
109
218
|
}
|
|
110
|
-
container.add(scrollBox);
|
|
111
219
|
|
|
112
220
|
// --- Separator ---
|
|
113
221
|
const sep = new TextRenderable(renderer, {
|
|
114
222
|
content: "\u2500".repeat(40),
|
|
115
223
|
width: "100%",
|
|
116
224
|
height: 1,
|
|
117
|
-
fg: theme.
|
|
118
|
-
wrapMode: "none",
|
|
119
|
-
});
|
|
120
|
-
container.add(sep);
|
|
121
|
-
|
|
122
|
-
// --- Bottom: textarea (visible in both modes, focused only in insert) ---
|
|
123
|
-
const textarea = new TextareaRenderable(renderer, {
|
|
124
|
-
width: "100%",
|
|
125
|
-
height: 4,
|
|
126
|
-
flexGrow: 0,
|
|
127
|
-
flexShrink: 0,
|
|
128
|
-
backgroundColor: theme.surface1,
|
|
129
|
-
textColor: theme.overlay,
|
|
130
|
-
focusedBackgroundColor: theme.surface0,
|
|
131
|
-
focusedTextColor: theme.text,
|
|
132
|
-
wrapMode: "word",
|
|
133
|
-
placeholder: "Press c to reply...",
|
|
134
|
-
placeholderColor: theme.overlay,
|
|
135
|
-
initialValue: "",
|
|
136
|
-
});
|
|
137
|
-
container.add(textarea);
|
|
138
|
-
|
|
139
|
-
// --- Hint bar (changes with mode) ---
|
|
140
|
-
const hintNormal = " [c] reply [r] resolve [Esc] close";
|
|
141
|
-
const hintInsert = " [Tab] send [Esc] back";
|
|
142
|
-
|
|
143
|
-
const hint = new TextRenderable(renderer, {
|
|
144
|
-
content: hintInsert,
|
|
145
|
-
width: "100%",
|
|
146
|
-
height: 1,
|
|
147
|
-
fg: theme.hintFg,
|
|
148
|
-
bg: theme.hintBg,
|
|
225
|
+
fg: theme.backgroundElement,
|
|
149
226
|
wrapMode: "none",
|
|
150
|
-
truncate: true,
|
|
151
227
|
});
|
|
152
|
-
container.add(
|
|
228
|
+
dialog.container.add(sep);
|
|
153
229
|
|
|
154
|
-
// ---
|
|
155
|
-
|
|
230
|
+
// --- Textarea (visible in both modes, focused only in insert) ---
|
|
231
|
+
dialog.container.add(textarea);
|
|
156
232
|
|
|
233
|
+
// --- Mode helpers (need dialog.setHints available) ---
|
|
157
234
|
function enterInsert(): void {
|
|
158
235
|
mode = "insert";
|
|
159
236
|
textarea.focus();
|
|
160
|
-
|
|
237
|
+
dialog.setHints(insertHints);
|
|
161
238
|
renderer.requestRender();
|
|
162
239
|
}
|
|
163
240
|
|
|
164
241
|
function enterNormal(): void {
|
|
165
242
|
mode = "normal";
|
|
166
243
|
textarea.blur();
|
|
167
|
-
|
|
244
|
+
dialog.setHints(normalHints);
|
|
168
245
|
renderer.requestRender();
|
|
169
246
|
}
|
|
170
247
|
|
|
@@ -192,89 +269,13 @@ function createThreadView(
|
|
|
192
269
|
}, 50);
|
|
193
270
|
}
|
|
194
271
|
|
|
195
|
-
const keyHandler = (key: KeyEvent) => {
|
|
196
|
-
if (mode === "insert") {
|
|
197
|
-
// --- INSERT MODE ---
|
|
198
|
-
if (key.name === "escape") {
|
|
199
|
-
key.preventDefault(); key.stopPropagation();
|
|
200
|
-
enterNormal();
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
if (key.name === "tab") {
|
|
204
|
-
key.preventDefault(); key.stopPropagation();
|
|
205
|
-
const text = textarea.plainText.trim();
|
|
206
|
-
if (text.length === 0) return;
|
|
207
|
-
onSubmit(text);
|
|
208
|
-
appendToConversation({ author: "reviewer", text, ts: Date.now() });
|
|
209
|
-
textarea.selectAll();
|
|
210
|
-
textarea.deleteChar();
|
|
211
|
-
enterNormal();
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
// All other keys: let textarea handle them (don't preventDefault)
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// --- NORMAL MODE (textarea blurred, all keys are ours) ---
|
|
219
|
-
key.preventDefault(); key.stopPropagation();
|
|
220
|
-
|
|
221
|
-
switch (key.name) {
|
|
222
|
-
case "escape":
|
|
223
|
-
case "q":
|
|
224
|
-
onCancel();
|
|
225
|
-
return;
|
|
226
|
-
|
|
227
|
-
case "c":
|
|
228
|
-
enterInsert();
|
|
229
|
-
return;
|
|
230
|
-
|
|
231
|
-
case "r":
|
|
232
|
-
onResolve();
|
|
233
|
-
return;
|
|
234
|
-
|
|
235
|
-
case "j":
|
|
236
|
-
case "down":
|
|
237
|
-
scrollBox.scrollBy({ x: 0, y: 1 });
|
|
238
|
-
renderer.requestRender();
|
|
239
|
-
return;
|
|
240
|
-
|
|
241
|
-
case "k":
|
|
242
|
-
case "up":
|
|
243
|
-
scrollBox.scrollBy({ x: 0, y: -1 });
|
|
244
|
-
renderer.requestRender();
|
|
245
|
-
return;
|
|
246
|
-
|
|
247
|
-
case "d":
|
|
248
|
-
if (key.ctrl) {
|
|
249
|
-
// Use same scrollBy as j/k, just more lines
|
|
250
|
-
scrollBox.scrollBy({ x: 0, y: 5 });
|
|
251
|
-
renderer.requestRender();
|
|
252
|
-
}
|
|
253
|
-
return;
|
|
254
|
-
|
|
255
|
-
case "u":
|
|
256
|
-
if (key.ctrl) {
|
|
257
|
-
scrollBox.scrollBy({ x: 0, y: -5 });
|
|
258
|
-
renderer.requestRender();
|
|
259
|
-
}
|
|
260
|
-
return;
|
|
261
|
-
|
|
262
|
-
case "g":
|
|
263
|
-
if (key.shift) {
|
|
264
|
-
// G = go to bottom
|
|
265
|
-
scrollBox.scrollTo(scrollBox.scrollHeight);
|
|
266
|
-
renderer.requestRender();
|
|
267
|
-
}
|
|
268
|
-
// TODO: gg = go to top (needs double-tap tracking)
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
renderer.keyInput.on("keypress", keyHandler);
|
|
274
|
-
|
|
275
272
|
return {
|
|
276
|
-
container,
|
|
277
|
-
cleanup() {
|
|
273
|
+
container: dialog.container,
|
|
274
|
+
cleanup() {
|
|
275
|
+
renderer.keyInput.off("keypress", keyHandler);
|
|
276
|
+
dialog.cleanup();
|
|
277
|
+
textarea.destroy();
|
|
278
|
+
},
|
|
278
279
|
threadId: thread.id,
|
|
279
280
|
addMessage(msg: Message) { appendToConversation(msg); },
|
|
280
281
|
};
|
package/src/tui/confirm.ts
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
|
-
BoxRenderable,
|
|
3
2
|
TextRenderable,
|
|
4
3
|
type CliRenderer,
|
|
5
4
|
type KeyEvent,
|
|
6
5
|
} from "@opentui/core";
|
|
7
|
-
import { theme } from "./theme";
|
|
6
|
+
import { theme } from "./ui/theme";
|
|
7
|
+
import { createDialog } from "./ui/dialog";
|
|
8
8
|
|
|
9
9
|
export interface ConfirmOptions {
|
|
10
10
|
renderer: CliRenderer;
|
|
11
11
|
message: string;
|
|
12
|
+
title?: string;
|
|
12
13
|
onConfirm: () => void;
|
|
13
14
|
onCancel: () => void;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export interface ConfirmOverlay {
|
|
17
|
-
container: BoxRenderable;
|
|
18
|
+
container: import("@opentui/core").BoxRenderable;
|
|
18
19
|
cleanup: () => void;
|
|
19
20
|
}
|
|
20
21
|
|
|
@@ -24,70 +25,55 @@ export interface ConfirmOverlay {
|
|
|
24
25
|
* y → confirm, n/Esc → cancel
|
|
25
26
|
*/
|
|
26
27
|
export function createConfirm(opts: ConfirmOptions): ConfirmOverlay {
|
|
27
|
-
const { renderer, message, onConfirm, onCancel } = opts;
|
|
28
|
+
const { renderer, message, title = "Confirm", onConfirm, onCancel } = opts;
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
const dialog = createDialog({
|
|
31
|
+
renderer,
|
|
32
|
+
title,
|
|
33
|
+
width: "50%",
|
|
34
|
+
height: 9,
|
|
32
35
|
top: "35%",
|
|
33
36
|
left: "25%",
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
borderColor: theme.borderConfirm,
|
|
41
|
-
title: " Confirm ",
|
|
42
|
-
flexDirection: "column",
|
|
43
|
-
padding: 1,
|
|
44
|
-
alignItems: "center",
|
|
45
|
-
justifyContent: "center",
|
|
37
|
+
borderColor: theme.warning,
|
|
38
|
+
onDismiss: onCancel,
|
|
39
|
+
hints: [
|
|
40
|
+
{ key: "y", action: "yes" },
|
|
41
|
+
{ key: "n/Esc", action: "no" },
|
|
42
|
+
],
|
|
46
43
|
});
|
|
47
44
|
|
|
48
45
|
const msgText = new TextRenderable(renderer, {
|
|
49
46
|
content: message,
|
|
50
47
|
width: "100%",
|
|
51
|
-
height: 1,
|
|
52
48
|
fg: theme.text,
|
|
53
|
-
wrapMode: "
|
|
54
|
-
truncate: true,
|
|
49
|
+
wrapMode: "word",
|
|
55
50
|
});
|
|
56
51
|
|
|
57
|
-
|
|
58
|
-
content: " [y] yes [n/Esc] no",
|
|
59
|
-
width: "100%",
|
|
60
|
-
height: 1,
|
|
61
|
-
fg: theme.hintFg,
|
|
62
|
-
bg: theme.hintBg,
|
|
63
|
-
wrapMode: "none",
|
|
64
|
-
truncate: true,
|
|
65
|
-
});
|
|
52
|
+
dialog.content.add(msgText);
|
|
66
53
|
|
|
67
|
-
|
|
68
|
-
container.add(hint);
|
|
69
|
-
|
|
70
|
-
// Key handler
|
|
71
|
-
const keyHandler = (key: KeyEvent) => {
|
|
54
|
+
const extraKeyHandler = (key: KeyEvent) => {
|
|
72
55
|
if (key.name === "y") {
|
|
73
56
|
key.preventDefault();
|
|
74
57
|
key.stopPropagation();
|
|
75
58
|
onConfirm();
|
|
76
59
|
return;
|
|
77
60
|
}
|
|
78
|
-
if (key.name === "n"
|
|
61
|
+
if (key.name === "n") {
|
|
79
62
|
key.preventDefault();
|
|
80
63
|
key.stopPropagation();
|
|
81
64
|
onCancel();
|
|
82
65
|
return;
|
|
83
66
|
}
|
|
67
|
+
// Esc is handled by dialog's built-in handler (calls onDismiss = onCancel)
|
|
84
68
|
};
|
|
85
69
|
|
|
86
|
-
renderer.keyInput.on("keypress",
|
|
87
|
-
|
|
88
|
-
function cleanup(): void {
|
|
89
|
-
renderer.keyInput.off("keypress", keyHandler);
|
|
90
|
-
}
|
|
70
|
+
renderer.keyInput.on("keypress", extraKeyHandler);
|
|
91
71
|
|
|
92
|
-
return {
|
|
72
|
+
return {
|
|
73
|
+
container: dialog.container,
|
|
74
|
+
cleanup() {
|
|
75
|
+
dialog.cleanup();
|
|
76
|
+
renderer.keyInput.off("keypress", extraKeyHandler);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
93
79
|
}
|
package/src/tui/help.ts
CHANGED
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
import {
|
|
2
|
-
BoxRenderable,
|
|
3
|
-
ScrollBoxRenderable,
|
|
4
2
|
TextRenderable,
|
|
5
3
|
type CliRenderer,
|
|
6
4
|
type KeyEvent,
|
|
7
5
|
} from "@opentui/core";
|
|
8
|
-
import { theme } from "./theme";
|
|
6
|
+
import { theme } from "./ui/theme";
|
|
7
|
+
import { createDialog } from "./ui/dialog";
|
|
9
8
|
|
|
10
9
|
export interface HelpOverlay {
|
|
11
|
-
container: BoxRenderable;
|
|
10
|
+
container: import("@opentui/core").BoxRenderable;
|
|
12
11
|
cleanup: () => void;
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
/**
|
|
16
15
|
* Create a help overlay popup showing all keybindings.
|
|
17
|
-
* Dismissable with
|
|
16
|
+
* Dismissable with `?`, `q`, or `Esc`.
|
|
18
17
|
*/
|
|
19
18
|
export function createHelp(opts: {
|
|
20
19
|
renderer: CliRenderer;
|
|
20
|
+
version: string;
|
|
21
21
|
onClose: () => void;
|
|
22
22
|
}): HelpOverlay {
|
|
23
|
-
const { renderer, onClose } = opts;
|
|
23
|
+
const { renderer, version, onClose } = opts;
|
|
24
24
|
|
|
25
25
|
const helpText = [
|
|
26
|
+
"",
|
|
27
|
+
` revspec v${version}`,
|
|
26
28
|
"",
|
|
27
29
|
" Navigation",
|
|
28
30
|
" j/k Down/up",
|
|
@@ -35,10 +37,7 @@ export function createHelp(opts: {
|
|
|
35
37
|
" ]t/[t Next/prev thread",
|
|
36
38
|
" ]r/[r Next/prev unread thread",
|
|
37
39
|
"",
|
|
38
|
-
"
|
|
39
|
-
" m Toggle markdown / line mode",
|
|
40
|
-
"",
|
|
41
|
-
" Review (switches to line mode)",
|
|
40
|
+
" Review",
|
|
42
41
|
" c Comment / view thread / reply",
|
|
43
42
|
" r Resolve thread",
|
|
44
43
|
" R Resolve all pending",
|
|
@@ -54,30 +53,19 @@ export function createHelp(opts: {
|
|
|
54
53
|
"",
|
|
55
54
|
].join("\n");
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
top: "10%",
|
|
61
|
-
left: "20%",
|
|
56
|
+
const dialog = createDialog({
|
|
57
|
+
renderer,
|
|
58
|
+
title: "Help",
|
|
62
59
|
width: "60%",
|
|
63
60
|
height: Math.min(26, renderer.height - 2),
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
const scrollBox = new ScrollBoxRenderable(renderer, {
|
|
75
|
-
width: "100%",
|
|
76
|
-
flexGrow: 1,
|
|
77
|
-
flexShrink: 1,
|
|
78
|
-
scrollY: true,
|
|
79
|
-
scrollX: false,
|
|
80
|
-
backgroundColor: theme.base,
|
|
61
|
+
top: "10%",
|
|
62
|
+
left: "20%",
|
|
63
|
+
borderColor: theme.info,
|
|
64
|
+
onDismiss: onClose,
|
|
65
|
+
hints: [
|
|
66
|
+
{ key: "q/?/Esc", action: "close" },
|
|
67
|
+
{ key: "j/k", action: "scroll" },
|
|
68
|
+
],
|
|
81
69
|
});
|
|
82
70
|
|
|
83
71
|
const content = new TextRenderable(renderer, {
|
|
@@ -87,24 +75,10 @@ export function createHelp(opts: {
|
|
|
87
75
|
wrapMode: "none",
|
|
88
76
|
});
|
|
89
77
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const hint = new TextRenderable(renderer, {
|
|
93
|
-
content: " [q/?/Esc] close [j/k] scroll",
|
|
94
|
-
width: "100%",
|
|
95
|
-
height: 1,
|
|
96
|
-
fg: theme.hintFg,
|
|
97
|
-
bg: theme.hintBg,
|
|
98
|
-
wrapMode: "none",
|
|
99
|
-
truncate: true,
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
container.add(scrollBox);
|
|
103
|
-
container.add(hint);
|
|
78
|
+
dialog.content.add(content);
|
|
104
79
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (key.name === "escape" || key.name === "q" || key.sequence === "?") {
|
|
80
|
+
const extraKeyHandler = (key: KeyEvent) => {
|
|
81
|
+
if (key.name === "q" || key.sequence === "?") {
|
|
108
82
|
key.preventDefault();
|
|
109
83
|
key.stopPropagation();
|
|
110
84
|
onClose();
|
|
@@ -113,24 +87,26 @@ export function createHelp(opts: {
|
|
|
113
87
|
if (key.name === "j" || key.name === "down") {
|
|
114
88
|
key.preventDefault();
|
|
115
89
|
key.stopPropagation();
|
|
116
|
-
|
|
90
|
+
dialog.content.scrollTo(Math.min(dialog.content.scrollTop + 1, dialog.content.scrollHeight));
|
|
117
91
|
renderer.requestRender();
|
|
118
92
|
return;
|
|
119
93
|
}
|
|
120
94
|
if (key.name === "k" || key.name === "up") {
|
|
121
95
|
key.preventDefault();
|
|
122
96
|
key.stopPropagation();
|
|
123
|
-
|
|
97
|
+
dialog.content.scrollTo(Math.max(dialog.content.scrollTop - 1, 0));
|
|
124
98
|
renderer.requestRender();
|
|
125
99
|
return;
|
|
126
100
|
}
|
|
127
101
|
};
|
|
128
102
|
|
|
129
|
-
renderer.keyInput.on("keypress",
|
|
103
|
+
renderer.keyInput.on("keypress", extraKeyHandler);
|
|
130
104
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
105
|
+
return {
|
|
106
|
+
container: dialog.container,
|
|
107
|
+
cleanup() {
|
|
108
|
+
dialog.cleanup();
|
|
109
|
+
renderer.keyInput.off("keypress", extraKeyHandler);
|
|
110
|
+
},
|
|
111
|
+
};
|
|
136
112
|
}
|