rv-bible-cli 0.1.5 → 0.1.6
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 +7 -2
- package/dist/format.d.ts +1 -0
- package/dist/format.js +1 -1
- package/dist/index.js +5 -1
- package/dist/ui/Pager.js +132 -46
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -114,6 +114,7 @@ When you read a chapter in a terminal, rvb opens a full-screen interactive reade
|
|
|
114
114
|
|-----|--------|
|
|
115
115
|
| `j` / `k` / arrows | scroll up/down |
|
|
116
116
|
| `Space` / `b` | page down/up |
|
|
117
|
+
| `Tab` / `Shift+Tab` | scroll down / up by a quarter of the chapter |
|
|
117
118
|
| `g` / `G` | jump to top / bottom |
|
|
118
119
|
| left / right | prev / next chapter (even across books) |
|
|
119
120
|
| `f` | toggle footnotes on/off |
|
|
@@ -134,18 +135,22 @@ When you read a chapter in a terminal, rvb opens a full-screen interactive reade
|
|
|
134
135
|
Deep-dive into footnotes and cross-references. Press `d` while reading any chapter.
|
|
135
136
|
|
|
136
137
|
- A cursor highlights the current verse
|
|
137
|
-
- The bottom panel shows
|
|
138
|
+
- The bottom panel shows the **full** footnote text for that verse — header line with marker + word + numbered cross-references, then the wrapped commentary body underneath
|
|
138
139
|
- **Type a number** to follow a cross-reference — you'll jump to that chapter and land on the exact verse
|
|
139
140
|
- Press `[` to go back to where you were
|
|
140
141
|
- Study mode stays active as you navigate — it's designed for chaining through references
|
|
142
|
+
- If a verse has very long notes, the panel caps at half your terminal height — use `,` / `.` to scroll within it
|
|
141
143
|
|
|
142
144
|
| Key | Action |
|
|
143
145
|
|-----|--------|
|
|
144
|
-
| up / down | move
|
|
146
|
+
| up / down | move cursor up / down one verse |
|
|
145
147
|
| left / right | prev / next chapter |
|
|
146
148
|
| type number | follow a cross-reference |
|
|
147
149
|
| `v` then up/down then `c` | select a range and copy |
|
|
148
150
|
| `c` | copy current verse |
|
|
151
|
+
| `Tab` / `Shift+Tab` | jump cursor by a quarter of the chapter |
|
|
152
|
+
| `g` / `G` | cursor to first / last verse |
|
|
153
|
+
| `,` / `.` | scroll the footnote panel up / down (when it overflows) |
|
|
149
154
|
| `f` / `o` | toggle footnotes / outline |
|
|
150
155
|
| `/` | find text |
|
|
151
156
|
| `:` | jump to a book |
|
package/dist/format.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export declare function getZoom(): number;
|
|
|
8
8
|
export declare function zoomIn(): void;
|
|
9
9
|
export declare function zoomOut(): void;
|
|
10
10
|
export declare function zoomReset(): void;
|
|
11
|
+
export declare function wrapWords(text: string, maxWidth: number, ansi?: boolean): string[];
|
|
11
12
|
export declare function renderFootnoteBlock(footnotes: Footnote[]): string;
|
|
12
13
|
export declare function renderNoteDisplayAll(verse: Verse, footnotes: Footnote[], bookName: string): string;
|
|
13
14
|
export declare function renderNoteDisplay(verse: Verse, fn: Footnote, bookName: string): string;
|
package/dist/format.js
CHANGED
|
@@ -118,7 +118,7 @@ function visibleLength(s) {
|
|
|
118
118
|
// ── Word-wrap helper ─────────────────────────────────────────────────────────
|
|
119
119
|
// Wraps plain `text` to `maxWidth` columns. Returns array of line strings.
|
|
120
120
|
// Use visibleLength=true when words may contain ANSI escape codes.
|
|
121
|
-
function wrapWords(text, maxWidth, ansi = false) {
|
|
121
|
+
export function wrapWords(text, maxWidth, ansi = false) {
|
|
122
122
|
if (maxWidth < 20)
|
|
123
123
|
return [text];
|
|
124
124
|
const words = text.split(' ');
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { program } from 'commander';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import clipboard from 'clipboardy';
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
5
7
|
import { parseRefList, resolveBook } from './parser.js';
|
|
6
8
|
import { getVersesByRef, getSectionHeaders, getFootnotesForChapter, getFootnotesForVerses, getFootnote, getVerse, getBookInfo, getAllBooks, isInConcordance, getTopicVerses, searchFTS, } from './db.js';
|
|
7
9
|
import { renderVerses, renderVerseInline, renderFootnoteBlock, renderNoteDisplay, renderNoteDisplayAll, stripMarkers, highlightTerms, } from './format.js';
|
|
@@ -10,10 +12,12 @@ import { getLastRead, saveLastRead } from './state.js';
|
|
|
10
12
|
function stripAnsi(s) {
|
|
11
13
|
return s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
12
14
|
}
|
|
15
|
+
// Read version from package.json (single source of truth — avoids drift)
|
|
16
|
+
const pkg = JSON.parse(readFileSync(path.join(import.meta.dirname, '../package.json'), 'utf-8'));
|
|
13
17
|
program
|
|
14
18
|
.name('rv')
|
|
15
19
|
.description('Recovery Version Bible CLI')
|
|
16
|
-
.version(
|
|
20
|
+
.version(pkg.version)
|
|
17
21
|
.enablePositionalOptions();
|
|
18
22
|
// Formats a ParsedRef + book name into a display label: "John 3", "John 3:16", "John 3:16–18"
|
|
19
23
|
function formatRefLabel(ref, bookName) {
|
package/dist/ui/Pager.js
CHANGED
|
@@ -4,7 +4,7 @@ import { render, Box, Text, useInput, useApp, useStdout } from 'ink';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import clipboard from 'clipboardy';
|
|
6
6
|
import { getVersesByRef, getSectionHeaders, getFootnotesForChapter, getFootnotesForVerses, getCrossRefsForVerse, getBookInfo, getAllBooks, } from '../db.js';
|
|
7
|
-
import { renderVerses, stripMarkers, highlightTerms, toSuperscript, zoomIn, zoomOut, zoomReset, getZoom } from '../format.js';
|
|
7
|
+
import { renderVerses, stripMarkers, highlightTerms, toSuperscript, wrapWords, zoomIn, zoomOut, zoomReset, getZoom } from '../format.js';
|
|
8
8
|
import { saveLastRead, getLastRead } from '../state.js';
|
|
9
9
|
import { resolveBook } from '../parser.js';
|
|
10
10
|
import { getNextChapter, getPrevChapter } from './nav.js';
|
|
@@ -144,6 +144,7 @@ function Pager({ initialBook, initialChapter, initialNotes, initialOutline, star
|
|
|
144
144
|
const [studyCursorIdx, setStudyCursorIdx] = useState(0);
|
|
145
145
|
const [studySelStart, setStudySelStart] = useState(null);
|
|
146
146
|
const [studyRefInput, setStudyRefInput] = useState(''); // multi-digit ref number
|
|
147
|
+
const [panelScrollOffset, setPanelScrollOffset] = useState(0); // scroll within study panel when notes overflow cap
|
|
147
148
|
const studyActiveRef = useRef(false); // persists across navigate so study mode survives ref follows
|
|
148
149
|
const pendingVerseRef = useRef(null); // target verse to jump to after navigate
|
|
149
150
|
// Flash message
|
|
@@ -215,10 +216,13 @@ function Pager({ initialBook, initialChapter, initialNotes, initialOutline, star
|
|
|
215
216
|
const panelLines = [];
|
|
216
217
|
const verseLabel = `${bookName} ${chapter}:${v.verse}`;
|
|
217
218
|
panelLines.push(` ${chalk.bold(verseLabel)} ${chalk.dim(`— ${fns.length} note${fns.length === 1 ? '' : 's'}`)}`);
|
|
219
|
+
const BODY_INDENT = ' '; // 5 spaces — visually under the word, after marker
|
|
220
|
+
const bodyWidth = Math.max(20, cols - BODY_INDENT.length);
|
|
218
221
|
for (const fn of fns) {
|
|
219
222
|
// Extract the word the marker attaches to (first word after "Book ch:v word" in footnote text)
|
|
220
|
-
const fnTextParts = fn.text.split(/\s+/);
|
|
223
|
+
const fnTextParts = fn.text.trim().split(/\s+/);
|
|
221
224
|
const word = fnTextParts[2] ?? ''; // "Joh 3:16 loved ..." → "loved"
|
|
225
|
+
const noteBody = fnTextParts.slice(3).join(' ').trim(); // everything after "Book ch:v word"
|
|
222
226
|
const markerDisp = toSuperscript(fn.marker);
|
|
223
227
|
const refs = refsByMarker.get(fn.marker) ?? [];
|
|
224
228
|
// Deduplicate refs by target
|
|
@@ -230,42 +234,74 @@ function Pager({ initialBook, initialChapter, initialNotes, initialOutline, star
|
|
|
230
234
|
seen.add(key);
|
|
231
235
|
return true;
|
|
232
236
|
});
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
+
// Build numbered ref labels (mutates studyRefs)
|
|
238
|
+
const refLabels = [];
|
|
239
|
+
for (const r of uniqueRefs) {
|
|
240
|
+
const refNum = studyRefs.length + 1;
|
|
241
|
+
const tgtBookName = getBookInfo(r.tgt_book)?.abbr ?? r.tgt_book;
|
|
242
|
+
const label = `${tgtBookName} ${r.tgt_chapter}:${r.tgt_verse}`;
|
|
243
|
+
studyRefs.push({
|
|
244
|
+
num: refNum, marker: fn.marker,
|
|
245
|
+
tgt_book: r.tgt_book, tgt_chapter: r.tgt_chapter, tgt_verse: r.tgt_verse, label,
|
|
246
|
+
});
|
|
247
|
+
refLabels.push(`${chalk.yellow(`[${refNum}]`)} ${chalk.dim(label)}`);
|
|
237
248
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
num: refNum, marker: fn.marker,
|
|
247
|
-
tgt_book: r.tgt_book, tgt_chapter: r.tgt_chapter, tgt_verse: r.tgt_verse, label,
|
|
248
|
-
});
|
|
249
|
-
refLabels.push(`${chalk.yellow(`[${refNum}]`)} ${chalk.dim(label)}`);
|
|
249
|
+
// Header line: marker + word + refs (refs may be empty)
|
|
250
|
+
const refsPart = refLabels.length > 0 ? ` ${refLabels.join(' ')}` : '';
|
|
251
|
+
panelLines.push(` ${chalk.cyan(markerDisp)} ${chalk.dim(word)}${refsPart}`);
|
|
252
|
+
// Body: wrap full note text under hanging indent, dim style. No truncation.
|
|
253
|
+
if (noteBody) {
|
|
254
|
+
const wrapped = wrapWords(noteBody, bodyWidth);
|
|
255
|
+
for (const wl of wrapped) {
|
|
256
|
+
panelLines.push(`${BODY_INDENT}${chalk.dim(wl)}`);
|
|
250
257
|
}
|
|
251
|
-
// Compact: marker + word + refs on one line (wrap if needed)
|
|
252
|
-
const hasStudyNote = fn.text.length > 80;
|
|
253
|
-
const noteHint = hasStudyNote ? chalk.dim(' (note)') : '';
|
|
254
|
-
panelLines.push(` ${chalk.cyan(markerDisp)} ${chalk.dim(word)}${noteHint} ${refLabels.join(' ')}`);
|
|
255
258
|
}
|
|
256
259
|
}
|
|
257
260
|
return { lines: panelLines, refs: studyRefs };
|
|
258
|
-
}, [mode, studyCursorIdx, verseMap, verses, book, chapter, bookName]);
|
|
261
|
+
}, [mode, studyCursorIdx, verseMap, verses, book, chapter, bookName, cols]);
|
|
262
|
+
// ── Study panel sizing & scroll (cap at 50% of screen, scrollable on overflow) ──
|
|
263
|
+
const panelLinesAll = studyPanel.lines;
|
|
264
|
+
const panelMaxH = Math.max(3, Math.floor(rows * 0.5));
|
|
265
|
+
const panelOverflow = panelLinesAll.length > panelMaxH;
|
|
266
|
+
const panelContentSlots = panelOverflow ? panelMaxH - 1 : panelLinesAll.length; // reserve 1 line for indicator
|
|
267
|
+
const maxPanelScroll = Math.max(0, panelLinesAll.length - panelContentSlots);
|
|
268
|
+
const panelScrollClamped = Math.max(0, Math.min(panelScrollOffset, maxPanelScroll));
|
|
269
|
+
const panelVisibleLines = panelLinesAll.slice(panelScrollClamped, panelScrollClamped + panelContentSlots);
|
|
270
|
+
const panelDisplayH = panelLinesAll.length > 0 ? panelVisibleLines.length + (panelOverflow ? 1 : 0) : 0;
|
|
259
271
|
// ── Viewport height (shrinks when study panel is visible) ────────────
|
|
260
|
-
const studyPanelH =
|
|
272
|
+
const studyPanelH = panelDisplayH > 0 ? panelDisplayH + 1 : 0; // +1 for separator above panel
|
|
261
273
|
const hasFooter2 = mode === 'normal' || mode === 'study';
|
|
262
274
|
const chromeH = 3 + (hasFooter2 ? 2 : 1) + studyPanelH; // header + sep + footer(s) + sep + panel
|
|
263
275
|
const viewportH = Math.max(1, rows - chromeH);
|
|
276
|
+
// Reset panel scroll when the verse cursor changes or when leaving study mode
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
setPanelScrollOffset(0);
|
|
279
|
+
}, [studyCursorIdx, book, chapter, mode]);
|
|
264
280
|
// Clamp scroll when content or viewport changes
|
|
265
281
|
const maxScroll = Math.max(0, lines.length - viewportH);
|
|
266
282
|
useEffect(() => {
|
|
267
283
|
setScrollOffset(prev => Math.min(prev, maxScroll));
|
|
268
284
|
}, [maxScroll]);
|
|
285
|
+
// Keep the study-mode cursor verse fully visible above the footnote panel.
|
|
286
|
+
// Triggers when cursor moves OR viewport shrinks (panel grew with longer notes).
|
|
287
|
+
useEffect(() => {
|
|
288
|
+
if (mode !== 'study' || verseMap.length === 0)
|
|
289
|
+
return;
|
|
290
|
+
const entry = verseMap[studyCursorIdx];
|
|
291
|
+
if (!entry)
|
|
292
|
+
return;
|
|
293
|
+
setScrollOffset(prev => {
|
|
294
|
+
// Cursor verse extends below the visible viewport — pull it in from the bottom
|
|
295
|
+
if (entry.endLine >= prev + viewportH) {
|
|
296
|
+
return Math.max(0, entry.endLine - viewportH + 1);
|
|
297
|
+
}
|
|
298
|
+
// Cursor verse starts above the visible viewport — pull it in from the top
|
|
299
|
+
if (entry.startLine < prev) {
|
|
300
|
+
return entry.startLine;
|
|
301
|
+
}
|
|
302
|
+
return prev;
|
|
303
|
+
});
|
|
304
|
+
}, [studyCursorIdx, viewportH, mode, verseMap]);
|
|
269
305
|
// ── Search highlights ────────────────────────────────────────────────────
|
|
270
306
|
const displayLines = useMemo(() => {
|
|
271
307
|
if (!searchSubmitted || searchMatches.length === 0)
|
|
@@ -288,6 +324,20 @@ function Pager({ initialBook, initialChapter, initialNotes, initialOutline, star
|
|
|
288
324
|
setStudyCursorIdx(0);
|
|
289
325
|
}, []);
|
|
290
326
|
const navigate = useCallback((newBook, newChapter, pushHistory = true) => {
|
|
327
|
+
const sameChapter = newBook === bookRef.current && newChapter === chapterRef.current;
|
|
328
|
+
// Same-chapter cross-ref follow: jump cursor directly. Don't reset state via
|
|
329
|
+
// restoreMode (would clobber cursor to 0), don't push to history (cursor isn't tracked there).
|
|
330
|
+
if (sameChapter && pendingVerseRef.current) {
|
|
331
|
+
const target = pendingVerseRef.current;
|
|
332
|
+
pendingVerseRef.current = null;
|
|
333
|
+
let idx = verseMap.findIndex(e => e.verse === target);
|
|
334
|
+
if (idx < 0)
|
|
335
|
+
idx = verseMap.findIndex(e => e.verse.startsWith(target));
|
|
336
|
+
if (idx >= 0)
|
|
337
|
+
setStudyCursorIdx(idx);
|
|
338
|
+
saveLastRead(newBook, newChapter);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
291
341
|
if (pushHistory) {
|
|
292
342
|
historyRef.current = [...historyRef.current, { book: bookRef.current, chapter: chapterRef.current, scroll: scrollRef.current }];
|
|
293
343
|
forwardRef.current = [];
|
|
@@ -297,7 +347,7 @@ function Pager({ initialBook, initialChapter, initialNotes, initialOutline, star
|
|
|
297
347
|
setScrollOffset(0);
|
|
298
348
|
restoreMode();
|
|
299
349
|
saveLastRead(newBook, newChapter);
|
|
300
|
-
}, [restoreMode]);
|
|
350
|
+
}, [restoreMode, verseMap]);
|
|
301
351
|
const goBack = useCallback(() => {
|
|
302
352
|
const hist = historyRef.current;
|
|
303
353
|
if (hist.length === 0)
|
|
@@ -342,6 +392,11 @@ function Pager({ initialBook, initialChapter, initialNotes, initialOutline, star
|
|
|
342
392
|
const scrollUp = useCallback(() => scrollTo(scrollOffset - 1), [scrollOffset, scrollTo]);
|
|
343
393
|
const pageDown = useCallback(() => scrollTo(scrollOffset + viewportH - 2), [scrollOffset, viewportH, scrollTo]);
|
|
344
394
|
const pageUp = useCallback(() => scrollTo(scrollOffset - viewportH + 2), [scrollOffset, viewportH, scrollTo]);
|
|
395
|
+
// Quarter-of-chapter slider — 4 presses spans top to bottom regardless of length
|
|
396
|
+
const scrollQuarter = useCallback((dir) => {
|
|
397
|
+
const q = Math.max(1, Math.floor(lines.length * 0.25));
|
|
398
|
+
scrollTo(scrollOffset + dir * q);
|
|
399
|
+
}, [lines.length, scrollOffset, scrollTo]);
|
|
345
400
|
// ── Copy helpers ─────────────────────────────────────────────────────────
|
|
346
401
|
const doCopy = useCallback(async () => {
|
|
347
402
|
const lo = selStart !== null ? Math.min(selStart, cursorIdx) : cursorIdx;
|
|
@@ -598,26 +653,23 @@ function Pager({ initialBook, initialChapter, initialNotes, initialOutline, star
|
|
|
598
653
|
return;
|
|
599
654
|
}
|
|
600
655
|
if (input === 'j' || key.downArrow) {
|
|
601
|
-
setStudyCursorIdx(prev =>
|
|
602
|
-
const next = Math.min(prev + 1, verseMap.length - 1);
|
|
603
|
-
const entry = verseMap[next];
|
|
604
|
-
if (entry && entry.startLine >= scrollOffset + viewportH) {
|
|
605
|
-
scrollTo(entry.startLine - viewportH + 3);
|
|
606
|
-
}
|
|
607
|
-
return next;
|
|
608
|
-
});
|
|
656
|
+
setStudyCursorIdx(prev => Math.min(prev + 1, verseMap.length - 1));
|
|
609
657
|
setStudyRefInput('');
|
|
610
658
|
return;
|
|
611
659
|
}
|
|
612
660
|
if (input === 'k' || key.upArrow) {
|
|
613
|
-
setStudyCursorIdx(prev =>
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
661
|
+
setStudyCursorIdx(prev => Math.max(prev - 1, 0));
|
|
662
|
+
setStudyRefInput('');
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
// Cursor to first / last verse (cursor-visible effect handles scroll)
|
|
666
|
+
if (input === 'g') {
|
|
667
|
+
setStudyCursorIdx(0);
|
|
668
|
+
setStudyRefInput('');
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
if (input === 'G') {
|
|
672
|
+
setStudyCursorIdx(Math.max(0, verseMap.length - 1));
|
|
621
673
|
setStudyRefInput('');
|
|
622
674
|
return;
|
|
623
675
|
}
|
|
@@ -632,6 +684,23 @@ function Pager({ initialBook, initialChapter, initialNotes, initialOutline, star
|
|
|
632
684
|
setZoomState(getZoom());
|
|
633
685
|
return;
|
|
634
686
|
}
|
|
687
|
+
// Quarter-of-chapter cursor jump (cursor-visible effect handles scroll)
|
|
688
|
+
if (key.tab) {
|
|
689
|
+
const step = Math.max(1, Math.floor(verseMap.length / 4));
|
|
690
|
+
const dir = key.shift ? -1 : 1;
|
|
691
|
+
setStudyCursorIdx(prev => Math.max(0, Math.min(verseMap.length - 1, prev + dir * step)));
|
|
692
|
+
setStudyRefInput('');
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
// Panel scroll (only meaningful when notes panel overflows)
|
|
696
|
+
if (input === ',') {
|
|
697
|
+
setPanelScrollOffset(prev => Math.max(0, prev - 1));
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
if (input === '.') {
|
|
701
|
+
setPanelScrollOffset(prev => Math.min(maxPanelScroll, prev + 1));
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
635
704
|
// Number keys: build ref number, then follow on Enter or after short delay
|
|
636
705
|
if (input && /^[0-9]$/.test(input)) {
|
|
637
706
|
const newInput = studyRefInput + input;
|
|
@@ -786,6 +855,14 @@ function Pager({ initialBook, initialChapter, initialNotes, initialOutline, star
|
|
|
786
855
|
scrollTo(maxScroll);
|
|
787
856
|
return;
|
|
788
857
|
}
|
|
858
|
+
if (key.tab && key.shift) {
|
|
859
|
+
scrollQuarter(-1);
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
if (key.tab) {
|
|
863
|
+
scrollQuarter(1);
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
789
866
|
if (input === 'n' || key.rightArrow) {
|
|
790
867
|
if (searchSubmitted && searchMatches.length > 0) {
|
|
791
868
|
nextMatch();
|
|
@@ -1011,7 +1088,8 @@ function Pager({ initialBook, initialChapter, initialNotes, initialOutline, star
|
|
|
1011
1088
|
const sNoteFlag = notes ? chalk.cyan('f notes') : chalk.dim('f notes');
|
|
1012
1089
|
const sOutlineFlag = outline ? chalk.cyan('o outline') : chalk.dim('o outline');
|
|
1013
1090
|
footer1 = ` ${chalk.cyan('STUDY')}${inputHint}${rangeHint} ${chalk.dim('# ref · v range · c copy ·')} ${sNoteFlag} ${chalk.dim('·')} ${sOutlineFlag} ${chalk.dim('· / find')}`;
|
|
1014
|
-
|
|
1091
|
+
const panelHint = panelOverflow ? chalk.dim(' · ,/. note') : '';
|
|
1092
|
+
footer2 = ` ${chalk.dim('↑↓ verse · Tab/⇧Tab quarter · g/G top/bot · ←→ ch · : goto · [/] back/fwd · d exit')}${panelHint}`;
|
|
1015
1093
|
}
|
|
1016
1094
|
else if (mode === 'copy') {
|
|
1017
1095
|
const rangeHint = selStart !== null ? chalk.dim(' (range active)') : '';
|
|
@@ -1021,13 +1099,21 @@ function Pager({ initialBook, initialChapter, initialNotes, initialOutline, star
|
|
|
1021
1099
|
const noteFlag = notes ? chalk.cyan('f notes') : chalk.dim('f notes');
|
|
1022
1100
|
const outlineFlag = outline ? chalk.cyan('o outline') : chalk.dim('o outline');
|
|
1023
1101
|
footer1 = ` ${noteFlag} ${chalk.dim('·')} ${outlineFlag} ${chalk.dim('· d study · c copy · / find')}`;
|
|
1024
|
-
footer2 = ` ${chalk.dim('↑↓ scroll · ←→ ch · +/- width · : goto · H home · [/] back/fwd · q quit')}`;
|
|
1102
|
+
footer2 = ` ${chalk.dim('↑↓ scroll · Tab/⇧Tab quarter · g/G top/bot · ←→ ch · +/- width · : goto · H home · [/] back/fwd · q quit')}`;
|
|
1025
1103
|
}
|
|
1026
1104
|
const separator = chalk.dim('· '.repeat(Math.floor(cols / 2)));
|
|
1027
1105
|
// ── Render ───────────────────────────────────────────────────────────────
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1106
|
+
let studyPanelContent = null;
|
|
1107
|
+
if (panelLinesAll.length > 0) {
|
|
1108
|
+
const parts = [panelVisibleLines.join('\n')];
|
|
1109
|
+
if (panelOverflow) {
|
|
1110
|
+
const lo = panelScrollClamped + 1;
|
|
1111
|
+
const hi = panelScrollClamped + panelVisibleLines.length;
|
|
1112
|
+
const total = panelLinesAll.length;
|
|
1113
|
+
parts.push(chalk.dim(` ── ${lo}–${hi} of ${total} · ,/. scroll ──`));
|
|
1114
|
+
}
|
|
1115
|
+
studyPanelContent = parts.join('\n');
|
|
1116
|
+
}
|
|
1031
1117
|
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsx(Text, { children: header }), _jsx(Text, { children: separator }), _jsx(Box, { flexGrow: 1, overflow: "hidden", children: _jsx(Text, { children: content }) }), _jsx(Text, { children: separator }), studyPanelContent && _jsx(Text, { children: studyPanelContent }), studyPanelContent && _jsx(Text, { children: separator }), _jsx(Text, { children: footer1 }), footer2 && _jsx(Text, { children: footer2 })] }));
|
|
1032
1118
|
}
|
|
1033
1119
|
// ── Launcher ─────────────────────────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rv-bible-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Fast, offline terminal Bible reader for the Recovery Version. Footnotes, cross-references, concordance search, interactive pager.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|