@readwise/cli 0.3.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.
@@ -0,0 +1,12 @@
1
+ import type { ToolDef } from "../config.js";
2
+ import { enterFullScreen, exitFullScreen } from "./term.js";
3
+ import { runApp } from "./app.js";
4
+
5
+ export async function startTui(tools: ToolDef[], token: string, authType: "oauth" | "token"): Promise<void> {
6
+ enterFullScreen();
7
+ try {
8
+ await runApp(tools);
9
+ } finally {
10
+ exitFullScreen();
11
+ }
12
+ }
@@ -0,0 +1,16 @@
1
+ export const LOGO = [
2
+ "╔════════════════════════╗",
3
+ "║░░░░░░░░░░░░░░░░░░░░░░░░║",
4
+ "║░░░░░░░░░░░░░░░░░░░░░░░░║",
5
+ "║░░░░░▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░║",
6
+ "║░░░░░░▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░║",
7
+ "║░░░░░░▓▓▓▓▓▓░░▓▓▓▓▓░░░░░║",
8
+ "║░░░░░░▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░║",
9
+ "║░░░░░░▓▓▓▓▓▓▓▓▓▓░░░░░░░░║",
10
+ "║░░░░░░▓▓▓▓▓░▓▓▓▓░░░░░░░░║",
11
+ "║░░░░░░▓▓▓▓▓░░▓▓▓▓▓░░░░░░║",
12
+ "║░░░░░▓▓▓▓▓▓▓░░▓▓▓▓▓▓░░░░║",
13
+ "║░░░░░░░░░░░░░░░░░░░░░░░░║",
14
+ "║░░░░░░░░░░░░░░░░░░░░░░░░║",
15
+ "╚════════════════════════╝",
16
+ ];
@@ -0,0 +1,151 @@
1
+ // Low-level terminal utilities for flicker-free full-screen rendering.
2
+ // Instead of clearing and rewriting (which blinks), we position the cursor
3
+ // at home and overwrite in-place — content is never erased before being replaced.
4
+
5
+ const ESC = "\x1b";
6
+
7
+ // --- ANSI helpers ---
8
+
9
+ export const style = {
10
+ bold: (s: string) => `${ESC}[1m${s}${ESC}[22m`,
11
+ dim: (s: string) => `${ESC}[2m${s}${ESC}[22m`,
12
+ inverse: (s: string) => `${ESC}[7m${s}${ESC}[27m`,
13
+ yellow: (s: string) => `${ESC}[33m${s}${ESC}[39m`,
14
+ red: (s: string) => `${ESC}[31m${s}${ESC}[39m`,
15
+ green: (s: string) => `${ESC}[32m${s}${ESC}[39m`,
16
+ cyan: (s: string) => `${ESC}[36m${s}${ESC}[39m`,
17
+ boldYellow: (s: string) => `${ESC}[1;33m${s}${ESC}[22;39m`,
18
+ blue: (s: string) => `${ESC}[38;2;60;110;253m${s}${ESC}[39m`,
19
+ };
20
+
21
+ // --- Screen control ---
22
+
23
+ export function enterFullScreen(): void {
24
+ process.stdout.write(`${ESC}[?1049h`); // alternate screen buffer
25
+ process.stdout.write(`${ESC}[?25l`); // hide cursor
26
+ process.stdout.write(`${ESC}[H`); // cursor home
27
+ }
28
+
29
+ export function exitFullScreen(): void {
30
+ process.stdout.write(`${ESC}[?25h`); // show cursor
31
+ process.stdout.write(`${ESC}[?1049l`); // restore screen buffer
32
+ }
33
+
34
+ /** Paint lines to terminal without flicker: cursor home → overwrite each line → clear remainder */
35
+ export function paint(lines: string[]): void {
36
+ const rows = process.stdout.rows ?? 24;
37
+ let out = `${ESC}[H`; // cursor home
38
+ const count = Math.min(lines.length, rows);
39
+ for (let i = 0; i < count; i++) {
40
+ out += lines[i] + `${ESC}[K\n`; // line content + clear to end of line
41
+ }
42
+ // Clear any remaining lines below content
43
+ if (count < rows) {
44
+ out += `${ESC}[J`; // clear from cursor to end of screen
45
+ }
46
+ process.stdout.write(out);
47
+ }
48
+
49
+ export function screenSize(): { cols: number; rows: number } {
50
+ return { cols: process.stdout.columns ?? 80, rows: process.stdout.rows ?? 24 };
51
+ }
52
+
53
+ // --- Keyboard input ---
54
+
55
+ export interface KeyEvent {
56
+ raw: string;
57
+ name: string; // 'up', 'down', 'return', 'escape', 'tab', 'backspace', 'pageup', 'pagedown', or the character
58
+ shift: boolean;
59
+ ctrl: boolean;
60
+ }
61
+
62
+ export function parseKey(data: Buffer): KeyEvent {
63
+ const s = data.toString("utf-8");
64
+ const ctrl = s.length === 1 && s.charCodeAt(0) < 32;
65
+
66
+ // Escape sequences
67
+ if (s === `${ESC}[A`) return { raw: s, name: "up", shift: false, ctrl: false };
68
+ if (s === `${ESC}[B`) return { raw: s, name: "down", shift: false, ctrl: false };
69
+ if (s === `${ESC}[C`) return { raw: s, name: "right", shift: false, ctrl: false };
70
+ if (s === `${ESC}[D`) return { raw: s, name: "left", shift: false, ctrl: false };
71
+ if (s === `${ESC}[5~`) return { raw: s, name: "pageup", shift: false, ctrl: false };
72
+ if (s === `${ESC}[6~`) return { raw: s, name: "pagedown", shift: false, ctrl: false };
73
+ if (s === `${ESC}[Z`) return { raw: s, name: "tab", shift: true, ctrl: false };
74
+ if (s === ESC || s === `${ESC}${ESC}`) return { raw: s, name: "escape", shift: false, ctrl: false };
75
+
76
+ // Shift+Enter sequences
77
+ if (s === `${ESC}[13;2u`) return { raw: s, name: "return", shift: true, ctrl: false }; // CSI u / kitty
78
+ if (s === `${ESC}[27;2;13~`) return { raw: s, name: "return", shift: true, ctrl: false }; // xterm
79
+ if (s === `${ESC}OM`) return { raw: s, name: "return", shift: true, ctrl: false }; // misc terminals
80
+
81
+ // Single characters
82
+ if (s === "\r" || s === "\n") return { raw: s, name: "return", shift: false, ctrl: false };
83
+ if (s === "\t") return { raw: s, name: "tab", shift: false, ctrl: false };
84
+ if (s === "\x7f" || s === "\b") return { raw: s, name: "backspace", shift: false, ctrl: false };
85
+ if (s === "\x03") return { raw: s, name: "c", shift: false, ctrl: true }; // Ctrl+C
86
+ if (s === "\x04") return { raw: s, name: "d", shift: false, ctrl: true }; // Ctrl+D
87
+
88
+ if (ctrl) {
89
+ return { raw: s, name: String.fromCharCode(s.charCodeAt(0) + 96), shift: false, ctrl: true };
90
+ }
91
+
92
+ return { raw: s, name: s, shift: false, ctrl: false };
93
+ }
94
+
95
+ /** Strip ANSI escape codes to get visible character count */
96
+ export function stripAnsi(s: string): string {
97
+ return s.replace(/\x1b\[[0-9;]*m/g, "");
98
+ }
99
+
100
+ /** Skip `offset` visible characters, preserving ANSI state, then return the rest */
101
+ export function ansiSlice(s: string, offset: number): string {
102
+ if (offset <= 0) return s;
103
+ // Collect ANSI sequences encountered while skipping so we can replay them
104
+ let activeAnsi = "";
105
+ let count = 0;
106
+ let i = 0;
107
+ while (i < s.length && count < offset) {
108
+ if (s[i] === "\x1b") {
109
+ const end = s.indexOf("m", i);
110
+ if (end >= 0) {
111
+ activeAnsi += s.slice(i, end + 1);
112
+ i = end + 1;
113
+ continue;
114
+ }
115
+ }
116
+ count++;
117
+ i++;
118
+ }
119
+ // Consume any ANSI codes right at the boundary
120
+ while (i < s.length && s[i] === "\x1b") {
121
+ const end = s.indexOf("m", i);
122
+ if (end >= 0) {
123
+ activeAnsi += s.slice(i, end + 1);
124
+ i = end + 1;
125
+ } else break;
126
+ }
127
+ return activeAnsi + s.slice(i);
128
+ }
129
+
130
+ /** Pad/truncate a string to a visible width (ANSI-aware) */
131
+ export function fitWidth(s: string, width: number): string {
132
+ const visible = stripAnsi(s);
133
+ if (visible.length >= width) {
134
+ // Truncate — need to be careful with ANSI codes
135
+ let count = 0;
136
+ let i = 0;
137
+ while (i < s.length && count < width) {
138
+ if (s[i] === "\x1b") {
139
+ const end = s.indexOf("m", i);
140
+ if (end >= 0) { i = end + 1; continue; }
141
+ }
142
+ count++;
143
+ i++;
144
+ }
145
+ // Include any trailing ANSI reset codes
146
+ const rest = s.slice(i);
147
+ const resets = rest.match(/^(\x1b\[[0-9;]*m)*/)?.[0] || "";
148
+ return s.slice(0, i) + resets;
149
+ }
150
+ return s + " ".repeat(width - visible.length);
151
+ }
package/src/version.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { createRequire } from "node:module";
2
+ const require = createRequire(import.meta.url);
3
+ const pkg = require("../package.json") as { version: string };
4
+ export const VERSION: string = pkg.version;
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "declaration": true
12
+ },
13
+ "include": ["src/**/*"]
14
+ }