revspec 0.3.0 → 0.5.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/README.md +13 -0
- package/docs/superpowers/plans/2026-03-15-ui-refactor.md +1025 -0
- package/package.json +1 -1
- package/src/state/review-state.ts +5 -0
- package/src/tui/app.ts +189 -234
- package/src/tui/comment-input.ts +146 -144
- package/src/tui/confirm.ts +29 -43
- package/src/tui/help.ts +77 -76
- package/src/tui/pager.ts +54 -267
- package/src/tui/search.ts +6 -6
- package/src/tui/status-bar.ts +27 -24
- package/src/tui/thread-list.ts +29 -55
- package/src/tui/ui/dialog.ts +106 -0
- package/src/tui/ui/hint-bar.ts +20 -0
- package/src/tui/ui/keybinds.ts +106 -0
- package/src/tui/ui/markdown.ts +292 -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/package.json
CHANGED
|
@@ -136,6 +136,11 @@ export class ReviewState {
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
deleteThread(threadId: string): void {
|
|
140
|
+
this.threads = this.threads.filter((t) => t.id !== threadId);
|
|
141
|
+
this._unreadThreadIds.delete(threadId);
|
|
142
|
+
}
|
|
143
|
+
|
|
139
144
|
addOwnerReply(threadId: string, text: string, ts?: number): void {
|
|
140
145
|
const thread = this.threads.find((t) => t.id === threadId);
|
|
141
146
|
if (!thread) return;
|
package/src/tui/app.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { buildPagerNodes, createPager, countExtraVisualLines, type PagerComponen
|
|
|
17
17
|
import {
|
|
18
18
|
buildTopBar,
|
|
19
19
|
buildBottomBar,
|
|
20
|
+
setBottomBarMessage,
|
|
20
21
|
createTopBar,
|
|
21
22
|
createBottomBar,
|
|
22
23
|
type TopBarComponents,
|
|
@@ -28,6 +29,7 @@ import { createSearch } from "./search";
|
|
|
28
29
|
import { createThreadList } from "./thread-list";
|
|
29
30
|
import { createConfirm } from "./confirm";
|
|
30
31
|
import { createHelp } from "./help";
|
|
32
|
+
import { createKeybindRegistry, type KeyBinding } from "./ui/keybinds";
|
|
31
33
|
|
|
32
34
|
export async function runTui(
|
|
33
35
|
specFile: string,
|
|
@@ -128,7 +130,8 @@ export async function runTui(
|
|
|
128
130
|
|
|
129
131
|
buildPagerNodes(pager.lineNode, state, searchQuery, state.unreadThreadIds);
|
|
130
132
|
buildTopBar(topBar, specFile, state, state.unreadCount(), specMtimeChanged);
|
|
131
|
-
|
|
133
|
+
const hasThread = !!state.threadAtLine(state.cursorLine);
|
|
134
|
+
buildBottomBar(bottomBar, commandBuffer, hasThread);
|
|
132
135
|
renderer.requestRender();
|
|
133
136
|
}
|
|
134
137
|
|
|
@@ -138,16 +141,6 @@ export async function runTui(
|
|
|
138
141
|
// Command mode state
|
|
139
142
|
let commandBuffer: string | null = null;
|
|
140
143
|
|
|
141
|
-
// Bracket-pending state for ]t / [t / ]r / [r navigation
|
|
142
|
-
let bracketPending: "]" | "[" | null = null;
|
|
143
|
-
let bracketPendingTimer: ReturnType<typeof setTimeout> | null = null;
|
|
144
|
-
|
|
145
|
-
// Delete-pending state: first `d` sets timer, second `d` within 500ms executes
|
|
146
|
-
let deletePendingTimer: ReturnType<typeof setTimeout> | null = null;
|
|
147
|
-
|
|
148
|
-
// g-pending state: first `g` sets timer, second `g` within 500ms goes to top
|
|
149
|
-
let gPendingTimer: ReturnType<typeof setTimeout> | null = null;
|
|
150
|
-
|
|
151
144
|
// Overlay state — when an overlay is active, normal keybindings are blocked.
|
|
152
145
|
// The overlay's own key handlers manage its lifecycle.
|
|
153
146
|
type ActiveOverlay = {
|
|
@@ -199,6 +192,7 @@ export async function runTui(
|
|
|
199
192
|
// Signal to watch process that session has ended
|
|
200
193
|
appendEvent(jsonlPath, { type: "session-end", author: "reviewer", ts: Date.now() });
|
|
201
194
|
liveWatcher.stop();
|
|
195
|
+
keybinds.destroy();
|
|
202
196
|
renderer.destroy();
|
|
203
197
|
resolve();
|
|
204
198
|
}
|
|
@@ -229,12 +223,12 @@ export async function runTui(
|
|
|
229
223
|
if (cmd === "w") {
|
|
230
224
|
// Merge JSONL -> JSON, stay open
|
|
231
225
|
doMerge();
|
|
232
|
-
bottomBar
|
|
226
|
+
setBottomBarMessage(bottomBar, " \u2714 Merged to review JSON");
|
|
233
227
|
renderer.requestRender();
|
|
234
228
|
setTimeout(() => { refreshPager(); }, 1200);
|
|
235
229
|
return "stay";
|
|
236
230
|
}
|
|
237
|
-
if (cmd === "wq") {
|
|
231
|
+
if (cmd === "wq" || cmd === "qw") {
|
|
238
232
|
// Merge and exit
|
|
239
233
|
mergeAndExit(resolve);
|
|
240
234
|
return "merged";
|
|
@@ -242,19 +236,21 @@ export async function runTui(
|
|
|
242
236
|
if (cmd === "q") {
|
|
243
237
|
// Exit only if merged (no pending changes)
|
|
244
238
|
if (hasPendingChanges()) {
|
|
245
|
-
bottomBar
|
|
239
|
+
setBottomBarMessage(bottomBar, " Unmerged changes. Use :w to save or :q! to discard");
|
|
246
240
|
renderer.requestRender();
|
|
247
241
|
setTimeout(() => { refreshPager(); }, 2000);
|
|
248
242
|
return "stay";
|
|
249
243
|
}
|
|
250
244
|
appendEvent(jsonlPath, { type: "session-end", author: "reviewer", ts: Date.now() });
|
|
251
245
|
liveWatcher.stop();
|
|
246
|
+
keybinds.destroy();
|
|
252
247
|
return "exit";
|
|
253
248
|
}
|
|
254
249
|
if (cmd === "q!") {
|
|
255
250
|
// Exit without merging
|
|
256
251
|
appendEvent(jsonlPath, { type: "session-end", author: "reviewer", ts: Date.now() });
|
|
257
252
|
liveWatcher.stop();
|
|
253
|
+
keybinds.destroy();
|
|
258
254
|
return "exit";
|
|
259
255
|
}
|
|
260
256
|
return "stay"; // unknown command, ignore
|
|
@@ -376,6 +372,35 @@ export async function runTui(
|
|
|
376
372
|
return null;
|
|
377
373
|
}
|
|
378
374
|
|
|
375
|
+
// --- Keybind registry ---
|
|
376
|
+
|
|
377
|
+
const bindings: KeyBinding[] = [
|
|
378
|
+
{ key: "j", action: "cursor-down" },
|
|
379
|
+
{ key: "down", action: "cursor-down" },
|
|
380
|
+
{ key: "k", action: "cursor-up" },
|
|
381
|
+
{ key: "up", action: "cursor-up" },
|
|
382
|
+
{ key: "C-d", action: "half-page-down" },
|
|
383
|
+
{ key: "C-u", action: "half-page-up" },
|
|
384
|
+
{ key: "G", action: "goto-bottom" },
|
|
385
|
+
{ key: "gg", action: "goto-top" },
|
|
386
|
+
{ key: "n", action: "search-next" },
|
|
387
|
+
{ key: "N", action: "search-prev" },
|
|
388
|
+
{ key: "c", action: "comment" },
|
|
389
|
+
{ key: "l", action: "thread-list" },
|
|
390
|
+
{ key: "r", action: "resolve" },
|
|
391
|
+
{ key: "R", action: "resolve-all" },
|
|
392
|
+
{ key: "dd", action: "delete-draft" },
|
|
393
|
+
{ key: "a", action: "approve" },
|
|
394
|
+
{ key: "]t", action: "next-thread" },
|
|
395
|
+
{ key: "[t", action: "prev-thread" },
|
|
396
|
+
{ key: "]r", action: "next-unread" },
|
|
397
|
+
{ key: "[r", action: "prev-unread" },
|
|
398
|
+
{ key: "?", action: "help" },
|
|
399
|
+
{ key: "/", action: "search" },
|
|
400
|
+
{ key: ":", action: "command-mode" },
|
|
401
|
+
];
|
|
402
|
+
const keybinds = createKeybindRegistry(bindings);
|
|
403
|
+
|
|
379
404
|
refreshPager();
|
|
380
405
|
renderer.start();
|
|
381
406
|
|
|
@@ -451,177 +476,149 @@ export async function runTui(
|
|
|
451
476
|
}
|
|
452
477
|
|
|
453
478
|
// Normal mode keybindings
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
479
|
+
const action = keybinds.match(key);
|
|
480
|
+
|
|
481
|
+
// Show pending sequence hint
|
|
482
|
+
if (!action) {
|
|
483
|
+
const p = keybinds.pending();
|
|
484
|
+
if (p) {
|
|
485
|
+
setBottomBarMessage(bottomBar, ` ${p}`);
|
|
486
|
+
renderer.requestRender();
|
|
487
|
+
}
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
switch (action) {
|
|
492
|
+
case "cursor-down":
|
|
457
493
|
if (state.cursorLine < state.lineCount) {
|
|
458
494
|
state.cursorLine++;
|
|
459
495
|
ensureCursorVisible();
|
|
460
496
|
refreshPager();
|
|
461
497
|
}
|
|
462
498
|
break;
|
|
463
|
-
|
|
464
|
-
case "k":
|
|
465
|
-
case "up": {
|
|
499
|
+
case "cursor-up":
|
|
466
500
|
if (state.cursorLine > 1) {
|
|
467
501
|
state.cursorLine--;
|
|
468
502
|
ensureCursorVisible();
|
|
469
503
|
refreshPager();
|
|
470
504
|
}
|
|
471
505
|
break;
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
const half = Math.max(1, Math.floor(pageSize() / 2));
|
|
478
|
-
state.cursorLine = Math.min(state.cursorLine + half, state.lineCount);
|
|
479
|
-
ensureCursorVisible();
|
|
480
|
-
refreshPager();
|
|
481
|
-
} else {
|
|
482
|
-
// d without ctrl — delete draft comment (dd = double-tap within 500ms)
|
|
483
|
-
refreshPager();
|
|
484
|
-
const thread = state.threadAtLine(state.cursorLine);
|
|
485
|
-
if (!thread) break;
|
|
486
|
-
if (deletePendingTimer) {
|
|
487
|
-
// Second d within 500ms — execute delete
|
|
488
|
-
clearTimeout(deletePendingTimer);
|
|
489
|
-
deletePendingTimer = null;
|
|
490
|
-
const hadReviewerMsg = thread.messages.some((m) => m.author === "reviewer");
|
|
491
|
-
if (hadReviewerMsg) {
|
|
492
|
-
state.deleteLastDraftMessage(thread.id);
|
|
493
|
-
appendEvent(jsonlPath, { type: "delete", threadId: thread.id, author: "reviewer", ts: Date.now() });
|
|
494
|
-
refreshPager();
|
|
495
|
-
bottomBar.text.content = " \u2714 Deleted draft comment";
|
|
496
|
-
renderer.requestRender();
|
|
497
|
-
setTimeout(() => { refreshPager(); }, 1500);
|
|
498
|
-
} else {
|
|
499
|
-
bottomBar.text.content = " No reviewer message to delete";
|
|
500
|
-
renderer.requestRender();
|
|
501
|
-
setTimeout(() => { refreshPager(); }, 1500);
|
|
502
|
-
}
|
|
503
|
-
} else {
|
|
504
|
-
// First d — show hint and start timer
|
|
505
|
-
bottomBar.text.content = " Press d again to delete";
|
|
506
|
-
renderer.requestRender();
|
|
507
|
-
deletePendingTimer = setTimeout(() => {
|
|
508
|
-
deletePendingTimer = null;
|
|
509
|
-
refreshPager();
|
|
510
|
-
}, 500);
|
|
511
|
-
}
|
|
512
|
-
}
|
|
506
|
+
case "half-page-down": {
|
|
507
|
+
const half = Math.max(1, Math.floor(pageSize() / 2));
|
|
508
|
+
state.cursorLine = Math.min(state.cursorLine + half, state.lineCount);
|
|
509
|
+
ensureCursorVisible();
|
|
510
|
+
refreshPager();
|
|
513
511
|
break;
|
|
514
512
|
}
|
|
515
|
-
case "
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
ensureCursorVisible();
|
|
521
|
-
refreshPager();
|
|
522
|
-
}
|
|
513
|
+
case "half-page-up": {
|
|
514
|
+
const half = Math.max(1, Math.floor(pageSize() / 2));
|
|
515
|
+
state.cursorLine = Math.max(state.cursorLine - half, 1);
|
|
516
|
+
ensureCursorVisible();
|
|
517
|
+
refreshPager();
|
|
523
518
|
break;
|
|
524
519
|
}
|
|
525
|
-
case "
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
520
|
+
case "goto-bottom":
|
|
521
|
+
state.cursorLine = state.lineCount;
|
|
522
|
+
ensureCursorVisible();
|
|
523
|
+
refreshPager();
|
|
524
|
+
break;
|
|
525
|
+
case "goto-top":
|
|
526
|
+
state.cursorLine = 1;
|
|
527
|
+
ensureCursorVisible();
|
|
528
|
+
refreshPager();
|
|
529
|
+
break;
|
|
530
|
+
case "search-next":
|
|
531
|
+
if (searchQuery) {
|
|
532
|
+
const match = findNextMatch(state.specLines, searchQuery, state.cursorLine, 1);
|
|
533
|
+
if (match !== null) {
|
|
534
|
+
state.cursorLine = match;
|
|
535
|
+
ensureCursorVisible();
|
|
537
536
|
}
|
|
538
537
|
} else {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
538
|
+
setBottomBarMessage(bottomBar, " No active search \u2014 use / to search");
|
|
539
|
+
renderer.requestRender();
|
|
540
|
+
setTimeout(() => { refreshPager(); }, 1500);
|
|
541
|
+
}
|
|
542
|
+
refreshPager();
|
|
543
|
+
break;
|
|
544
|
+
case "search-prev":
|
|
545
|
+
if (searchQuery) {
|
|
546
|
+
const match = findNextMatch(state.specLines, searchQuery, state.cursorLine, -1);
|
|
547
|
+
if (match !== null) {
|
|
548
|
+
state.cursorLine = match;
|
|
549
|
+
ensureCursorVisible();
|
|
550
550
|
}
|
|
551
|
+
} else {
|
|
552
|
+
setBottomBarMessage(bottomBar, " No active search \u2014 use / to search");
|
|
553
|
+
renderer.requestRender();
|
|
554
|
+
setTimeout(() => { refreshPager(); }, 1500);
|
|
551
555
|
}
|
|
552
556
|
refreshPager();
|
|
553
557
|
break;
|
|
554
|
-
|
|
555
|
-
case "c": {
|
|
558
|
+
case "comment":
|
|
556
559
|
showCommentInput();
|
|
557
560
|
break;
|
|
558
|
-
|
|
559
|
-
case "l": {
|
|
560
|
-
// Thread list
|
|
561
|
+
case "thread-list":
|
|
561
562
|
showThreadListOverlay();
|
|
562
563
|
break;
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
if (
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
state.resolveThread(thread.id);
|
|
571
|
-
state.markRead(thread.id);
|
|
572
|
-
appendEvent(jsonlPath, { type: wasResolved ? "unresolve" : "resolve", threadId: thread.id, author: "reviewer", ts: Date.now() });
|
|
573
|
-
refreshPager();
|
|
574
|
-
const msg = wasResolved
|
|
575
|
-
? ` \u21a9 Reopened thread #${thread.id}`
|
|
576
|
-
: ` \u2714 Resolved thread #${thread.id}`;
|
|
577
|
-
bottomBar.text.content = msg;
|
|
578
|
-
renderer.requestRender();
|
|
579
|
-
setTimeout(() => { refreshPager(); }, 1500);
|
|
580
|
-
}
|
|
581
|
-
} else {
|
|
582
|
-
// Shift+R = resolve all pending
|
|
583
|
-
const { pending } = state.activeThreadCount();
|
|
584
|
-
const pendingThreads = state.threads.filter(t => t.status === "pending");
|
|
585
|
-
state.resolveAllPending();
|
|
586
|
-
for (const t of pendingThreads) {
|
|
587
|
-
appendEvent(jsonlPath, { type: "resolve", threadId: t.id, author: "reviewer", ts: Date.now() });
|
|
588
|
-
}
|
|
564
|
+
case "resolve": {
|
|
565
|
+
const thread = state.threadAtLine(state.cursorLine);
|
|
566
|
+
if (thread) {
|
|
567
|
+
const wasResolved = thread.status === "resolved";
|
|
568
|
+
state.resolveThread(thread.id);
|
|
569
|
+
state.markRead(thread.id);
|
|
570
|
+
appendEvent(jsonlPath, { type: wasResolved ? "unresolve" : "resolve", threadId: thread.id, author: "reviewer", ts: Date.now() });
|
|
589
571
|
refreshPager();
|
|
590
|
-
|
|
572
|
+
const msg = wasResolved
|
|
573
|
+
? ` \u21a9 Reopened thread #${thread.id}`
|
|
574
|
+
: ` \u2714 Resolved thread #${thread.id}`;
|
|
575
|
+
setBottomBarMessage(bottomBar, msg);
|
|
591
576
|
renderer.requestRender();
|
|
592
577
|
setTimeout(() => { refreshPager(); }, 1500);
|
|
593
578
|
}
|
|
594
579
|
break;
|
|
595
580
|
}
|
|
596
|
-
case "
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
} else {
|
|
603
|
-
// g — first of gg sequence
|
|
604
|
-
if (gPendingTimer) {
|
|
605
|
-
// Second g within 500ms — go to first line
|
|
606
|
-
clearTimeout(gPendingTimer);
|
|
607
|
-
gPendingTimer = null;
|
|
608
|
-
state.cursorLine = 1;
|
|
609
|
-
ensureCursorVisible();
|
|
610
|
-
refreshPager();
|
|
611
|
-
} else {
|
|
612
|
-
gPendingTimer = setTimeout(() => {
|
|
613
|
-
gPendingTimer = null;
|
|
614
|
-
}, 500);
|
|
615
|
-
}
|
|
581
|
+
case "resolve-all": {
|
|
582
|
+
const { pending } = state.activeThreadCount();
|
|
583
|
+
const pendingThreads = state.threads.filter(t => t.status === "pending");
|
|
584
|
+
state.resolveAllPending();
|
|
585
|
+
for (const t of pendingThreads) {
|
|
586
|
+
appendEvent(jsonlPath, { type: "resolve", threadId: t.id, author: "reviewer", ts: Date.now() });
|
|
616
587
|
}
|
|
588
|
+
refreshPager();
|
|
589
|
+
setBottomBarMessage(bottomBar, ` \u2714 Resolved ${pending} pending thread(s)`);
|
|
590
|
+
renderer.requestRender();
|
|
591
|
+
setTimeout(() => { refreshPager(); }, 1500);
|
|
617
592
|
break;
|
|
618
593
|
}
|
|
619
|
-
case "
|
|
620
|
-
|
|
594
|
+
case "delete-draft": {
|
|
595
|
+
const thread = state.threadAtLine(state.cursorLine);
|
|
596
|
+
if (!thread) break;
|
|
597
|
+
const deleteOverlay = createConfirm({
|
|
598
|
+
renderer,
|
|
599
|
+
title: "Delete Thread",
|
|
600
|
+
message: `Delete thread #${thread.id} on line ${thread.line}?`,
|
|
601
|
+
onConfirm: () => {
|
|
602
|
+
dismissOverlay();
|
|
603
|
+
state.deleteThread(thread.id);
|
|
604
|
+
appendEvent(jsonlPath, { type: "delete", threadId: thread.id, author: "reviewer", ts: Date.now() });
|
|
605
|
+
refreshPager();
|
|
606
|
+
setBottomBarMessage(bottomBar, ` \u2714 Deleted thread #${thread.id}`);
|
|
607
|
+
renderer.requestRender();
|
|
608
|
+
setTimeout(() => { refreshPager(); }, 1500);
|
|
609
|
+
},
|
|
610
|
+
onCancel: () => {
|
|
611
|
+
dismissOverlay();
|
|
612
|
+
},
|
|
613
|
+
});
|
|
614
|
+
showOverlay(deleteOverlay);
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
case "approve":
|
|
621
618
|
if (state.canApprove()) {
|
|
622
619
|
const confirmOverlay = createConfirm({
|
|
623
620
|
renderer,
|
|
624
|
-
message: "Approve spec and proceed to implementation?
|
|
621
|
+
message: "Approve spec and proceed to implementation?",
|
|
625
622
|
onConfirm: () => {
|
|
626
623
|
dismissOverlay();
|
|
627
624
|
appendEvent(jsonlPath, { type: "approve", author: "reviewer", ts: Date.now() });
|
|
@@ -632,105 +629,63 @@ export async function runTui(
|
|
|
632
629
|
},
|
|
633
630
|
});
|
|
634
631
|
showOverlay(confirmOverlay);
|
|
635
|
-
return;
|
|
636
632
|
} else {
|
|
637
|
-
// Show why approval is blocked
|
|
638
633
|
const { open, pending } = state.activeThreadCount();
|
|
639
634
|
const total = open + pending;
|
|
640
|
-
const msg =
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
bottomBar.text.content = ` \u26a0 ${msg}`;
|
|
635
|
+
const msg = total === 0
|
|
636
|
+
? "No threads to approve"
|
|
637
|
+
: `${total} thread${total !== 1 ? "s" : ""} still open/pending`;
|
|
638
|
+
setBottomBarMessage(bottomBar, ` \u26a0 ${msg}`);
|
|
645
639
|
renderer.requestRender();
|
|
646
|
-
setTimeout(() => {
|
|
647
|
-
refreshPager();
|
|
648
|
-
}, 2000);
|
|
640
|
+
setTimeout(() => { refreshPager(); }, 2000);
|
|
649
641
|
}
|
|
650
642
|
break;
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
bracketPending = null;
|
|
657
|
-
if (bracketPendingTimer) { clearTimeout(bracketPendingTimer); bracketPendingTimer = null; }
|
|
658
|
-
if (key.name === "t" || key.sequence === "t") {
|
|
659
|
-
if (pending === "]") {
|
|
660
|
-
const next = state.nextActiveThread();
|
|
661
|
-
if (next !== null) {
|
|
662
|
-
state.cursorLine = next;
|
|
663
|
-
ensureCursorVisible();
|
|
664
|
-
refreshPager();
|
|
665
|
-
}
|
|
666
|
-
} else {
|
|
667
|
-
const prev = state.prevActiveThread();
|
|
668
|
-
if (prev !== null) {
|
|
669
|
-
state.cursorLine = prev;
|
|
670
|
-
ensureCursorVisible();
|
|
671
|
-
refreshPager();
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
} else if (key.name === "r" || key.sequence === "r") {
|
|
675
|
-
if (pending === "]") {
|
|
676
|
-
const nextLine = state.nextUnreadThread();
|
|
677
|
-
if (nextLine !== null) {
|
|
678
|
-
state.cursorLine = nextLine;
|
|
679
|
-
ensureCursorVisible();
|
|
680
|
-
refreshPager();
|
|
681
|
-
}
|
|
682
|
-
} else {
|
|
683
|
-
const prevLine = state.prevUnreadThread();
|
|
684
|
-
if (prevLine !== null) {
|
|
685
|
-
state.cursorLine = prevLine;
|
|
686
|
-
ensureCursorVisible();
|
|
687
|
-
refreshPager();
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
refreshPager(); // clear the bracket hint
|
|
692
|
-
break;
|
|
693
|
-
}
|
|
694
|
-
// Check for "]" or "[" to start bracket sequence
|
|
695
|
-
if (key.sequence === "]") {
|
|
696
|
-
bracketPending = "]";
|
|
697
|
-
bottomBar.text.content = " ]...";
|
|
698
|
-
renderer.requestRender();
|
|
699
|
-
bracketPendingTimer = setTimeout(() => {
|
|
700
|
-
bracketPending = null;
|
|
701
|
-
bracketPendingTimer = null;
|
|
702
|
-
refreshPager();
|
|
703
|
-
}, 500);
|
|
704
|
-
break;
|
|
705
|
-
}
|
|
706
|
-
if (key.sequence === "[") {
|
|
707
|
-
bracketPending = "[";
|
|
708
|
-
bottomBar.text.content = " [...";
|
|
709
|
-
renderer.requestRender();
|
|
710
|
-
bracketPendingTimer = setTimeout(() => {
|
|
711
|
-
bracketPending = null;
|
|
712
|
-
bracketPendingTimer = null;
|
|
713
|
-
refreshPager();
|
|
714
|
-
}, 500);
|
|
715
|
-
break;
|
|
643
|
+
case "next-thread": {
|
|
644
|
+
const next = state.nextActiveThread();
|
|
645
|
+
if (next !== null) {
|
|
646
|
+
state.cursorLine = next;
|
|
647
|
+
ensureCursorVisible();
|
|
716
648
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
649
|
+
refreshPager();
|
|
650
|
+
break;
|
|
651
|
+
}
|
|
652
|
+
case "prev-thread": {
|
|
653
|
+
const prev = state.prevActiveThread();
|
|
654
|
+
if (prev !== null) {
|
|
655
|
+
state.cursorLine = prev;
|
|
656
|
+
ensureCursorVisible();
|
|
721
657
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
658
|
+
refreshPager();
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
case "next-unread": {
|
|
662
|
+
const nextLine = state.nextUnreadThread();
|
|
663
|
+
if (nextLine !== null) {
|
|
664
|
+
state.cursorLine = nextLine;
|
|
665
|
+
ensureCursorVisible();
|
|
726
666
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
667
|
+
refreshPager();
|
|
668
|
+
break;
|
|
669
|
+
}
|
|
670
|
+
case "prev-unread": {
|
|
671
|
+
const prevLine = state.prevUnreadThread();
|
|
672
|
+
if (prevLine !== null) {
|
|
673
|
+
state.cursorLine = prevLine;
|
|
674
|
+
ensureCursorVisible();
|
|
731
675
|
}
|
|
676
|
+
refreshPager();
|
|
732
677
|
break;
|
|
733
678
|
}
|
|
679
|
+
case "help":
|
|
680
|
+
showHelpOverlay();
|
|
681
|
+
break;
|
|
682
|
+
case "search":
|
|
683
|
+
showSearchOverlay();
|
|
684
|
+
break;
|
|
685
|
+
case "command-mode":
|
|
686
|
+
commandBuffer = "";
|
|
687
|
+
refreshPager();
|
|
688
|
+
break;
|
|
734
689
|
}
|
|
735
690
|
});
|
|
736
691
|
});
|