duckdb-terminal 0.1.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 +505 -0
- package/dist/duckdb-terminal.js +3407 -0
- package/dist/duckdb-terminal.umd.cjs +29 -0
- package/dist/lib.d.ts +2194 -0
- package/package.json +58 -0
|
@@ -0,0 +1,3407 @@
|
|
|
1
|
+
var st = Object.defineProperty;
|
|
2
|
+
var rt = (i, t, e) => t in i ? st(i, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : i[t] = e;
|
|
3
|
+
var a = (i, t, e) => rt(i, typeof t != "symbol" ? t + "" : t, e);
|
|
4
|
+
import { Ghostty as nt, Terminal as ot, FitAddon as at } from "ghostty-web";
|
|
5
|
+
import * as R from "@duckdb/duckdb-wasm";
|
|
6
|
+
const lt = "https://cdn.jsdelivr.net/npm/ghostty-web", ht = () => `${lt}@0.3.0/dist/ghostty-vt.wasm`;
|
|
7
|
+
class ct {
|
|
8
|
+
constructor() {
|
|
9
|
+
a(this, "terminal");
|
|
10
|
+
a(this, "fitAddon");
|
|
11
|
+
a(this, "ghostty");
|
|
12
|
+
a(this, "dataHandler");
|
|
13
|
+
a(this, "resizeHandler");
|
|
14
|
+
a(this, "currentTheme", null);
|
|
15
|
+
a(this, "initialized", !1);
|
|
16
|
+
a(this, "container", null);
|
|
17
|
+
a(this, "options", {});
|
|
18
|
+
a(this, "resizeObserver", null);
|
|
19
|
+
a(this, "mobileInput", null);
|
|
20
|
+
/**
|
|
21
|
+
* Handles window resize events.
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
a(this, "handleResize", () => {
|
|
25
|
+
this.fit();
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Initializes the Ghostty terminal and mounts it to the container.
|
|
30
|
+
*
|
|
31
|
+
* This method:
|
|
32
|
+
* 1. Initializes the Ghostty WASM module
|
|
33
|
+
* 2. Creates the terminal with the specified options
|
|
34
|
+
* 3. Loads the FitAddon for auto-resizing
|
|
35
|
+
* 4. Mounts the terminal to the container
|
|
36
|
+
* 5. Sets up resize observers
|
|
37
|
+
*
|
|
38
|
+
* @param container - The HTML element to mount the terminal into
|
|
39
|
+
* @param options - Configuration options for the terminal
|
|
40
|
+
* @returns A promise that resolves when initialization is complete
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* const adapter = new TerminalAdapter();
|
|
45
|
+
* await adapter.init(document.getElementById('terminal'), {
|
|
46
|
+
* fontSize: 14,
|
|
47
|
+
* fontFamily: 'JetBrains Mono',
|
|
48
|
+
* theme: darkTheme,
|
|
49
|
+
* });
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
async init(t, e = {}) {
|
|
53
|
+
this.initialized || (this.container = t, this.options = e, this.currentTheme = e.theme ?? null, this.ghostty = await nt.load(ht()), this.createTerminal(), this.initialized = !0, this.focus());
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Creates or recreates the terminal instance.
|
|
57
|
+
* @internal
|
|
58
|
+
*/
|
|
59
|
+
createTerminal() {
|
|
60
|
+
this.container && (this.terminal = new ot({
|
|
61
|
+
ghostty: this.ghostty,
|
|
62
|
+
cursorBlink: !0,
|
|
63
|
+
fontSize: this.options.fontSize ?? 14,
|
|
64
|
+
fontFamily: this.options.fontFamily ?? '"Fira Code", "Cascadia Code", "JetBrains Mono", Consolas, monospace',
|
|
65
|
+
theme: this.currentTheme ? this.themeToGhostty(this.currentTheme) : void 0,
|
|
66
|
+
// Scrollback is in bytes, not lines. 10MB is a reasonable default.
|
|
67
|
+
scrollback: this.options.scrollback ?? 10 * 1024 * 1024
|
|
68
|
+
}), this.fitAddon = new at(), this.terminal.loadAddon(this.fitAddon), this.container.innerHTML = "", this.terminal.open(this.container), this.fit(), window.addEventListener("resize", this.handleResize), typeof ResizeObserver < "u" && (this.resizeObserver?.disconnect(), this.resizeObserver = new ResizeObserver(() => {
|
|
69
|
+
this.fit();
|
|
70
|
+
}), this.resizeObserver.observe(this.container)), this.terminal.onData((t) => {
|
|
71
|
+
this.dataHandler?.(t);
|
|
72
|
+
}), this.terminal.onResize(({ cols: t, rows: e }) => {
|
|
73
|
+
this.resizeHandler?.(t, e);
|
|
74
|
+
}), this.setupMobileInput());
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Sets up mobile-specific functionality: touch scrolling and keyboard input.
|
|
78
|
+
* @internal
|
|
79
|
+
*/
|
|
80
|
+
setupMobileInput() {
|
|
81
|
+
this.container && this.isTouchDevice() && (this.setupTouchScrolling(), this.setupMobileKeyboard());
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Creates a hidden textarea for mobile keyboard input.
|
|
85
|
+
* Appended to document.body to avoid interfering with terminal touch events.
|
|
86
|
+
* @internal
|
|
87
|
+
*/
|
|
88
|
+
setupMobileKeyboard() {
|
|
89
|
+
this.mobileInput?.remove();
|
|
90
|
+
const t = document.createElement("textarea");
|
|
91
|
+
t.className = "mobile-keyboard-input", t.setAttribute("autocapitalize", "off"), t.setAttribute("autocomplete", "off"), t.setAttribute("autocorrect", "off"), t.setAttribute("spellcheck", "false"), t.setAttribute("enterkeyhint", "send"), t.setAttribute("aria-label", "Terminal input"), t.style.cssText = `
|
|
92
|
+
position: fixed;
|
|
93
|
+
bottom: 0;
|
|
94
|
+
left: 50%;
|
|
95
|
+
transform: translateX(-50%);
|
|
96
|
+
width: 1px;
|
|
97
|
+
height: 1px;
|
|
98
|
+
opacity: 0;
|
|
99
|
+
font-size: 16px;
|
|
100
|
+
border: none;
|
|
101
|
+
outline: none;
|
|
102
|
+
resize: none;
|
|
103
|
+
background: transparent;
|
|
104
|
+
color: transparent;
|
|
105
|
+
z-index: -1;
|
|
106
|
+
`, t.addEventListener("input", () => {
|
|
107
|
+
const e = t.value;
|
|
108
|
+
e && this.dataHandler && this.dataHandler(e), t.value = "";
|
|
109
|
+
}), t.addEventListener("keydown", (e) => {
|
|
110
|
+
e.key.length === 1 && !e.ctrlKey && !e.altKey && !e.metaKey || (e.key === "Enter" ? (e.preventDefault(), this.dataHandler?.("\r"), t.value = "", t.blur()) : e.key === "Backspace" ? (e.preventDefault(), this.dataHandler?.("")) : e.key === "Tab" ? (e.preventDefault(), this.dataHandler?.(" ")) : e.key === "ArrowUp" ? (e.preventDefault(), this.dataHandler?.("\x1B[A")) : e.key === "ArrowDown" ? (e.preventDefault(), this.dataHandler?.("\x1B[B")) : e.key === "ArrowLeft" ? (e.preventDefault(), this.dataHandler?.("\x1B[D")) : e.key === "ArrowRight" && (e.preventDefault(), this.dataHandler?.("\x1B[C")));
|
|
111
|
+
}), document.body.appendChild(t), this.mobileInput = t;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Sets up touch-based scrolling for the terminal.
|
|
115
|
+
* Translates touch gestures into scroll commands.
|
|
116
|
+
* @internal
|
|
117
|
+
*/
|
|
118
|
+
setupTouchScrolling() {
|
|
119
|
+
if (!this.container) return;
|
|
120
|
+
let t = 0, e = 0, s = !1;
|
|
121
|
+
const n = (l) => {
|
|
122
|
+
l.touches.length === 1 && (t = l.touches[0].clientY, e = t, s = !1);
|
|
123
|
+
}, r = (l) => {
|
|
124
|
+
if (l.touches.length !== 1) return;
|
|
125
|
+
const h = l.touches[0].clientY, d = e - h;
|
|
126
|
+
if (!s && Math.abs(h - t) > 10 && (s = !0), s) {
|
|
127
|
+
l.preventDefault();
|
|
128
|
+
const f = Math.round(d / 20);
|
|
129
|
+
f !== 0 && this.terminal && this.terminal.scrollLines?.(f), e = h;
|
|
130
|
+
}
|
|
131
|
+
}, o = () => {
|
|
132
|
+
s = !1;
|
|
133
|
+
};
|
|
134
|
+
this.container.addEventListener("touchstart", n, { passive: !0 }), this.container.addEventListener("touchmove", r, { passive: !1 }), this.container.addEventListener("touchend", o, { passive: !0 });
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Converts a Theme object to Ghostty's theme format.
|
|
138
|
+
*
|
|
139
|
+
* @internal
|
|
140
|
+
* @param theme - The theme to convert
|
|
141
|
+
* @returns The theme in Ghostty's expected format
|
|
142
|
+
*/
|
|
143
|
+
themeToGhostty(t) {
|
|
144
|
+
return {
|
|
145
|
+
background: t.colors.background,
|
|
146
|
+
foreground: t.colors.foreground,
|
|
147
|
+
cursor: t.colors.cursor,
|
|
148
|
+
black: t.colors.black,
|
|
149
|
+
red: t.colors.red,
|
|
150
|
+
green: t.colors.green,
|
|
151
|
+
yellow: t.colors.yellow,
|
|
152
|
+
blue: t.colors.blue,
|
|
153
|
+
magenta: t.colors.magenta,
|
|
154
|
+
cyan: t.colors.cyan,
|
|
155
|
+
white: t.colors.white,
|
|
156
|
+
brightBlack: t.colors.brightBlack,
|
|
157
|
+
brightRed: t.colors.brightRed,
|
|
158
|
+
brightGreen: t.colors.brightGreen,
|
|
159
|
+
brightYellow: t.colors.brightYellow,
|
|
160
|
+
brightBlue: t.colors.brightBlue,
|
|
161
|
+
brightMagenta: t.colors.brightMagenta,
|
|
162
|
+
brightCyan: t.colors.brightCyan,
|
|
163
|
+
brightWhite: t.colors.brightWhite
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Fits the terminal to its container dimensions.
|
|
168
|
+
*
|
|
169
|
+
* This method should be called when the container size changes.
|
|
170
|
+
* It's automatically called on window resize, but you may need
|
|
171
|
+
* to call it manually after dynamic layout changes.
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```typescript
|
|
175
|
+
* // After changing container size
|
|
176
|
+
* container.style.height = '500px';
|
|
177
|
+
* adapter.fit();
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
180
|
+
fit() {
|
|
181
|
+
this.fitAddon && this.fitAddon.fit();
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Writes text to the terminal without a trailing newline.
|
|
185
|
+
*
|
|
186
|
+
* @param text - The text to write
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```typescript
|
|
190
|
+
* adapter.write('Hello');
|
|
191
|
+
* adapter.write(' World');
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
write(t) {
|
|
195
|
+
this.terminal?.write(t);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Writes text to the terminal followed by a newline.
|
|
199
|
+
*
|
|
200
|
+
* @param text - The text to write
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* ```typescript
|
|
204
|
+
* adapter.writeln('First line');
|
|
205
|
+
* adapter.writeln('Second line');
|
|
206
|
+
* ```
|
|
207
|
+
*/
|
|
208
|
+
writeln(t) {
|
|
209
|
+
this.terminal?.writeln(t);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Clears the terminal screen and moves cursor to top-left.
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* ```typescript
|
|
216
|
+
* adapter.clear();
|
|
217
|
+
* adapter.writeln('Fresh start!');
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
clear() {
|
|
221
|
+
this.terminal?.write("\x1B[2J\x1B[H");
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Gives keyboard focus to the terminal.
|
|
225
|
+
* On mobile devices, focuses the hidden input to trigger the virtual keyboard.
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```typescript
|
|
229
|
+
* adapter.focus();
|
|
230
|
+
* ```
|
|
231
|
+
*/
|
|
232
|
+
focus() {
|
|
233
|
+
this.mobileInput && this.isTouchDevice() ? this.mobileInput.focus() : this.terminal?.focus();
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Checks if the current device supports touch input.
|
|
237
|
+
* @internal
|
|
238
|
+
*/
|
|
239
|
+
isTouchDevice() {
|
|
240
|
+
return "ontouchstart" in window || navigator.maxTouchPoints > 0;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Registers a callback to receive user input data.
|
|
244
|
+
*
|
|
245
|
+
* The callback is invoked whenever the user types in the terminal.
|
|
246
|
+
* This includes regular characters, control sequences, and escape codes.
|
|
247
|
+
*
|
|
248
|
+
* @param handler - The callback function to handle input data
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* ```typescript
|
|
252
|
+
* adapter.onData((data) => {
|
|
253
|
+
* if (data === '\r') {
|
|
254
|
+
* console.log('User pressed Enter');
|
|
255
|
+
* } else {
|
|
256
|
+
* console.log('User typed:', data);
|
|
257
|
+
* }
|
|
258
|
+
* });
|
|
259
|
+
* ```
|
|
260
|
+
*/
|
|
261
|
+
onData(t) {
|
|
262
|
+
this.dataHandler = t;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Registers a callback to receive terminal resize events.
|
|
266
|
+
*
|
|
267
|
+
* @param handler - The callback function receiving new dimensions
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* ```typescript
|
|
271
|
+
* adapter.onResize((cols, rows) => {
|
|
272
|
+
* console.log(`Terminal resized to ${cols}x${rows}`);
|
|
273
|
+
* });
|
|
274
|
+
* ```
|
|
275
|
+
*/
|
|
276
|
+
onResize(t) {
|
|
277
|
+
this.resizeHandler = t;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* The number of columns (characters per line) in the terminal.
|
|
281
|
+
*
|
|
282
|
+
* @returns The current column count, or 80 if not initialized
|
|
283
|
+
*/
|
|
284
|
+
get cols() {
|
|
285
|
+
return this.terminal?.cols ?? 80;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* The number of rows (lines) in the terminal.
|
|
289
|
+
*
|
|
290
|
+
* @returns The current row count, or 24 if not initialized
|
|
291
|
+
*/
|
|
292
|
+
get rows() {
|
|
293
|
+
return this.terminal?.rows ?? 24;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Sets the terminal color theme.
|
|
297
|
+
*
|
|
298
|
+
* Since Ghostty-web doesn't fully support runtime theme changes,
|
|
299
|
+
* this method recreates the terminal with the new theme.
|
|
300
|
+
*
|
|
301
|
+
* @param theme - The theme to apply
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* ```typescript
|
|
305
|
+
* adapter.setTheme({
|
|
306
|
+
* name: 'dark',
|
|
307
|
+
* colors: {
|
|
308
|
+
* background: '#1e1e1e',
|
|
309
|
+
* foreground: '#d4d4d4',
|
|
310
|
+
* // ... other colors
|
|
311
|
+
* },
|
|
312
|
+
* });
|
|
313
|
+
* ```
|
|
314
|
+
*/
|
|
315
|
+
setTheme(t) {
|
|
316
|
+
this.currentTheme = t, !(!this.initialized || !this.container) && (window.removeEventListener("resize", this.handleResize), this.terminal?.dispose(), this.createTerminal(), this.focus());
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Gets the current terminal theme.
|
|
320
|
+
*
|
|
321
|
+
* @returns The current theme, or null if no theme is set
|
|
322
|
+
*/
|
|
323
|
+
getTheme() {
|
|
324
|
+
return this.currentTheme;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Disposes of the terminal and cleans up resources.
|
|
328
|
+
*
|
|
329
|
+
* After calling this method, the adapter cannot be used again.
|
|
330
|
+
* Create a new instance if you need another terminal.
|
|
331
|
+
*
|
|
332
|
+
* @example
|
|
333
|
+
* ```typescript
|
|
334
|
+
* adapter.dispose();
|
|
335
|
+
* // adapter is no longer usable
|
|
336
|
+
* ```
|
|
337
|
+
*/
|
|
338
|
+
dispose() {
|
|
339
|
+
window.removeEventListener("resize", this.handleResize), this.resizeObserver?.disconnect(), this.resizeObserver = null, this.mobileInput?.remove(), this.mobileInput = null, this.terminal?.dispose(), this.initialized = !1, this.container = null;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
class ut {
|
|
343
|
+
constructor(t = {}) {
|
|
344
|
+
a(this, "db", null);
|
|
345
|
+
a(this, "conn", null);
|
|
346
|
+
a(this, "worker", null);
|
|
347
|
+
a(this, "initialized", !1);
|
|
348
|
+
a(this, "options");
|
|
349
|
+
this.options = {
|
|
350
|
+
storage: "memory",
|
|
351
|
+
databasePath: ":memory:",
|
|
352
|
+
...t
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Initializes the DuckDB database.
|
|
357
|
+
*
|
|
358
|
+
* This method performs the following setup:
|
|
359
|
+
* 1. Downloads the appropriate DuckDB WASM bundle
|
|
360
|
+
* 2. Creates a Web Worker for database operations
|
|
361
|
+
* 3. Instantiates the DuckDB instance
|
|
362
|
+
* 4. Opens the database (in-memory or OPFS-backed)
|
|
363
|
+
* 5. Creates a connection for query execution
|
|
364
|
+
*
|
|
365
|
+
* @returns A promise that resolves when initialization is complete
|
|
366
|
+
*
|
|
367
|
+
* @throws Error if initialization fails (network issues, WASM loading, etc.)
|
|
368
|
+
*
|
|
369
|
+
* @example
|
|
370
|
+
* ```typescript
|
|
371
|
+
* const db = new Database();
|
|
372
|
+
* await db.init();
|
|
373
|
+
* console.log('Database ready:', db.isReady());
|
|
374
|
+
* ```
|
|
375
|
+
*/
|
|
376
|
+
async init() {
|
|
377
|
+
if (this.initialized)
|
|
378
|
+
return;
|
|
379
|
+
const t = R.getJsDelivrBundles(), e = await R.selectBundle(t), s = URL.createObjectURL(
|
|
380
|
+
new Blob([`importScripts("${e.mainWorker}");`], {
|
|
381
|
+
type: "text/javascript"
|
|
382
|
+
})
|
|
383
|
+
);
|
|
384
|
+
this.worker = new Worker(s);
|
|
385
|
+
const n = new R.ConsoleLogger();
|
|
386
|
+
this.db = new R.AsyncDuckDB(n, this.worker), await this.db.instantiate(e.mainModule, e.pthreadWorker), URL.revokeObjectURL(s);
|
|
387
|
+
const r = {
|
|
388
|
+
castDecimalToDouble: !0
|
|
389
|
+
};
|
|
390
|
+
this.options.storage === "opfs" && this.options.databasePath ? await this.db.open({
|
|
391
|
+
path: this.options.databasePath,
|
|
392
|
+
accessMode: R.DuckDBAccessMode.READ_WRITE,
|
|
393
|
+
query: r
|
|
394
|
+
}) : await this.db.open({
|
|
395
|
+
path: ":memory:",
|
|
396
|
+
query: r
|
|
397
|
+
}), this.conn = await this.db.connect(), this.initialized = !0;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Executes a SQL query and returns the results.
|
|
401
|
+
*
|
|
402
|
+
* @param sql - The SQL statement to execute
|
|
403
|
+
* @returns A promise that resolves to the query result containing columns, rows, row count, and duration
|
|
404
|
+
*
|
|
405
|
+
* @throws Error if the database is not initialized
|
|
406
|
+
* @throws Error if the SQL query is invalid or fails
|
|
407
|
+
*
|
|
408
|
+
* @example SELECT query
|
|
409
|
+
* ```typescript
|
|
410
|
+
* const result = await db.executeQuery('SELECT * FROM users WHERE age > 18;');
|
|
411
|
+
* console.log('Columns:', result.columns); // ['id', 'name', 'age']
|
|
412
|
+
* console.log('Rows:', result.rows); // [[1, 'Alice', 25], [2, 'Bob', 30]]
|
|
413
|
+
* console.log('Duration:', result.duration); // 5.23 (milliseconds)
|
|
414
|
+
* ```
|
|
415
|
+
*
|
|
416
|
+
* @example DDL statement
|
|
417
|
+
* ```typescript
|
|
418
|
+
* await db.executeQuery('CREATE TABLE products (id INTEGER, name VARCHAR);');
|
|
419
|
+
* ```
|
|
420
|
+
*/
|
|
421
|
+
async executeQuery(t) {
|
|
422
|
+
if (!this.conn)
|
|
423
|
+
throw new Error("Database not initialized");
|
|
424
|
+
const e = performance.now(), s = await this.conn.query(t), n = performance.now() - e, r = s.schema.fields.map((h) => h.name), o = [];
|
|
425
|
+
for (let h = 0; h < s.numRows; h++) {
|
|
426
|
+
const d = [];
|
|
427
|
+
for (let f = 0; f < r.length; f++) {
|
|
428
|
+
const m = s.getChildAt(f);
|
|
429
|
+
d.push(m?.get(h));
|
|
430
|
+
}
|
|
431
|
+
o.push(d);
|
|
432
|
+
}
|
|
433
|
+
const l = typeof s.numRows == "bigint" ? Number(s.numRows) : s.numRows;
|
|
434
|
+
return {
|
|
435
|
+
columns: r,
|
|
436
|
+
rows: o,
|
|
437
|
+
rowCount: l,
|
|
438
|
+
duration: n
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Gets auto-completion suggestions for the current input.
|
|
443
|
+
*
|
|
444
|
+
* Provides suggestions for:
|
|
445
|
+
* - SQL keywords (SELECT, FROM, WHERE, etc.)
|
|
446
|
+
* - Table names from the current database
|
|
447
|
+
* - Common SQL functions
|
|
448
|
+
*
|
|
449
|
+
* @param text - The current input text
|
|
450
|
+
* @param cursorPosition - The cursor position within the text
|
|
451
|
+
* @returns A promise that resolves to an array of completion suggestions
|
|
452
|
+
*
|
|
453
|
+
* @example
|
|
454
|
+
* ```typescript
|
|
455
|
+
* const suggestions = await db.getCompletions('SEL', 3);
|
|
456
|
+
* console.log(suggestions);
|
|
457
|
+
* // [{ value: 'SELECT', type: 'keyword' }]
|
|
458
|
+
* ```
|
|
459
|
+
*/
|
|
460
|
+
async getCompletions(t, e) {
|
|
461
|
+
if (!this.db)
|
|
462
|
+
return [];
|
|
463
|
+
try {
|
|
464
|
+
const n = t.substring(0, e).match(/[\w.]*$/), r = n ? n[0].toLowerCase() : "";
|
|
465
|
+
if (!r)
|
|
466
|
+
return [];
|
|
467
|
+
const o = [], l = [
|
|
468
|
+
"SELECT",
|
|
469
|
+
"FROM",
|
|
470
|
+
"WHERE",
|
|
471
|
+
"AND",
|
|
472
|
+
"OR",
|
|
473
|
+
"NOT",
|
|
474
|
+
"INSERT",
|
|
475
|
+
"INTO",
|
|
476
|
+
"VALUES",
|
|
477
|
+
"UPDATE",
|
|
478
|
+
"SET",
|
|
479
|
+
"DELETE",
|
|
480
|
+
"CREATE",
|
|
481
|
+
"TABLE",
|
|
482
|
+
"DROP",
|
|
483
|
+
"ALTER",
|
|
484
|
+
"INDEX",
|
|
485
|
+
"VIEW",
|
|
486
|
+
"JOIN",
|
|
487
|
+
"LEFT",
|
|
488
|
+
"RIGHT",
|
|
489
|
+
"INNER",
|
|
490
|
+
"OUTER",
|
|
491
|
+
"ON",
|
|
492
|
+
"GROUP",
|
|
493
|
+
"BY",
|
|
494
|
+
"ORDER",
|
|
495
|
+
"HAVING",
|
|
496
|
+
"LIMIT",
|
|
497
|
+
"OFFSET",
|
|
498
|
+
"UNION",
|
|
499
|
+
"EXCEPT",
|
|
500
|
+
"INTERSECT",
|
|
501
|
+
"AS",
|
|
502
|
+
"DISTINCT",
|
|
503
|
+
"ALL",
|
|
504
|
+
"NULL",
|
|
505
|
+
"TRUE",
|
|
506
|
+
"FALSE",
|
|
507
|
+
"CASE",
|
|
508
|
+
"WHEN",
|
|
509
|
+
"THEN",
|
|
510
|
+
"ELSE",
|
|
511
|
+
"END",
|
|
512
|
+
"IS",
|
|
513
|
+
"IN",
|
|
514
|
+
"BETWEEN",
|
|
515
|
+
"LIKE",
|
|
516
|
+
"EXISTS",
|
|
517
|
+
"COUNT",
|
|
518
|
+
"SUM",
|
|
519
|
+
"AVG",
|
|
520
|
+
"MIN",
|
|
521
|
+
"MAX",
|
|
522
|
+
"CAST",
|
|
523
|
+
"COALESCE",
|
|
524
|
+
"NULLIF"
|
|
525
|
+
];
|
|
526
|
+
for (const d of l)
|
|
527
|
+
d.toLowerCase().startsWith(r) && o.push({ value: d, type: "keyword" });
|
|
528
|
+
try {
|
|
529
|
+
const d = await this.executeQuery(
|
|
530
|
+
"SELECT table_name FROM information_schema.tables WHERE table_schema = 'main'"
|
|
531
|
+
);
|
|
532
|
+
for (const f of d.rows) {
|
|
533
|
+
const m = String(f[0]);
|
|
534
|
+
m.toLowerCase().startsWith(r) && o.push({ value: m, type: "table" });
|
|
535
|
+
}
|
|
536
|
+
} catch {
|
|
537
|
+
}
|
|
538
|
+
const h = [
|
|
539
|
+
"abs",
|
|
540
|
+
"ceil",
|
|
541
|
+
"floor",
|
|
542
|
+
"round",
|
|
543
|
+
"sqrt",
|
|
544
|
+
"log",
|
|
545
|
+
"exp",
|
|
546
|
+
"power",
|
|
547
|
+
"length",
|
|
548
|
+
"lower",
|
|
549
|
+
"upper",
|
|
550
|
+
"trim",
|
|
551
|
+
"ltrim",
|
|
552
|
+
"rtrim",
|
|
553
|
+
"replace",
|
|
554
|
+
"substring",
|
|
555
|
+
"concat",
|
|
556
|
+
"now",
|
|
557
|
+
"current_date",
|
|
558
|
+
"current_time",
|
|
559
|
+
"current_timestamp",
|
|
560
|
+
"date_part",
|
|
561
|
+
"date_trunc",
|
|
562
|
+
"extract",
|
|
563
|
+
"array_agg",
|
|
564
|
+
"string_agg",
|
|
565
|
+
"list_agg",
|
|
566
|
+
"first",
|
|
567
|
+
"last",
|
|
568
|
+
"any_value"
|
|
569
|
+
];
|
|
570
|
+
for (const d of h)
|
|
571
|
+
d.toLowerCase().startsWith(r) && o.push({ value: d, type: "function" });
|
|
572
|
+
return o.sort((d, f) => {
|
|
573
|
+
const m = d.value.toLowerCase() === r, P = f.value.toLowerCase() === r;
|
|
574
|
+
return m && !P ? -1 : !m && P ? 1 : d.value.localeCompare(f.value);
|
|
575
|
+
}), o.slice(0, 20);
|
|
576
|
+
} catch {
|
|
577
|
+
return [];
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Gets a list of all tables in the database.
|
|
582
|
+
*
|
|
583
|
+
* Queries the information_schema to retrieve table names from the 'main' schema.
|
|
584
|
+
*
|
|
585
|
+
* @returns A promise that resolves to an array of table names
|
|
586
|
+
*
|
|
587
|
+
* @example
|
|
588
|
+
* ```typescript
|
|
589
|
+
* const tables = await db.getTables();
|
|
590
|
+
* console.log('Tables:', tables); // ['users', 'products', 'orders']
|
|
591
|
+
* ```
|
|
592
|
+
*/
|
|
593
|
+
async getTables() {
|
|
594
|
+
if (!this.conn)
|
|
595
|
+
return [];
|
|
596
|
+
try {
|
|
597
|
+
return (await this.executeQuery(
|
|
598
|
+
"SELECT table_name FROM information_schema.tables WHERE table_schema = 'main' ORDER BY table_name"
|
|
599
|
+
)).rows.map((e) => String(e[0]));
|
|
600
|
+
} catch {
|
|
601
|
+
return [];
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Gets the schema (column definitions) for a specific table.
|
|
606
|
+
*
|
|
607
|
+
* @param tableName - The name of the table to get the schema for
|
|
608
|
+
* @returns A promise that resolves to an array of column definitions
|
|
609
|
+
*
|
|
610
|
+
* @example
|
|
611
|
+
* ```typescript
|
|
612
|
+
* const schema = await db.getTableSchema('users');
|
|
613
|
+
* console.log(schema);
|
|
614
|
+
* // [
|
|
615
|
+
* // { name: 'id', type: 'INTEGER' },
|
|
616
|
+
* // { name: 'name', type: 'VARCHAR' },
|
|
617
|
+
* // { name: 'email', type: 'VARCHAR' }
|
|
618
|
+
* // ]
|
|
619
|
+
* ```
|
|
620
|
+
*/
|
|
621
|
+
async getTableSchema(t) {
|
|
622
|
+
if (!this.conn)
|
|
623
|
+
return [];
|
|
624
|
+
try {
|
|
625
|
+
const e = t.replace(/'/g, "''");
|
|
626
|
+
return (await this.executeQuery(
|
|
627
|
+
`SELECT column_name, data_type FROM information_schema.columns WHERE table_name = '${e}' ORDER BY ordinal_position`
|
|
628
|
+
)).rows.map((n) => ({
|
|
629
|
+
name: String(n[0]),
|
|
630
|
+
type: String(n[1])
|
|
631
|
+
}));
|
|
632
|
+
} catch {
|
|
633
|
+
return [];
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Registers a file in DuckDB's virtual filesystem.
|
|
638
|
+
*
|
|
639
|
+
* This allows you to load external files (CSV, Parquet, JSON) into DuckDB
|
|
640
|
+
* and query them using functions like `read_csv()`, `read_parquet()`, etc.
|
|
641
|
+
*
|
|
642
|
+
* @param filename - The virtual filename to register (e.g., 'data.csv')
|
|
643
|
+
* @param data - The file contents as a Uint8Array
|
|
644
|
+
*
|
|
645
|
+
* @throws Error if the database is not initialized
|
|
646
|
+
*
|
|
647
|
+
* @example
|
|
648
|
+
* ```typescript
|
|
649
|
+
* // Load a CSV file
|
|
650
|
+
* const response = await fetch('https://example.com/data.csv');
|
|
651
|
+
* const data = new Uint8Array(await response.arrayBuffer());
|
|
652
|
+
* await db.registerFile('data.csv', data);
|
|
653
|
+
*
|
|
654
|
+
* // Query the file
|
|
655
|
+
* const result = await db.executeQuery("SELECT * FROM read_csv('data.csv');");
|
|
656
|
+
* ```
|
|
657
|
+
*/
|
|
658
|
+
async registerFile(t, e) {
|
|
659
|
+
if (!this.db)
|
|
660
|
+
throw new Error("Database not initialized");
|
|
661
|
+
await this.db.registerFileBuffer(t, e);
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Removes a file from DuckDB's virtual filesystem.
|
|
665
|
+
*
|
|
666
|
+
* @param filename - The filename to remove
|
|
667
|
+
*
|
|
668
|
+
* @throws Error if the database is not initialized
|
|
669
|
+
*
|
|
670
|
+
* @example
|
|
671
|
+
* ```typescript
|
|
672
|
+
* await db.dropFile('data.csv');
|
|
673
|
+
* ```
|
|
674
|
+
*/
|
|
675
|
+
async dropFile(t) {
|
|
676
|
+
if (!this.db)
|
|
677
|
+
throw new Error("Database not initialized");
|
|
678
|
+
await this.db.dropFile(t);
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Checks if the database is initialized and ready for queries.
|
|
682
|
+
*
|
|
683
|
+
* @returns True if the database is ready, false otherwise
|
|
684
|
+
*
|
|
685
|
+
* @example
|
|
686
|
+
* ```typescript
|
|
687
|
+
* if (db.isReady()) {
|
|
688
|
+
* const result = await db.executeQuery('SELECT 1;');
|
|
689
|
+
* }
|
|
690
|
+
* ```
|
|
691
|
+
*/
|
|
692
|
+
isReady() {
|
|
693
|
+
return this.initialized && this.conn !== null;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Closes the database connection and releases resources.
|
|
697
|
+
*
|
|
698
|
+
* This method:
|
|
699
|
+
* 1. Closes the database connection
|
|
700
|
+
* 2. Terminates the DuckDB instance
|
|
701
|
+
* 3. Terminates the Web Worker
|
|
702
|
+
*
|
|
703
|
+
* After calling this method, {@link init} must be called again before
|
|
704
|
+
* executing any queries.
|
|
705
|
+
*
|
|
706
|
+
* @returns A promise that resolves when cleanup is complete
|
|
707
|
+
*
|
|
708
|
+
* @example
|
|
709
|
+
* ```typescript
|
|
710
|
+
* await db.close();
|
|
711
|
+
* console.log('Database closed:', !db.isReady());
|
|
712
|
+
* ```
|
|
713
|
+
*/
|
|
714
|
+
async close() {
|
|
715
|
+
this.conn && (await this.conn.close(), this.conn = null), this.db && (await this.db.terminate(), this.db = null), this.worker && (this.worker.terminate(), this.worker = null), this.initialized = !1;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
const v = "\x1B[0m", dt = "\x1B[1m", ft = "\x1B[2m", mt = "\x1B[30m", g = "\x1B[31m", b = "\x1B[32m", N = "\x1B[33m", U = "\x1B[34m", j = "\x1B[35m", S = "\x1B[36m", F = "\x1B[37m", w = "\x1B[90m", gt = "\x1B[91m", pt = "\x1B[92m", wt = "\x1B[93m", bt = "\x1B[94m", Tt = "\x1B[95m", Et = "\x1B[96m", yt = "\x1B[97m", St = "\x1B[C", x = "\x1B[D", C = "\x1B[K";
|
|
719
|
+
function Lt(i) {
|
|
720
|
+
return `\x1B[${i}C`;
|
|
721
|
+
}
|
|
722
|
+
function T(i) {
|
|
723
|
+
return `\x1B[${i}D`;
|
|
724
|
+
}
|
|
725
|
+
const Rt = {
|
|
726
|
+
black: mt,
|
|
727
|
+
red: g,
|
|
728
|
+
green: b,
|
|
729
|
+
yellow: N,
|
|
730
|
+
blue: U,
|
|
731
|
+
magenta: j,
|
|
732
|
+
cyan: S,
|
|
733
|
+
white: F,
|
|
734
|
+
brightBlack: w,
|
|
735
|
+
brightRed: gt,
|
|
736
|
+
brightGreen: pt,
|
|
737
|
+
brightYellow: wt,
|
|
738
|
+
brightBlue: bt,
|
|
739
|
+
brightMagenta: Tt,
|
|
740
|
+
brightCyan: Et,
|
|
741
|
+
brightWhite: yt
|
|
742
|
+
};
|
|
743
|
+
function c(i, t) {
|
|
744
|
+
return `${Rt[t] || t}${i}${v}`;
|
|
745
|
+
}
|
|
746
|
+
function y(i) {
|
|
747
|
+
return `${dt}${i}${v}`;
|
|
748
|
+
}
|
|
749
|
+
function u(i) {
|
|
750
|
+
return `${ft}${i}${v}`;
|
|
751
|
+
}
|
|
752
|
+
const At = 64 * 1024;
|
|
753
|
+
class Ct {
|
|
754
|
+
constructor() {
|
|
755
|
+
a(this, "buffer", "");
|
|
756
|
+
a(this, "cursorPos", 0);
|
|
757
|
+
a(this, "maxSize", At);
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Set the prompt length for cursor calculations (reserved for future use)
|
|
761
|
+
*/
|
|
762
|
+
setPromptLength(t) {
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Sets the maximum buffer size.
|
|
766
|
+
*
|
|
767
|
+
* @param size - Maximum size in bytes (defaults to MAX_BUFFER_SIZE)
|
|
768
|
+
*/
|
|
769
|
+
setMaxSize(t) {
|
|
770
|
+
this.maxSize = t;
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Returns the maximum buffer size.
|
|
774
|
+
*
|
|
775
|
+
* @returns The maximum size in bytes
|
|
776
|
+
*/
|
|
777
|
+
getMaxSize() {
|
|
778
|
+
return this.maxSize;
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Returns the current buffer content.
|
|
782
|
+
*
|
|
783
|
+
* @returns The full text content of the buffer
|
|
784
|
+
*/
|
|
785
|
+
getContent() {
|
|
786
|
+
return this.buffer;
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Returns the current cursor position within the buffer.
|
|
790
|
+
*
|
|
791
|
+
* @returns The zero-based cursor position
|
|
792
|
+
*/
|
|
793
|
+
getCursorPos() {
|
|
794
|
+
return this.cursorPos;
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Clears the buffer and resets cursor to position 0.
|
|
798
|
+
*/
|
|
799
|
+
clear() {
|
|
800
|
+
this.buffer = "", this.cursorPos = 0;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Sets the buffer content and moves cursor to end.
|
|
804
|
+
*
|
|
805
|
+
* Used for history navigation when replacing the current line with a previous command.
|
|
806
|
+
*
|
|
807
|
+
* @param content - The new buffer content
|
|
808
|
+
*/
|
|
809
|
+
setContent(t) {
|
|
810
|
+
t.length > this.maxSize && (t = t.substring(0, this.maxSize)), this.buffer = t, this.cursorPos = t.length;
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Inserts text at the cursor position.
|
|
814
|
+
*
|
|
815
|
+
* @param char - The character(s) to insert
|
|
816
|
+
* @returns VT100 escape sequences to update the terminal display
|
|
817
|
+
*/
|
|
818
|
+
insert(t) {
|
|
819
|
+
if (this.buffer.length + t.length > this.maxSize) {
|
|
820
|
+
const n = this.maxSize - this.buffer.length;
|
|
821
|
+
if (n <= 0)
|
|
822
|
+
return "";
|
|
823
|
+
t = t.substring(0, n);
|
|
824
|
+
}
|
|
825
|
+
const e = this.buffer.substring(0, this.cursorPos), s = this.buffer.substring(this.cursorPos);
|
|
826
|
+
return this.buffer = e + t + s, this.cursorPos += t.length, s.length === 0 ? t : t + s + T(s.length);
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Deletes the character before the cursor (backspace operation).
|
|
830
|
+
*
|
|
831
|
+
* @returns VT100 escape sequences to update the terminal display, or empty string if at start
|
|
832
|
+
*/
|
|
833
|
+
backspace() {
|
|
834
|
+
if (this.cursorPos === 0)
|
|
835
|
+
return "";
|
|
836
|
+
const t = this.buffer.substring(0, this.cursorPos - 1), e = this.buffer.substring(this.cursorPos);
|
|
837
|
+
return this.buffer = t + e, this.cursorPos--, e.length === 0 ? "\b \b" : x + e + " " + T(e.length + 1);
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Deletes the character at the cursor position (delete key operation).
|
|
841
|
+
*
|
|
842
|
+
* @returns VT100 escape sequences to update the terminal display, or empty string if at end
|
|
843
|
+
*/
|
|
844
|
+
delete() {
|
|
845
|
+
if (this.cursorPos >= this.buffer.length)
|
|
846
|
+
return "";
|
|
847
|
+
const t = this.buffer.substring(0, this.cursorPos), e = this.buffer.substring(this.cursorPos + 1);
|
|
848
|
+
return this.buffer = t + e, e + " " + T(e.length + 1);
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Moves the cursor one position to the left.
|
|
852
|
+
*
|
|
853
|
+
* @returns VT100 escape sequence, or empty string if already at start
|
|
854
|
+
*/
|
|
855
|
+
moveLeft() {
|
|
856
|
+
return this.cursorPos > 0 ? (this.cursorPos--, x) : "";
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Moves the cursor one position to the right.
|
|
860
|
+
*
|
|
861
|
+
* @returns VT100 escape sequence, or empty string if already at end
|
|
862
|
+
*/
|
|
863
|
+
moveRight() {
|
|
864
|
+
return this.cursorPos < this.buffer.length ? (this.cursorPos++, St) : "";
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Moves the cursor to the start of the line (Home key / Ctrl+A).
|
|
868
|
+
*
|
|
869
|
+
* @returns VT100 escape sequence, or empty string if already at start
|
|
870
|
+
*/
|
|
871
|
+
moveToStart() {
|
|
872
|
+
if (this.cursorPos === 0)
|
|
873
|
+
return "";
|
|
874
|
+
const t = this.cursorPos;
|
|
875
|
+
return this.cursorPos = 0, T(t);
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Moves the cursor to the end of the line (End key / Ctrl+E).
|
|
879
|
+
*
|
|
880
|
+
* @returns VT100 escape sequence, or empty string if already at end
|
|
881
|
+
*/
|
|
882
|
+
moveToEnd() {
|
|
883
|
+
if (this.cursorPos === this.buffer.length)
|
|
884
|
+
return "";
|
|
885
|
+
const t = this.buffer.length - this.cursorPos;
|
|
886
|
+
return this.cursorPos = this.buffer.length, Lt(t);
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Clears from cursor position to end of line (Ctrl+K).
|
|
890
|
+
*
|
|
891
|
+
* @returns VT100 escape sequence to clear the terminal
|
|
892
|
+
*/
|
|
893
|
+
clearToEnd() {
|
|
894
|
+
return this.buffer = this.buffer.substring(0, this.cursorPos), C;
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Clears the entire line and resets buffer (Ctrl+U).
|
|
898
|
+
*
|
|
899
|
+
* @returns VT100 escape sequences to clear the terminal line
|
|
900
|
+
*/
|
|
901
|
+
clearLine() {
|
|
902
|
+
const t = this.moveToStart() + C;
|
|
903
|
+
return this.buffer = "", this.cursorPos = 0, t;
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Returns the word immediately before the cursor for auto-completion.
|
|
907
|
+
*
|
|
908
|
+
* A word is defined as a sequence of word characters (`\w`) and dots.
|
|
909
|
+
*
|
|
910
|
+
* @returns The word before cursor, or empty string if none
|
|
911
|
+
*/
|
|
912
|
+
getWordBeforeCursor() {
|
|
913
|
+
const e = this.buffer.substring(0, this.cursorPos).match(/[\w.]*$/);
|
|
914
|
+
return e ? e[0] : "";
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Replaces the word before the cursor with a completion string.
|
|
918
|
+
*
|
|
919
|
+
* @param completion - The completion string to insert
|
|
920
|
+
* @returns VT100 escape sequences to update the terminal display
|
|
921
|
+
*/
|
|
922
|
+
replaceWordBeforeCursor(t) {
|
|
923
|
+
const e = this.getWordBeforeCursor(), s = this.cursorPos - e.length, n = this.buffer.substring(0, s), r = this.buffer.substring(this.cursorPos);
|
|
924
|
+
this.buffer = n + t + r, this.cursorPos = s + t.length;
|
|
925
|
+
let o = "";
|
|
926
|
+
return e.length > 0 && (o += T(e.length)), o += C, o += t + r, r.length > 0 && (o += T(r.length)), o;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Checks if the buffer is empty.
|
|
930
|
+
*
|
|
931
|
+
* @returns True if buffer has no content
|
|
932
|
+
*/
|
|
933
|
+
isEmpty() {
|
|
934
|
+
return this.buffer.length === 0;
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Checks if the buffer contains only whitespace.
|
|
938
|
+
*
|
|
939
|
+
* @returns True if buffer is empty or contains only whitespace
|
|
940
|
+
*/
|
|
941
|
+
isBlank() {
|
|
942
|
+
return this.buffer.trim().length === 0;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
const It = "duckdb-terminal", p = "history", A = 1e3;
|
|
946
|
+
class Pt {
|
|
947
|
+
constructor() {
|
|
948
|
+
a(this, "db", null);
|
|
949
|
+
a(this, "history", []);
|
|
950
|
+
a(this, "cursor", -1);
|
|
951
|
+
a(this, "currentInput", "");
|
|
952
|
+
a(this, "initialized", !1);
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Initializes the history store by opening IndexedDB and loading existing history.
|
|
956
|
+
*
|
|
957
|
+
* This method must be called before using other methods. It loads any
|
|
958
|
+
* previously stored commands from IndexedDB. If IndexedDB is unavailable
|
|
959
|
+
* (e.g., in private browsing), the store falls back to in-memory storage.
|
|
960
|
+
*
|
|
961
|
+
* @returns A promise that resolves when initialization is complete
|
|
962
|
+
*
|
|
963
|
+
* @example
|
|
964
|
+
* ```typescript
|
|
965
|
+
* const history = new HistoryStore();
|
|
966
|
+
* await history.init();
|
|
967
|
+
* // Now ready to use
|
|
968
|
+
* ```
|
|
969
|
+
*/
|
|
970
|
+
async init() {
|
|
971
|
+
if (!this.initialized)
|
|
972
|
+
try {
|
|
973
|
+
this.db = await this.openDatabase(), this.history = await this.loadHistory(), this.history.length > A && (this.history = this.history.slice(-A), this.db && await this.trimHistory()), this.cursor = this.history.length, this.initialized = !0;
|
|
974
|
+
} catch (t) {
|
|
975
|
+
console.warn("IndexedDB not available, using in-memory history:", t), this.initialized = !0;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Open IndexedDB database
|
|
980
|
+
*/
|
|
981
|
+
openDatabase() {
|
|
982
|
+
return new Promise((t, e) => {
|
|
983
|
+
const s = indexedDB.open(It, 1);
|
|
984
|
+
s.onerror = () => e(s.error), s.onsuccess = () => t(s.result), s.onupgradeneeded = (n) => {
|
|
985
|
+
const r = n.target.result;
|
|
986
|
+
r.objectStoreNames.contains(p) || r.createObjectStore(p, {
|
|
987
|
+
keyPath: "id",
|
|
988
|
+
autoIncrement: !0
|
|
989
|
+
});
|
|
990
|
+
};
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Load history from IndexedDB
|
|
995
|
+
*/
|
|
996
|
+
loadHistory() {
|
|
997
|
+
return new Promise((t, e) => {
|
|
998
|
+
if (!this.db) {
|
|
999
|
+
t([]);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
const r = this.db.transaction(p, "readonly").objectStore(p).getAll();
|
|
1003
|
+
r.onerror = () => e(r.error), r.onsuccess = () => {
|
|
1004
|
+
const o = r.result;
|
|
1005
|
+
o.sort((l, h) => l.id - h.id), t(o.map((l) => l.command));
|
|
1006
|
+
};
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Adds a command to the history.
|
|
1011
|
+
*
|
|
1012
|
+
* The command is stored both in memory and persisted to IndexedDB.
|
|
1013
|
+
* Empty commands and consecutive duplicates are ignored.
|
|
1014
|
+
*
|
|
1015
|
+
* @param command - The command to add to history
|
|
1016
|
+
* @returns A promise that resolves when the command has been persisted
|
|
1017
|
+
*
|
|
1018
|
+
* @example
|
|
1019
|
+
* ```typescript
|
|
1020
|
+
* await history.add('SELECT * FROM users;');
|
|
1021
|
+
* await history.add('SELECT * FROM users;'); // Ignored (duplicate)
|
|
1022
|
+
* await history.add(''); // Ignored (empty)
|
|
1023
|
+
* ```
|
|
1024
|
+
*/
|
|
1025
|
+
async add(t) {
|
|
1026
|
+
const e = t.trim();
|
|
1027
|
+
if (e) {
|
|
1028
|
+
if (this.history.length > 0 && this.history[this.history.length - 1] === e) {
|
|
1029
|
+
this.reset();
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
for (this.history.push(e); this.history.length > A; )
|
|
1033
|
+
this.history.shift();
|
|
1034
|
+
if (this.db)
|
|
1035
|
+
try {
|
|
1036
|
+
await this.saveCommand(e), await this.trimHistory();
|
|
1037
|
+
} catch (s) {
|
|
1038
|
+
console.warn("Failed to save history:", s);
|
|
1039
|
+
}
|
|
1040
|
+
this.reset();
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Save command to IndexedDB
|
|
1045
|
+
*/
|
|
1046
|
+
saveCommand(t) {
|
|
1047
|
+
return new Promise((e, s) => {
|
|
1048
|
+
if (!this.db) {
|
|
1049
|
+
e();
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
const o = this.db.transaction(p, "readwrite").objectStore(p).add({ command: t });
|
|
1053
|
+
o.onerror = () => s(o.error), o.onsuccess = () => e();
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Trim history in IndexedDB to max size.
|
|
1058
|
+
*
|
|
1059
|
+
* Uses a single transaction for all deletions for better performance.
|
|
1060
|
+
*/
|
|
1061
|
+
trimHistory() {
|
|
1062
|
+
return new Promise((t, e) => {
|
|
1063
|
+
if (!this.db) {
|
|
1064
|
+
t();
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
const s = this.db.transaction(p, "readwrite"), n = s.objectStore(p), r = n.count();
|
|
1068
|
+
r.onsuccess = () => {
|
|
1069
|
+
const o = r.result;
|
|
1070
|
+
if (o <= A) {
|
|
1071
|
+
t();
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
const l = o - A, h = n.getAllKeys();
|
|
1075
|
+
h.onsuccess = () => {
|
|
1076
|
+
const f = h.result.slice(0, l);
|
|
1077
|
+
if (f.length === 0) {
|
|
1078
|
+
t();
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
s.oncomplete = () => t(), s.onerror = () => e(s.error);
|
|
1082
|
+
for (const m of f)
|
|
1083
|
+
n.delete(m);
|
|
1084
|
+
}, h.onerror = () => e(h.error);
|
|
1085
|
+
}, r.onerror = () => e(r.error);
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Navigates to the previous command in history.
|
|
1090
|
+
*
|
|
1091
|
+
* When called from the end of history (most recent position), this method
|
|
1092
|
+
* saves the current input so it can be restored when navigating forward.
|
|
1093
|
+
*
|
|
1094
|
+
* @param currentInput - The current input buffer content to preserve
|
|
1095
|
+
* @returns The previous command, or null if at the beginning of history
|
|
1096
|
+
*
|
|
1097
|
+
* @example
|
|
1098
|
+
* ```typescript
|
|
1099
|
+
* // Assuming history has ['SELECT 1;', 'SELECT 2;']
|
|
1100
|
+
* const cmd1 = history.previous('SELECT 3'); // Returns 'SELECT 2;'
|
|
1101
|
+
* const cmd2 = history.previous(''); // Returns 'SELECT 1;'
|
|
1102
|
+
* const cmd3 = history.previous(''); // Returns null (at beginning)
|
|
1103
|
+
* ```
|
|
1104
|
+
*/
|
|
1105
|
+
previous(t) {
|
|
1106
|
+
return this.history.length === 0 ? null : (this.cursor === this.history.length && (this.currentInput = t), this.cursor > 0 ? (this.cursor--, this.history[this.cursor]) : null);
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Navigates to the next command in history.
|
|
1110
|
+
*
|
|
1111
|
+
* When reaching the end of history, returns the previously saved current input.
|
|
1112
|
+
*
|
|
1113
|
+
* @returns The next command, the saved current input if at the end, or null if beyond
|
|
1114
|
+
*
|
|
1115
|
+
* @example
|
|
1116
|
+
* ```typescript
|
|
1117
|
+
* // After navigating backward with previous()
|
|
1118
|
+
* const cmd1 = history.next(); // Returns next newer command
|
|
1119
|
+
* const cmd2 = history.next(); // Returns original input when at end
|
|
1120
|
+
* ```
|
|
1121
|
+
*/
|
|
1122
|
+
next() {
|
|
1123
|
+
return this.cursor < this.history.length - 1 ? (this.cursor++, this.history[this.cursor]) : this.cursor === this.history.length - 1 ? (this.cursor++, this.currentInput) : null;
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Resets the history cursor to the end and clears the saved current input.
|
|
1127
|
+
*
|
|
1128
|
+
* This should be called after a command is executed to prepare for new navigation.
|
|
1129
|
+
*/
|
|
1130
|
+
reset() {
|
|
1131
|
+
this.cursor = this.history.length, this.currentInput = "";
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Returns a copy of all commands in history.
|
|
1135
|
+
*
|
|
1136
|
+
* @returns An array of all stored commands, oldest first
|
|
1137
|
+
*/
|
|
1138
|
+
getAll() {
|
|
1139
|
+
return [...this.history];
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Clears all history from both memory and IndexedDB.
|
|
1143
|
+
*
|
|
1144
|
+
* @returns A promise that resolves when the history has been cleared
|
|
1145
|
+
*/
|
|
1146
|
+
async clear() {
|
|
1147
|
+
if (this.history = [], this.cursor = 0, this.currentInput = "", this.db)
|
|
1148
|
+
return new Promise((t, e) => {
|
|
1149
|
+
const r = this.db.transaction(p, "readwrite").objectStore(p).clear();
|
|
1150
|
+
r.onerror = () => e(r.error), r.onsuccess = () => t();
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
const Nt = 50;
|
|
1155
|
+
function B(i) {
|
|
1156
|
+
let t = 0;
|
|
1157
|
+
for (const e of i) {
|
|
1158
|
+
const s = e.charCodeAt(0);
|
|
1159
|
+
s >= 4352 && s <= 4447 || s >= 11904 && s <= 42191 || s >= 44032 && s <= 55203 || s >= 63744 && s <= 64255 || s >= 65040 && s <= 65055 || s >= 65072 && s <= 65135 || s >= 65280 && s <= 65376 || s >= 65504 && s <= 65510 ? t += 2 : t += 1;
|
|
1160
|
+
}
|
|
1161
|
+
return t;
|
|
1162
|
+
}
|
|
1163
|
+
function _(i, t) {
|
|
1164
|
+
const e = B(i), s = t - e;
|
|
1165
|
+
return s <= 0 ? i : i + " ".repeat(s);
|
|
1166
|
+
}
|
|
1167
|
+
function z(i, t) {
|
|
1168
|
+
if (B(i) <= t) return i;
|
|
1169
|
+
let e = "", s = 0;
|
|
1170
|
+
for (const n of i) {
|
|
1171
|
+
const r = n.charCodeAt(0) >= 4352 && n.charCodeAt(0) <= 65535 ? 2 : 1;
|
|
1172
|
+
if (s + r + 1 > t) break;
|
|
1173
|
+
e += n, s += r;
|
|
1174
|
+
}
|
|
1175
|
+
return e + "…";
|
|
1176
|
+
}
|
|
1177
|
+
function Y(i) {
|
|
1178
|
+
return i >= Number.MIN_SAFE_INTEGER && i <= Number.MAX_SAFE_INTEGER ? Number(i) : i.toString();
|
|
1179
|
+
}
|
|
1180
|
+
function Bt(i) {
|
|
1181
|
+
const t = [];
|
|
1182
|
+
if (i.months) {
|
|
1183
|
+
const e = Math.floor(i.months / 12), s = i.months % 12;
|
|
1184
|
+
e && t.push(`${e} year${e !== 1 ? "s" : ""}`), s && t.push(`${s} month${s !== 1 ? "s" : ""}`);
|
|
1185
|
+
}
|
|
1186
|
+
if (i.days && t.push(`${i.days} day${i.days !== 1 ? "s" : ""}`), i.micros) {
|
|
1187
|
+
const e = typeof i.micros == "bigint" ? Number(i.micros) : i.micros, s = Math.floor(e / 36e8), n = Math.floor(e % 36e8 / 6e7), r = e % 6e7 / 1e6;
|
|
1188
|
+
(s || n || r) && t.push(`${String(s).padStart(2, "0")}:${String(n).padStart(2, "0")}:${r.toFixed(6).padStart(9, "0")}`);
|
|
1189
|
+
}
|
|
1190
|
+
return t.length ? t.join(" ") : "00:00:00";
|
|
1191
|
+
}
|
|
1192
|
+
function vt(i) {
|
|
1193
|
+
if (typeof i != "object" || i === null) return !1;
|
|
1194
|
+
const t = i;
|
|
1195
|
+
return "months" in t || "days" in t || "micros" in t;
|
|
1196
|
+
}
|
|
1197
|
+
function V(i, t) {
|
|
1198
|
+
return typeof t == "bigint" ? Y(t) : t instanceof Date ? t.toISOString() : t instanceof Map ? Object.fromEntries(t) : t instanceof Uint8Array ? "\\x" + Array.from(t).map((e) => e.toString(16).padStart(2, "0")).join("") : t;
|
|
1199
|
+
}
|
|
1200
|
+
function L(i, t) {
|
|
1201
|
+
return i == null ? t : typeof i == "string" ? i : typeof i == "boolean" ? i ? "true" : "false" : typeof i == "bigint" ? String(Y(i)) : i instanceof Date ? i.toISOString() : i instanceof Map ? "{" + Array.from(i.entries()).map(
|
|
1202
|
+
([s, n]) => `${L(s, t)}: ${L(n, t)}`
|
|
1203
|
+
).join(", ") + "}" : i instanceof Uint8Array ? "\\x" + Array.from(i).map((e) => e.toString(16).padStart(2, "0")).join("") : Array.isArray(i) ? "[" + i.map((e) => L(e, t)).join(", ") + "]" : vt(i) ? Bt(i) : typeof i == "object" ? JSON.stringify(i, V) : String(i);
|
|
1204
|
+
}
|
|
1205
|
+
function k(i, t, e = {}) {
|
|
1206
|
+
const { maxColumnWidth: s = Nt, nullValue: n = "NULL" } = e;
|
|
1207
|
+
if (i.length === 0)
|
|
1208
|
+
return "";
|
|
1209
|
+
const r = t.map(
|
|
1210
|
+
(h) => h.map((d) => {
|
|
1211
|
+
const f = L(d, n);
|
|
1212
|
+
return s ? z(f, s) : f;
|
|
1213
|
+
})
|
|
1214
|
+
), o = i.map((h, d) => {
|
|
1215
|
+
const f = B(h), m = r.reduce((P, et) => {
|
|
1216
|
+
const it = B(et[d] ?? "");
|
|
1217
|
+
return Math.max(P, it);
|
|
1218
|
+
}, 0);
|
|
1219
|
+
return Math.min(Math.max(f, m), s);
|
|
1220
|
+
}), l = [];
|
|
1221
|
+
l.push("┌" + o.map((h) => "─".repeat(h + 2)).join("┬") + "┐"), l.push(
|
|
1222
|
+
"│" + i.map((h, d) => " " + _(z(h, o[d]), o[d]) + " ").join("│") + "│"
|
|
1223
|
+
), l.push("├" + o.map((h) => "─".repeat(h + 2)).join("┼") + "┤");
|
|
1224
|
+
for (const h of r)
|
|
1225
|
+
l.push(
|
|
1226
|
+
"│" + h.map((d, f) => " " + _(d, o[f]) + " ").join("│") + "│"
|
|
1227
|
+
);
|
|
1228
|
+
return l.push("└" + o.map((h) => "─".repeat(h + 2)).join("┴") + "┘"), l.join(`
|
|
1229
|
+
`);
|
|
1230
|
+
}
|
|
1231
|
+
function O(i, t) {
|
|
1232
|
+
const e = (n) => {
|
|
1233
|
+
const r = L(n, "");
|
|
1234
|
+
return r.includes(",") || r.includes('"') || r.includes(`
|
|
1235
|
+
`) ? '"' + r.replace(/"/g, '""') + '"' : r;
|
|
1236
|
+
}, s = [];
|
|
1237
|
+
s.push(i.map(e).join(","));
|
|
1238
|
+
for (const n of t)
|
|
1239
|
+
s.push(n.map(e).join(","));
|
|
1240
|
+
return s.join(`
|
|
1241
|
+
`);
|
|
1242
|
+
}
|
|
1243
|
+
function $(i, t) {
|
|
1244
|
+
const e = (n) => L(n, "").replace(/\t/g, " ").replace(/\n/g, " "), s = [];
|
|
1245
|
+
s.push(i.map(e).join(" "));
|
|
1246
|
+
for (const n of t)
|
|
1247
|
+
s.push(n.map(e).join(" "));
|
|
1248
|
+
return s.join(`
|
|
1249
|
+
`);
|
|
1250
|
+
}
|
|
1251
|
+
function D(i, t) {
|
|
1252
|
+
const e = t.map((s) => {
|
|
1253
|
+
const n = {};
|
|
1254
|
+
return i.forEach((r, o) => {
|
|
1255
|
+
n[r] = s[o] ?? null;
|
|
1256
|
+
}), n;
|
|
1257
|
+
});
|
|
1258
|
+
return JSON.stringify(e, V, 2);
|
|
1259
|
+
}
|
|
1260
|
+
const M = 100 * 1024 * 1024;
|
|
1261
|
+
class kt extends Error {
|
|
1262
|
+
constructor(t, e, s = M) {
|
|
1263
|
+
super(
|
|
1264
|
+
`File "${t}" (${I(e)}) exceeds maximum size of ${I(s)}`
|
|
1265
|
+
), this.filename = t, this.size = e, this.maxSize = s, this.name = "FileSizeError";
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
function Ot(i, t = M) {
|
|
1269
|
+
if (i.size > t)
|
|
1270
|
+
throw new kt(i.name, i.size, t);
|
|
1271
|
+
}
|
|
1272
|
+
async function K(i) {
|
|
1273
|
+
return new Promise((t) => {
|
|
1274
|
+
const e = document.createElement("input");
|
|
1275
|
+
e.type = "file", e.multiple = i?.multiple ?? !0, e.accept = i?.accept ?? ".csv,.parquet,.json,.db,.duckdb", e.onchange = () => {
|
|
1276
|
+
const s = Array.from(e.files ?? []);
|
|
1277
|
+
t(s);
|
|
1278
|
+
}, e.oncancel = () => {
|
|
1279
|
+
t([]);
|
|
1280
|
+
}, e.click();
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
async function $t(i, t = M) {
|
|
1284
|
+
return Ot(i, t), new Promise((e, s) => {
|
|
1285
|
+
const n = new FileReader();
|
|
1286
|
+
n.onload = () => {
|
|
1287
|
+
e(new Uint8Array(n.result));
|
|
1288
|
+
}, n.onerror = () => s(n.error), n.readAsArrayBuffer(i);
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
function I(i) {
|
|
1292
|
+
if (i === 0) return "0 B";
|
|
1293
|
+
const t = 1024, e = ["B", "KB", "MB", "GB"], s = Math.floor(Math.log(i) / Math.log(t));
|
|
1294
|
+
return parseFloat((i / Math.pow(t, s)).toFixed(2)) + " " + e[s];
|
|
1295
|
+
}
|
|
1296
|
+
function X(i) {
|
|
1297
|
+
const t = i.lastIndexOf(".");
|
|
1298
|
+
return t === -1 ? "" : i.substring(t + 1).toLowerCase();
|
|
1299
|
+
}
|
|
1300
|
+
function Dt(i) {
|
|
1301
|
+
return {
|
|
1302
|
+
name: i.name,
|
|
1303
|
+
size: i.size,
|
|
1304
|
+
type: i.type || X(i.name),
|
|
1305
|
+
lastModified: new Date(i.lastModified)
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
function Ut(i, t) {
|
|
1309
|
+
const e = (r) => {
|
|
1310
|
+
r.preventDefault(), r.stopPropagation(), i.classList.add("drag-over");
|
|
1311
|
+
}, s = (r) => {
|
|
1312
|
+
r.preventDefault(), r.stopPropagation(), i.classList.remove("drag-over");
|
|
1313
|
+
}, n = (r) => {
|
|
1314
|
+
r.preventDefault(), r.stopPropagation(), i.classList.remove("drag-over");
|
|
1315
|
+
const o = Array.from(r.dataTransfer?.files ?? []);
|
|
1316
|
+
o.length > 0 && t(o);
|
|
1317
|
+
};
|
|
1318
|
+
return i.addEventListener("dragover", e), i.addEventListener("dragleave", s), i.addEventListener("drop", n), () => {
|
|
1319
|
+
i.removeEventListener("dragover", e), i.removeEventListener("dragleave", s), i.removeEventListener("drop", n);
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
async function J(i) {
|
|
1323
|
+
try {
|
|
1324
|
+
if (navigator.clipboard && navigator.clipboard.writeText)
|
|
1325
|
+
return await navigator.clipboard.writeText(i), !0;
|
|
1326
|
+
const t = document.createElement("textarea");
|
|
1327
|
+
t.value = i, t.style.position = "fixed", t.style.left = "-9999px", t.style.top = "-9999px", document.body.appendChild(t), t.select();
|
|
1328
|
+
try {
|
|
1329
|
+
return document.execCommand("copy"), !0;
|
|
1330
|
+
} finally {
|
|
1331
|
+
document.body.removeChild(t);
|
|
1332
|
+
}
|
|
1333
|
+
} catch {
|
|
1334
|
+
return !1;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
async function Ft() {
|
|
1338
|
+
try {
|
|
1339
|
+
return navigator.clipboard && navigator.clipboard.readText ? await navigator.clipboard.readText() : null;
|
|
1340
|
+
} catch {
|
|
1341
|
+
return null;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
function ye() {
|
|
1345
|
+
return !!(navigator.clipboard && navigator.clipboard.writeText);
|
|
1346
|
+
}
|
|
1347
|
+
const Mt = /* @__PURE__ */ new Set([
|
|
1348
|
+
"SELECT",
|
|
1349
|
+
"FROM",
|
|
1350
|
+
"WHERE",
|
|
1351
|
+
"AND",
|
|
1352
|
+
"OR",
|
|
1353
|
+
"NOT",
|
|
1354
|
+
"IN",
|
|
1355
|
+
"IS",
|
|
1356
|
+
"NULL",
|
|
1357
|
+
"INSERT",
|
|
1358
|
+
"INTO",
|
|
1359
|
+
"VALUES",
|
|
1360
|
+
"UPDATE",
|
|
1361
|
+
"SET",
|
|
1362
|
+
"DELETE",
|
|
1363
|
+
"CREATE",
|
|
1364
|
+
"TABLE",
|
|
1365
|
+
"DROP",
|
|
1366
|
+
"ALTER",
|
|
1367
|
+
"INDEX",
|
|
1368
|
+
"VIEW",
|
|
1369
|
+
"DATABASE",
|
|
1370
|
+
"SCHEMA",
|
|
1371
|
+
"JOIN",
|
|
1372
|
+
"LEFT",
|
|
1373
|
+
"RIGHT",
|
|
1374
|
+
"INNER",
|
|
1375
|
+
"OUTER",
|
|
1376
|
+
"FULL",
|
|
1377
|
+
"CROSS",
|
|
1378
|
+
"ON",
|
|
1379
|
+
"USING",
|
|
1380
|
+
"GROUP",
|
|
1381
|
+
"BY",
|
|
1382
|
+
"ORDER",
|
|
1383
|
+
"HAVING",
|
|
1384
|
+
"LIMIT",
|
|
1385
|
+
"OFFSET",
|
|
1386
|
+
"UNION",
|
|
1387
|
+
"EXCEPT",
|
|
1388
|
+
"INTERSECT",
|
|
1389
|
+
"ALL",
|
|
1390
|
+
"DISTINCT",
|
|
1391
|
+
"AS",
|
|
1392
|
+
"CASE",
|
|
1393
|
+
"WHEN",
|
|
1394
|
+
"THEN",
|
|
1395
|
+
"ELSE",
|
|
1396
|
+
"END",
|
|
1397
|
+
"BETWEEN",
|
|
1398
|
+
"LIKE",
|
|
1399
|
+
"ILIKE",
|
|
1400
|
+
"EXISTS",
|
|
1401
|
+
"ANY",
|
|
1402
|
+
"SOME",
|
|
1403
|
+
"TRUE",
|
|
1404
|
+
"FALSE",
|
|
1405
|
+
"ASC",
|
|
1406
|
+
"DESC",
|
|
1407
|
+
"NULLS",
|
|
1408
|
+
"FIRST",
|
|
1409
|
+
"LAST",
|
|
1410
|
+
"WITH",
|
|
1411
|
+
"RECURSIVE",
|
|
1412
|
+
"OVER",
|
|
1413
|
+
"PARTITION",
|
|
1414
|
+
"WINDOW",
|
|
1415
|
+
"CAST",
|
|
1416
|
+
"COALESCE",
|
|
1417
|
+
"NULLIF",
|
|
1418
|
+
"EXTRACT",
|
|
1419
|
+
"INTERVAL",
|
|
1420
|
+
"PRIMARY",
|
|
1421
|
+
"KEY",
|
|
1422
|
+
"FOREIGN",
|
|
1423
|
+
"REFERENCES",
|
|
1424
|
+
"UNIQUE",
|
|
1425
|
+
"CHECK",
|
|
1426
|
+
"DEFAULT",
|
|
1427
|
+
"CONSTRAINT",
|
|
1428
|
+
"CASCADE",
|
|
1429
|
+
"RESTRICT",
|
|
1430
|
+
"NO",
|
|
1431
|
+
"ACTION",
|
|
1432
|
+
"BEGIN",
|
|
1433
|
+
"COMMIT",
|
|
1434
|
+
"ROLLBACK",
|
|
1435
|
+
"TRANSACTION",
|
|
1436
|
+
"SAVEPOINT",
|
|
1437
|
+
"GRANT",
|
|
1438
|
+
"REVOKE",
|
|
1439
|
+
"PRIVILEGES",
|
|
1440
|
+
"TO",
|
|
1441
|
+
"PUBLIC",
|
|
1442
|
+
"IF",
|
|
1443
|
+
"REPLACE",
|
|
1444
|
+
"TEMPORARY",
|
|
1445
|
+
"TEMP",
|
|
1446
|
+
"VIRTUAL",
|
|
1447
|
+
"MATERIALIZED"
|
|
1448
|
+
]), xt = /* @__PURE__ */ new Set([
|
|
1449
|
+
"INTEGER",
|
|
1450
|
+
"INT",
|
|
1451
|
+
"BIGINT",
|
|
1452
|
+
"SMALLINT",
|
|
1453
|
+
"TINYINT",
|
|
1454
|
+
"HUGEINT",
|
|
1455
|
+
"FLOAT",
|
|
1456
|
+
"DOUBLE",
|
|
1457
|
+
"REAL",
|
|
1458
|
+
"DECIMAL",
|
|
1459
|
+
"NUMERIC",
|
|
1460
|
+
"VARCHAR",
|
|
1461
|
+
"CHAR",
|
|
1462
|
+
"TEXT",
|
|
1463
|
+
"STRING",
|
|
1464
|
+
"BLOB",
|
|
1465
|
+
"BYTEA",
|
|
1466
|
+
"BOOLEAN",
|
|
1467
|
+
"BOOL",
|
|
1468
|
+
"BIT",
|
|
1469
|
+
"DATE",
|
|
1470
|
+
"TIME",
|
|
1471
|
+
"TIMESTAMP",
|
|
1472
|
+
"DATETIME",
|
|
1473
|
+
"TIMESTAMPTZ",
|
|
1474
|
+
"UUID",
|
|
1475
|
+
"JSON",
|
|
1476
|
+
"ARRAY",
|
|
1477
|
+
"LIST",
|
|
1478
|
+
"MAP",
|
|
1479
|
+
"STRUCT"
|
|
1480
|
+
]), _t = /* @__PURE__ */ new Set([
|
|
1481
|
+
"COUNT",
|
|
1482
|
+
"SUM",
|
|
1483
|
+
"AVG",
|
|
1484
|
+
"MIN",
|
|
1485
|
+
"MAX",
|
|
1486
|
+
"TOTAL",
|
|
1487
|
+
"ABS",
|
|
1488
|
+
"CEIL",
|
|
1489
|
+
"CEILING",
|
|
1490
|
+
"FLOOR",
|
|
1491
|
+
"ROUND",
|
|
1492
|
+
"TRUNC",
|
|
1493
|
+
"TRUNCATE",
|
|
1494
|
+
"SQRT",
|
|
1495
|
+
"POWER",
|
|
1496
|
+
"POW",
|
|
1497
|
+
"EXP",
|
|
1498
|
+
"LOG",
|
|
1499
|
+
"LOG10",
|
|
1500
|
+
"LOG2",
|
|
1501
|
+
"LN",
|
|
1502
|
+
"SIN",
|
|
1503
|
+
"COS",
|
|
1504
|
+
"TAN",
|
|
1505
|
+
"ASIN",
|
|
1506
|
+
"ACOS",
|
|
1507
|
+
"ATAN",
|
|
1508
|
+
"ATAN2",
|
|
1509
|
+
"LENGTH",
|
|
1510
|
+
"CHAR_LENGTH",
|
|
1511
|
+
"OCTET_LENGTH",
|
|
1512
|
+
"BIT_LENGTH",
|
|
1513
|
+
"UPPER",
|
|
1514
|
+
"LOWER",
|
|
1515
|
+
"INITCAP",
|
|
1516
|
+
"TRIM",
|
|
1517
|
+
"LTRIM",
|
|
1518
|
+
"RTRIM",
|
|
1519
|
+
"SUBSTR",
|
|
1520
|
+
"SUBSTRING",
|
|
1521
|
+
"LEFT",
|
|
1522
|
+
"RIGHT",
|
|
1523
|
+
"LPAD",
|
|
1524
|
+
"RPAD",
|
|
1525
|
+
"CONCAT",
|
|
1526
|
+
"CONCAT_WS",
|
|
1527
|
+
"REPLACE",
|
|
1528
|
+
"REVERSE",
|
|
1529
|
+
"REPEAT",
|
|
1530
|
+
"POSITION",
|
|
1531
|
+
"STRPOS",
|
|
1532
|
+
"INSTR",
|
|
1533
|
+
"LOCATE",
|
|
1534
|
+
"SPLIT_PART",
|
|
1535
|
+
"STRING_SPLIT",
|
|
1536
|
+
"STRING_AGG",
|
|
1537
|
+
"LISTAGG",
|
|
1538
|
+
"ARRAY_AGG",
|
|
1539
|
+
"NOW",
|
|
1540
|
+
"CURRENT_DATE",
|
|
1541
|
+
"CURRENT_TIME",
|
|
1542
|
+
"CURRENT_TIMESTAMP",
|
|
1543
|
+
"DATE_PART",
|
|
1544
|
+
"DATE_TRUNC",
|
|
1545
|
+
"DATE_DIFF",
|
|
1546
|
+
"DATE_ADD",
|
|
1547
|
+
"DATE_SUB",
|
|
1548
|
+
"YEAR",
|
|
1549
|
+
"MONTH",
|
|
1550
|
+
"DAY",
|
|
1551
|
+
"HOUR",
|
|
1552
|
+
"MINUTE",
|
|
1553
|
+
"SECOND",
|
|
1554
|
+
"COALESCE",
|
|
1555
|
+
"NULLIF",
|
|
1556
|
+
"IFNULL",
|
|
1557
|
+
"NVL",
|
|
1558
|
+
"IIF",
|
|
1559
|
+
"GREATEST",
|
|
1560
|
+
"LEAST",
|
|
1561
|
+
"RANDOM",
|
|
1562
|
+
"SETSEED",
|
|
1563
|
+
"ROW_NUMBER",
|
|
1564
|
+
"RANK",
|
|
1565
|
+
"DENSE_RANK",
|
|
1566
|
+
"NTILE",
|
|
1567
|
+
"LAG",
|
|
1568
|
+
"LEAD",
|
|
1569
|
+
"FIRST_VALUE",
|
|
1570
|
+
"LAST_VALUE",
|
|
1571
|
+
"NTH_VALUE",
|
|
1572
|
+
"TYPEOF",
|
|
1573
|
+
"TRY_CAST",
|
|
1574
|
+
"UNNEST",
|
|
1575
|
+
"GENERATE_SERIES",
|
|
1576
|
+
"RANGE",
|
|
1577
|
+
"READ_CSV",
|
|
1578
|
+
"READ_PARQUET",
|
|
1579
|
+
"READ_JSON"
|
|
1580
|
+
]);
|
|
1581
|
+
function Z(i) {
|
|
1582
|
+
const t = [];
|
|
1583
|
+
let e = 0;
|
|
1584
|
+
for (; e < i.length; ) {
|
|
1585
|
+
const s = i[e];
|
|
1586
|
+
if (/\s/.test(s)) {
|
|
1587
|
+
const r = e;
|
|
1588
|
+
for (; e < i.length && /\s/.test(i[e]); )
|
|
1589
|
+
e++;
|
|
1590
|
+
t.push({ type: "whitespace", value: i.slice(r, e), start: r, end: e });
|
|
1591
|
+
continue;
|
|
1592
|
+
}
|
|
1593
|
+
if (i.slice(e, e + 2) === "--") {
|
|
1594
|
+
const r = e;
|
|
1595
|
+
for (; e < i.length && i[e] !== `
|
|
1596
|
+
`; )
|
|
1597
|
+
e++;
|
|
1598
|
+
t.push({ type: "comment", value: i.slice(r, e), start: r, end: e });
|
|
1599
|
+
continue;
|
|
1600
|
+
}
|
|
1601
|
+
if (i.slice(e, e + 2) === "/*") {
|
|
1602
|
+
const r = e;
|
|
1603
|
+
for (e += 2; e < i.length - 1 && i.slice(e, e + 2) !== "*/"; )
|
|
1604
|
+
e++;
|
|
1605
|
+
e += 2, t.push({ type: "comment", value: i.slice(r, e), start: r, end: e });
|
|
1606
|
+
continue;
|
|
1607
|
+
}
|
|
1608
|
+
if (s === "'") {
|
|
1609
|
+
const r = e;
|
|
1610
|
+
for (e++; e < i.length; )
|
|
1611
|
+
if (i[e] === "'" && i[e + 1] === "'")
|
|
1612
|
+
e += 2;
|
|
1613
|
+
else if (i[e] === "'") {
|
|
1614
|
+
e++;
|
|
1615
|
+
break;
|
|
1616
|
+
} else
|
|
1617
|
+
e++;
|
|
1618
|
+
t.push({ type: "string", value: i.slice(r, e), start: r, end: e });
|
|
1619
|
+
continue;
|
|
1620
|
+
}
|
|
1621
|
+
if (s === '"') {
|
|
1622
|
+
const r = e;
|
|
1623
|
+
for (e++; e < i.length && i[e] !== '"'; )
|
|
1624
|
+
e++;
|
|
1625
|
+
e++, t.push({ type: "identifier", value: i.slice(r, e), start: r, end: e });
|
|
1626
|
+
continue;
|
|
1627
|
+
}
|
|
1628
|
+
if (/[0-9]/.test(s) || s === "." && /[0-9]/.test(i[e + 1] || "")) {
|
|
1629
|
+
const r = e;
|
|
1630
|
+
for (; e < i.length && /[0-9]/.test(i[e]); )
|
|
1631
|
+
e++;
|
|
1632
|
+
if (i[e] === "." && /[0-9]/.test(i[e + 1] || ""))
|
|
1633
|
+
for (e++; e < i.length && /[0-9]/.test(i[e]); )
|
|
1634
|
+
e++;
|
|
1635
|
+
if (i[e] === "e" || i[e] === "E")
|
|
1636
|
+
for (e++, (i[e] === "+" || i[e] === "-") && e++; e < i.length && /[0-9]/.test(i[e]); )
|
|
1637
|
+
e++;
|
|
1638
|
+
t.push({ type: "number", value: i.slice(r, e), start: r, end: e });
|
|
1639
|
+
continue;
|
|
1640
|
+
}
|
|
1641
|
+
if (/[a-zA-Z_]/.test(s)) {
|
|
1642
|
+
const r = e;
|
|
1643
|
+
for (; e < i.length && /[a-zA-Z0-9_]/.test(i[e]); )
|
|
1644
|
+
e++;
|
|
1645
|
+
const o = i.slice(r, e), l = o.toUpperCase();
|
|
1646
|
+
let h = "identifier";
|
|
1647
|
+
Mt.has(l) ? h = "keyword" : xt.has(l) ? h = "type" : _t.has(l) && (h = "function"), t.push({ type: h, value: o, start: r, end: e });
|
|
1648
|
+
continue;
|
|
1649
|
+
}
|
|
1650
|
+
const n = e;
|
|
1651
|
+
e++, t.push({ type: "operator", value: i.slice(n, e), start: n, end: e });
|
|
1652
|
+
}
|
|
1653
|
+
return t;
|
|
1654
|
+
}
|
|
1655
|
+
function zt(i) {
|
|
1656
|
+
switch (i) {
|
|
1657
|
+
case "keyword":
|
|
1658
|
+
return U;
|
|
1659
|
+
case "type":
|
|
1660
|
+
return S;
|
|
1661
|
+
case "function":
|
|
1662
|
+
return N;
|
|
1663
|
+
case "string":
|
|
1664
|
+
return b;
|
|
1665
|
+
case "number":
|
|
1666
|
+
return j;
|
|
1667
|
+
case "comment":
|
|
1668
|
+
return w;
|
|
1669
|
+
case "operator":
|
|
1670
|
+
return F;
|
|
1671
|
+
case "identifier":
|
|
1672
|
+
case "whitespace":
|
|
1673
|
+
default:
|
|
1674
|
+
return "";
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
function H(i) {
|
|
1678
|
+
const t = Z(i);
|
|
1679
|
+
let e = "";
|
|
1680
|
+
for (const s of t) {
|
|
1681
|
+
const n = zt(s.type);
|
|
1682
|
+
n ? e += n + s.value + v : e += s.value;
|
|
1683
|
+
}
|
|
1684
|
+
return e;
|
|
1685
|
+
}
|
|
1686
|
+
function Se(i) {
|
|
1687
|
+
const t = i.trim();
|
|
1688
|
+
return t ? [...Z(t)].reverse().find((n) => n.type !== "whitespace")?.value === ";" : !1;
|
|
1689
|
+
}
|
|
1690
|
+
function Ht(i, t) {
|
|
1691
|
+
let e = null;
|
|
1692
|
+
const s = function(...n) {
|
|
1693
|
+
e !== null && clearTimeout(e), e = setTimeout(() => {
|
|
1694
|
+
i.apply(this, n), e = null;
|
|
1695
|
+
}, t);
|
|
1696
|
+
};
|
|
1697
|
+
return s.cancel = () => {
|
|
1698
|
+
e !== null && (clearTimeout(e), e = null);
|
|
1699
|
+
}, s;
|
|
1700
|
+
}
|
|
1701
|
+
function Gt(i) {
|
|
1702
|
+
const t = [];
|
|
1703
|
+
let e = "", s = null, n = !1, r = !1;
|
|
1704
|
+
for (let o = 0; o < i.length; o++) {
|
|
1705
|
+
const l = i[o];
|
|
1706
|
+
if (n) {
|
|
1707
|
+
switch (l) {
|
|
1708
|
+
case "n":
|
|
1709
|
+
e += `
|
|
1710
|
+
`;
|
|
1711
|
+
break;
|
|
1712
|
+
case "t":
|
|
1713
|
+
e += " ";
|
|
1714
|
+
break;
|
|
1715
|
+
case "r":
|
|
1716
|
+
e += "\r";
|
|
1717
|
+
break;
|
|
1718
|
+
case "\\":
|
|
1719
|
+
e += "\\";
|
|
1720
|
+
break;
|
|
1721
|
+
case '"':
|
|
1722
|
+
e += '"';
|
|
1723
|
+
break;
|
|
1724
|
+
case "'":
|
|
1725
|
+
e += "'";
|
|
1726
|
+
break;
|
|
1727
|
+
case " ":
|
|
1728
|
+
e += " ";
|
|
1729
|
+
break;
|
|
1730
|
+
default:
|
|
1731
|
+
e += "\\" + l;
|
|
1732
|
+
}
|
|
1733
|
+
n = !1;
|
|
1734
|
+
continue;
|
|
1735
|
+
}
|
|
1736
|
+
if (l === "\\" && s !== "'") {
|
|
1737
|
+
n = !0;
|
|
1738
|
+
continue;
|
|
1739
|
+
}
|
|
1740
|
+
if ((l === '"' || l === "'") && !s) {
|
|
1741
|
+
s = l, r = !0;
|
|
1742
|
+
continue;
|
|
1743
|
+
}
|
|
1744
|
+
if (l === s) {
|
|
1745
|
+
s = null;
|
|
1746
|
+
continue;
|
|
1747
|
+
}
|
|
1748
|
+
if (l === " " && !s) {
|
|
1749
|
+
(e || r) && (t.push(e), e = "", r = !1);
|
|
1750
|
+
continue;
|
|
1751
|
+
}
|
|
1752
|
+
e += l;
|
|
1753
|
+
}
|
|
1754
|
+
return (e || r) && t.push(e), t;
|
|
1755
|
+
}
|
|
1756
|
+
function Qt(i) {
|
|
1757
|
+
const t = Gt(i), e = t[0]?.toLowerCase() ?? "", s = t.slice(1);
|
|
1758
|
+
return { command: e, args: s, allArgs: t };
|
|
1759
|
+
}
|
|
1760
|
+
const E = /https?:\/\/[^\s<>"\])\[}{'|^`\\]+/gi, Wt = "\x1B]8;;", jt = "\x07", Yt = "\x1B]8;;\x07";
|
|
1761
|
+
function Le(i) {
|
|
1762
|
+
return E.lastIndex = 0, E.test(i);
|
|
1763
|
+
}
|
|
1764
|
+
function Re(i) {
|
|
1765
|
+
return E.lastIndex = 0, i.match(E) || [];
|
|
1766
|
+
}
|
|
1767
|
+
function q(i, t) {
|
|
1768
|
+
return `${Wt}${i}${jt}${t ?? i}${Yt}`;
|
|
1769
|
+
}
|
|
1770
|
+
function Vt(i) {
|
|
1771
|
+
return E.lastIndex = 0, i.replace(E, (t) => q(t));
|
|
1772
|
+
}
|
|
1773
|
+
function Ae(i) {
|
|
1774
|
+
try {
|
|
1775
|
+
return new URL(i), !0;
|
|
1776
|
+
} catch {
|
|
1777
|
+
return !1;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
function Kt(i, t = 60) {
|
|
1781
|
+
if (i.length <= t)
|
|
1782
|
+
return i;
|
|
1783
|
+
try {
|
|
1784
|
+
const e = new URL(i), s = e.host, n = e.pathname + e.search + e.hash, r = s.length, o = t - r - 8;
|
|
1785
|
+
if (o < 10)
|
|
1786
|
+
return i.substring(0, t - 3) + "...";
|
|
1787
|
+
const l = n.substring(0, Math.floor(o / 2)), h = n.substring(n.length - Math.floor(o / 2));
|
|
1788
|
+
return `${e.protocol}//${s}${l}...${h}`;
|
|
1789
|
+
} catch {
|
|
1790
|
+
return i.substring(0, t - 3) + "...";
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
class Xt {
|
|
1794
|
+
constructor() {
|
|
1795
|
+
a(this, "enabled", !0);
|
|
1796
|
+
a(this, "maxURLLength", 80);
|
|
1797
|
+
}
|
|
1798
|
+
/**
|
|
1799
|
+
* Enable or disable URL linking
|
|
1800
|
+
*/
|
|
1801
|
+
setEnabled(t) {
|
|
1802
|
+
this.enabled = t;
|
|
1803
|
+
}
|
|
1804
|
+
/**
|
|
1805
|
+
* Check if URL linking is enabled
|
|
1806
|
+
*/
|
|
1807
|
+
isEnabled() {
|
|
1808
|
+
return this.enabled;
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* Set maximum URL display length
|
|
1812
|
+
*/
|
|
1813
|
+
setMaxURLLength(t) {
|
|
1814
|
+
this.maxURLLength = t;
|
|
1815
|
+
}
|
|
1816
|
+
/**
|
|
1817
|
+
* Process text and add hyperlinks to URLs
|
|
1818
|
+
*/
|
|
1819
|
+
process(t) {
|
|
1820
|
+
return this.enabled ? Vt(t) : t;
|
|
1821
|
+
}
|
|
1822
|
+
/**
|
|
1823
|
+
* Process text with truncated URL display
|
|
1824
|
+
*/
|
|
1825
|
+
processWithTruncation(t) {
|
|
1826
|
+
return this.enabled ? (E.lastIndex = 0, t.replace(E, (e) => {
|
|
1827
|
+
const s = Kt(e, this.maxURLLength);
|
|
1828
|
+
return q(e, s);
|
|
1829
|
+
})) : t;
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
const Jt = {
|
|
1833
|
+
background: "#1e1e1e",
|
|
1834
|
+
foreground: "#d4d4d4",
|
|
1835
|
+
cursor: "#d4d4d4",
|
|
1836
|
+
selection: "#264f78",
|
|
1837
|
+
black: "#000000",
|
|
1838
|
+
red: "#cd3131",
|
|
1839
|
+
green: "#0dbc79",
|
|
1840
|
+
yellow: "#e5e510",
|
|
1841
|
+
blue: "#2472c8",
|
|
1842
|
+
magenta: "#bc3fbc",
|
|
1843
|
+
cyan: "#11a8cd",
|
|
1844
|
+
white: "#e5e5e5",
|
|
1845
|
+
brightBlack: "#666666",
|
|
1846
|
+
brightRed: "#f14c4c",
|
|
1847
|
+
brightGreen: "#23d18b",
|
|
1848
|
+
brightYellow: "#f5f543",
|
|
1849
|
+
brightBlue: "#3b8eea",
|
|
1850
|
+
brightMagenta: "#d670d6",
|
|
1851
|
+
brightCyan: "#29b8db",
|
|
1852
|
+
brightWhite: "#ffffff"
|
|
1853
|
+
}, Zt = {
|
|
1854
|
+
background: "#ffffff",
|
|
1855
|
+
foreground: "#1e1e1e",
|
|
1856
|
+
cursor: "#1e1e1e",
|
|
1857
|
+
selection: "#add6ff",
|
|
1858
|
+
black: "#000000",
|
|
1859
|
+
red: "#cd3131",
|
|
1860
|
+
green: "#008000",
|
|
1861
|
+
yellow: "#795e00",
|
|
1862
|
+
blue: "#0451a5",
|
|
1863
|
+
magenta: "#bc05bc",
|
|
1864
|
+
cyan: "#0598bc",
|
|
1865
|
+
white: "#e5e5e5",
|
|
1866
|
+
brightBlack: "#666666",
|
|
1867
|
+
brightRed: "#cd3131",
|
|
1868
|
+
brightGreen: "#14ce14",
|
|
1869
|
+
brightYellow: "#b5ba00",
|
|
1870
|
+
brightBlue: "#0451a5",
|
|
1871
|
+
brightMagenta: "#bc05bc",
|
|
1872
|
+
brightCyan: "#0598bc",
|
|
1873
|
+
brightWhite: "#a5a5a5"
|
|
1874
|
+
}, qt = {
|
|
1875
|
+
name: "dark",
|
|
1876
|
+
colors: Jt
|
|
1877
|
+
}, te = {
|
|
1878
|
+
name: "light",
|
|
1879
|
+
colors: Zt
|
|
1880
|
+
};
|
|
1881
|
+
function G(i) {
|
|
1882
|
+
return i === "light" ? te : qt;
|
|
1883
|
+
}
|
|
1884
|
+
function ee() {
|
|
1885
|
+
try {
|
|
1886
|
+
const i = localStorage.getItem("duckdb-terminal-theme");
|
|
1887
|
+
if (i === "light" || i === "dark")
|
|
1888
|
+
return i;
|
|
1889
|
+
} catch {
|
|
1890
|
+
}
|
|
1891
|
+
return "dark";
|
|
1892
|
+
}
|
|
1893
|
+
function ie(i) {
|
|
1894
|
+
try {
|
|
1895
|
+
localStorage.setItem("duckdb-terminal-theme", i);
|
|
1896
|
+
} catch {
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
const Q = "🦆 ", W = " > ";
|
|
1900
|
+
class se {
|
|
1901
|
+
constructor(t) {
|
|
1902
|
+
a(this, "terminalAdapter");
|
|
1903
|
+
a(this, "database");
|
|
1904
|
+
a(this, "inputBuffer");
|
|
1905
|
+
a(this, "history");
|
|
1906
|
+
a(this, "state", "idle");
|
|
1907
|
+
a(this, "collectedSQL", []);
|
|
1908
|
+
a(this, "commands", /* @__PURE__ */ new Map());
|
|
1909
|
+
a(this, "showTimer", !1);
|
|
1910
|
+
a(this, "outputMode", "table");
|
|
1911
|
+
a(this, "currentThemeName");
|
|
1912
|
+
a(this, "customTheme", null);
|
|
1913
|
+
a(this, "config");
|
|
1914
|
+
a(this, "loadedFiles", /* @__PURE__ */ new Map());
|
|
1915
|
+
a(this, "syntaxHighlighting", !0);
|
|
1916
|
+
a(this, "lastQueryResult", null);
|
|
1917
|
+
a(this, "linkProvider");
|
|
1918
|
+
// Event emitter
|
|
1919
|
+
a(this, "eventListeners", /* @__PURE__ */ new Map());
|
|
1920
|
+
// Pagination state (0 = disabled by default)
|
|
1921
|
+
a(this, "pageSize", 0);
|
|
1922
|
+
a(this, "paginationQuery", null);
|
|
1923
|
+
a(this, "currentPage", 0);
|
|
1924
|
+
a(this, "totalRows", 0);
|
|
1925
|
+
// Prompt customization
|
|
1926
|
+
a(this, "prompt");
|
|
1927
|
+
a(this, "continuationPrompt");
|
|
1928
|
+
// Debounced syntax highlighting (150ms delay to avoid excessive redraws)
|
|
1929
|
+
a(this, "debouncedHighlight", Ht(() => this.redrawLineHighlighted(), 150));
|
|
1930
|
+
// Query queue to prevent race conditions
|
|
1931
|
+
a(this, "queryQueue", Promise.resolve(null));
|
|
1932
|
+
// Cleanup function for drag-and-drop
|
|
1933
|
+
a(this, "dragDropCleanup", null);
|
|
1934
|
+
this.config = t, this.terminalAdapter = new ct(), this.database = new ut({
|
|
1935
|
+
storage: t.storage ?? "memory",
|
|
1936
|
+
databasePath: t.databasePath
|
|
1937
|
+
}), this.inputBuffer = new Ct(), this.history = new Pt(), this.linkProvider = new Xt(), t.linkDetection === !1 && this.linkProvider.setEnabled(!1), this.prompt = t.prompt ?? Q, this.continuationPrompt = t.continuationPrompt ?? W, typeof t.theme == "object" ? (this.customTheme = t.theme, this.currentThemeName = "custom") : this.currentThemeName = t.theme ?? ee(), this.registerCommands();
|
|
1938
|
+
}
|
|
1939
|
+
// ==================== Event Emitter ====================
|
|
1940
|
+
/**
|
|
1941
|
+
* Subscribes to a terminal event.
|
|
1942
|
+
*
|
|
1943
|
+
* The terminal emits various events during its lifecycle that you can
|
|
1944
|
+
* subscribe to for monitoring, logging, or integrating with your application.
|
|
1945
|
+
*
|
|
1946
|
+
* @typeParam K - The event type key from {@link TerminalEvents}
|
|
1947
|
+
* @param event - The event name to subscribe to
|
|
1948
|
+
* @param listener - The callback function to invoke when the event occurs
|
|
1949
|
+
* @returns An unsubscribe function that removes the listener when called
|
|
1950
|
+
*
|
|
1951
|
+
* @example Subscribe to query events
|
|
1952
|
+
* ```typescript
|
|
1953
|
+
* const unsubscribe = terminal.on('queryEnd', ({ sql, result, duration }) => {
|
|
1954
|
+
* console.log(`Query completed in ${duration}ms`);
|
|
1955
|
+
* });
|
|
1956
|
+
*
|
|
1957
|
+
* // Later, to stop listening:
|
|
1958
|
+
* unsubscribe();
|
|
1959
|
+
* ```
|
|
1960
|
+
*
|
|
1961
|
+
* @example Monitor state changes
|
|
1962
|
+
* ```typescript
|
|
1963
|
+
* terminal.on('stateChange', ({ state, previous }) => {
|
|
1964
|
+
* console.log(`Terminal state: ${previous} -> ${state}`);
|
|
1965
|
+
* });
|
|
1966
|
+
* ```
|
|
1967
|
+
*/
|
|
1968
|
+
on(t, e) {
|
|
1969
|
+
return this.eventListeners.has(t) || this.eventListeners.set(t, /* @__PURE__ */ new Set()), this.eventListeners.get(t).add(e), () => this.off(t, e);
|
|
1970
|
+
}
|
|
1971
|
+
/**
|
|
1972
|
+
* Unsubscribes a listener from a terminal event.
|
|
1973
|
+
*
|
|
1974
|
+
* This is an alternative to using the unsubscribe function returned by {@link on}.
|
|
1975
|
+
*
|
|
1976
|
+
* @typeParam K - The event type key from {@link TerminalEvents}
|
|
1977
|
+
* @param event - The event name to unsubscribe from
|
|
1978
|
+
* @param listener - The callback function to remove
|
|
1979
|
+
*
|
|
1980
|
+
* @example
|
|
1981
|
+
* ```typescript
|
|
1982
|
+
* const handler = ({ sql }) => console.log(sql);
|
|
1983
|
+
* terminal.on('queryStart', handler);
|
|
1984
|
+
*
|
|
1985
|
+
* // Later:
|
|
1986
|
+
* terminal.off('queryStart', handler);
|
|
1987
|
+
* ```
|
|
1988
|
+
*/
|
|
1989
|
+
off(t, e) {
|
|
1990
|
+
this.eventListeners.get(t)?.delete(e);
|
|
1991
|
+
}
|
|
1992
|
+
/**
|
|
1993
|
+
* Emits an event to all registered listeners.
|
|
1994
|
+
*
|
|
1995
|
+
* @internal
|
|
1996
|
+
* @typeParam K - The event type key from {@link TerminalEvents}
|
|
1997
|
+
* @param event - The event name to emit
|
|
1998
|
+
* @param payload - The event payload to pass to listeners
|
|
1999
|
+
*/
|
|
2000
|
+
emit(t, e) {
|
|
2001
|
+
this.eventListeners.get(t)?.forEach((s) => {
|
|
2002
|
+
try {
|
|
2003
|
+
s(e);
|
|
2004
|
+
} catch (n) {
|
|
2005
|
+
console.error(`Error in ${t} event listener:`, n);
|
|
2006
|
+
}
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
/**
|
|
2010
|
+
* Sets the terminal state and emits a stateChange event.
|
|
2011
|
+
*
|
|
2012
|
+
* @internal
|
|
2013
|
+
* @param newState - The new terminal state
|
|
2014
|
+
*/
|
|
2015
|
+
setState(t) {
|
|
2016
|
+
const e = this.state;
|
|
2017
|
+
e !== t && (this.state = t, this.emit("stateChange", { state: t, previous: e }));
|
|
2018
|
+
}
|
|
2019
|
+
// ==================== Syntax Highlighting ====================
|
|
2020
|
+
/**
|
|
2021
|
+
* Returns SQL with syntax highlighting applied.
|
|
2022
|
+
*
|
|
2023
|
+
* @param sql - The SQL string to highlight
|
|
2024
|
+
* @returns The SQL with VT100 color codes applied, or the original SQL if highlighting is disabled
|
|
2025
|
+
*/
|
|
2026
|
+
getHighlightedSQL(t) {
|
|
2027
|
+
return this.syntaxHighlighting ? H(t) : t;
|
|
2028
|
+
}
|
|
2029
|
+
/**
|
|
2030
|
+
* Redraws the current input line with syntax highlighting applied.
|
|
2031
|
+
*
|
|
2032
|
+
* This method clears the current line content and rewrites it with
|
|
2033
|
+
* color codes for SQL keywords, strings, numbers, etc. Called on
|
|
2034
|
+
* delimiter characters (space, semicolon, parentheses, comma) via debouncing.
|
|
2035
|
+
*/
|
|
2036
|
+
redrawLineHighlighted() {
|
|
2037
|
+
if (!this.syntaxHighlighting)
|
|
2038
|
+
return;
|
|
2039
|
+
const t = this.inputBuffer.getContent();
|
|
2040
|
+
if (t.length === 0)
|
|
2041
|
+
return;
|
|
2042
|
+
const e = this.inputBuffer.getCursorPos(), s = H(t);
|
|
2043
|
+
e > 0 && this.write(T(e)), this.write(C), this.write(s);
|
|
2044
|
+
const n = t.length - e;
|
|
2045
|
+
n > 0 && this.write(T(n));
|
|
2046
|
+
}
|
|
2047
|
+
/**
|
|
2048
|
+
* Returns the current theme object for terminal styling.
|
|
2049
|
+
*
|
|
2050
|
+
* @returns The custom theme if set, otherwise the built-in theme ('dark' or 'light')
|
|
2051
|
+
*/
|
|
2052
|
+
getCurrentThemeObject() {
|
|
2053
|
+
return this.customTheme ? this.customTheme : G(this.currentThemeName);
|
|
2054
|
+
}
|
|
2055
|
+
/**
|
|
2056
|
+
* Initializes and starts the terminal.
|
|
2057
|
+
*
|
|
2058
|
+
* This method performs the following initialization steps:
|
|
2059
|
+
* 1. Resolves the container element
|
|
2060
|
+
* 2. Initializes the terminal adapter (Ghostty), database (DuckDB), and history store in parallel
|
|
2061
|
+
* 3. Sets up input handling and drag-and-drop file loading
|
|
2062
|
+
* 4. Displays the welcome message (if enabled)
|
|
2063
|
+
* 5. Shows the command prompt
|
|
2064
|
+
* 6. Emits the 'ready' event
|
|
2065
|
+
*
|
|
2066
|
+
* @returns A promise that resolves when the terminal is fully initialized
|
|
2067
|
+
*
|
|
2068
|
+
* @throws Error if the container element cannot be found
|
|
2069
|
+
* @throws Error if DuckDB WASM initialization fails
|
|
2070
|
+
*
|
|
2071
|
+
* @example
|
|
2072
|
+
* ```typescript
|
|
2073
|
+
* const terminal = new DuckDBTerminal({ container: '#terminal' });
|
|
2074
|
+
* await terminal.start();
|
|
2075
|
+
* console.log('Terminal is ready!');
|
|
2076
|
+
* ```
|
|
2077
|
+
*
|
|
2078
|
+
* @fires ready - Emitted when initialization is complete
|
|
2079
|
+
*/
|
|
2080
|
+
async start() {
|
|
2081
|
+
const t = this.resolveContainer();
|
|
2082
|
+
await this.terminalAdapter.init(t, {
|
|
2083
|
+
fontFamily: this.config.fontFamily,
|
|
2084
|
+
fontSize: this.config.fontSize,
|
|
2085
|
+
theme: this.getCurrentThemeObject(),
|
|
2086
|
+
scrollback: this.config.scrollback
|
|
2087
|
+
}), this.config.welcomeMessage !== !1 && (this.writeln(y("DuckDB Terminal") + " v0.1.0"), this.write(u("Loading DuckDB WASM..."))), await Promise.all([
|
|
2088
|
+
this.database.init(),
|
|
2089
|
+
this.history.init()
|
|
2090
|
+
]), this.config.welcomeMessage !== !1 && (this.write("\r" + C), this.writeln(u("Powered by DuckDB WASM and Ghostty")), this.writeln(""), this.writeln("Type " + c(".help", S) + " for available commands"), this.writeln("Enter SQL statements ending with " + c(";", N)), this.writeln("")), this.terminalAdapter.onData(this.handleInput.bind(this)), this.dragDropCleanup = Ut(t, (e) => {
|
|
2091
|
+
this.handleDroppedFiles(e);
|
|
2092
|
+
}), this.showPrompt(), this.emit("ready", {});
|
|
2093
|
+
}
|
|
2094
|
+
/**
|
|
2095
|
+
* Cleans up resources and event listeners.
|
|
2096
|
+
*
|
|
2097
|
+
* Call this method when disposing of the terminal to prevent memory leaks.
|
|
2098
|
+
* This removes drag-and-drop handlers and clears internal state.
|
|
2099
|
+
*/
|
|
2100
|
+
destroy() {
|
|
2101
|
+
this.dragDropCleanup && (this.dragDropCleanup(), this.dragDropCleanup = null), this.eventListeners.clear(), this.debouncedHighlight.cancel();
|
|
2102
|
+
}
|
|
2103
|
+
/**
|
|
2104
|
+
* Processes files dropped onto the terminal via drag-and-drop.
|
|
2105
|
+
*
|
|
2106
|
+
* Uses Promise.allSettled to ensure all files are attempted even if some fail.
|
|
2107
|
+
*
|
|
2108
|
+
* @param files - Array of File objects to load into DuckDB
|
|
2109
|
+
*/
|
|
2110
|
+
async handleDroppedFiles(t) {
|
|
2111
|
+
const e = await Promise.allSettled(
|
|
2112
|
+
t.map((r) => this.loadFile(r))
|
|
2113
|
+
), s = e.filter((r) => r.status === "fulfilled" && r.value).length, n = e.length - s;
|
|
2114
|
+
n > 0 && s > 0 && this.writeln(
|
|
2115
|
+
u(`Loaded ${s} of ${e.length} files (${n} failed)`)
|
|
2116
|
+
), this.showPrompt();
|
|
2117
|
+
}
|
|
2118
|
+
/**
|
|
2119
|
+
* Loads a file into DuckDB's virtual filesystem.
|
|
2120
|
+
*
|
|
2121
|
+
* Registers the file and displays usage hints for supported file types
|
|
2122
|
+
* (CSV, Parquet, JSON). Emits a 'fileLoaded' event on success.
|
|
2123
|
+
*
|
|
2124
|
+
* @param file - The file to load
|
|
2125
|
+
* @returns True if the file was loaded successfully, false otherwise
|
|
2126
|
+
*/
|
|
2127
|
+
async loadFile(t) {
|
|
2128
|
+
const e = X(t.name), s = Dt(t);
|
|
2129
|
+
try {
|
|
2130
|
+
const n = await $t(t);
|
|
2131
|
+
await this.database.registerFile(t.name, n), this.loadedFiles.set(t.name, s), this.emit("fileLoaded", {
|
|
2132
|
+
filename: t.name,
|
|
2133
|
+
size: t.size,
|
|
2134
|
+
type: e
|
|
2135
|
+
}), this.writeln(""), this.writeln(
|
|
2136
|
+
c(`Loaded: ${t.name}`, b) + ` (${I(t.size)})`
|
|
2137
|
+
);
|
|
2138
|
+
const r = t.name.replace(/'/g, "''");
|
|
2139
|
+
if (e === "csv") {
|
|
2140
|
+
const o = t.name.replace(/\.[^.]+$/, "").replace(/[^a-zA-Z0-9_]/g, "_");
|
|
2141
|
+
this.writeln(
|
|
2142
|
+
u(`Hint: SELECT * FROM read_csv('${r}');`)
|
|
2143
|
+
), this.writeln(
|
|
2144
|
+
u(` or: CREATE TABLE ${o} AS SELECT * FROM read_csv('${r}');`)
|
|
2145
|
+
);
|
|
2146
|
+
} else e === "parquet" ? this.writeln(
|
|
2147
|
+
u(`Hint: SELECT * FROM read_parquet('${r}');`)
|
|
2148
|
+
) : e === "json" && this.writeln(
|
|
2149
|
+
u(`Hint: SELECT * FROM read_json('${r}');`)
|
|
2150
|
+
);
|
|
2151
|
+
return !0;
|
|
2152
|
+
} catch (n) {
|
|
2153
|
+
const r = n instanceof Error ? n.message : String(n);
|
|
2154
|
+
return this.writeln(c(`Error loading ${t.name}: ${r}`, g)), !1;
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
/**
|
|
2158
|
+
* Resolves the container element from the configuration.
|
|
2159
|
+
*
|
|
2160
|
+
* @returns The resolved HTML element to attach the terminal to
|
|
2161
|
+
* @throws Error if the container selector doesn't match any element
|
|
2162
|
+
*/
|
|
2163
|
+
resolveContainer() {
|
|
2164
|
+
if (typeof this.config.container == "string") {
|
|
2165
|
+
const t = document.querySelector(this.config.container);
|
|
2166
|
+
if (!t)
|
|
2167
|
+
throw new Error(`Container not found: ${this.config.container}`);
|
|
2168
|
+
return t;
|
|
2169
|
+
}
|
|
2170
|
+
return this.config.container;
|
|
2171
|
+
}
|
|
2172
|
+
/**
|
|
2173
|
+
* Registers all built-in dot commands (.help, .tables, .schema, etc.).
|
|
2174
|
+
*
|
|
2175
|
+
* Called during construction to populate the commands map with handlers
|
|
2176
|
+
* for terminal commands.
|
|
2177
|
+
*/
|
|
2178
|
+
registerCommands() {
|
|
2179
|
+
this.commands.set(".help", {
|
|
2180
|
+
name: ".help",
|
|
2181
|
+
description: "Show available commands",
|
|
2182
|
+
handler: () => this.cmdHelp()
|
|
2183
|
+
}), this.commands.set(".clear", {
|
|
2184
|
+
name: ".clear",
|
|
2185
|
+
description: "Clear the terminal",
|
|
2186
|
+
handler: () => this.clear()
|
|
2187
|
+
}), this.commands.set(".tables", {
|
|
2188
|
+
name: ".tables",
|
|
2189
|
+
description: "List all tables",
|
|
2190
|
+
handler: () => this.cmdTables()
|
|
2191
|
+
}), this.commands.set(".schema", {
|
|
2192
|
+
name: ".schema",
|
|
2193
|
+
description: "Show table schema",
|
|
2194
|
+
usage: ".schema <table_name>",
|
|
2195
|
+
handler: (t) => this.cmdSchema(t)
|
|
2196
|
+
}), this.commands.set(".timer", {
|
|
2197
|
+
name: ".timer",
|
|
2198
|
+
description: "Toggle query timing",
|
|
2199
|
+
usage: ".timer on|off",
|
|
2200
|
+
handler: (t) => this.cmdTimer(t)
|
|
2201
|
+
}), this.commands.set(".mode", {
|
|
2202
|
+
name: ".mode",
|
|
2203
|
+
description: "Set output mode",
|
|
2204
|
+
usage: ".mode table|csv|tsv|json",
|
|
2205
|
+
handler: (t) => this.cmdMode(t)
|
|
2206
|
+
}), this.commands.set(".theme", {
|
|
2207
|
+
name: ".theme",
|
|
2208
|
+
description: "Set color theme (clears screen)",
|
|
2209
|
+
usage: ".theme dark|light",
|
|
2210
|
+
handler: (t) => this.cmdTheme(t)
|
|
2211
|
+
}), this.commands.set(".examples", {
|
|
2212
|
+
name: ".examples",
|
|
2213
|
+
description: "Show example queries",
|
|
2214
|
+
handler: () => this.cmdExamples()
|
|
2215
|
+
}), this.commands.set(".files", {
|
|
2216
|
+
name: ".files",
|
|
2217
|
+
description: "Manage loaded files",
|
|
2218
|
+
usage: ".files [list|add|remove <name|index>]",
|
|
2219
|
+
handler: (t) => this.cmdFiles(t)
|
|
2220
|
+
}), this.commands.set(".open", {
|
|
2221
|
+
name: ".open",
|
|
2222
|
+
description: "Open a file picker to load files",
|
|
2223
|
+
handler: () => this.cmdOpen()
|
|
2224
|
+
}), this.commands.set(".copy", {
|
|
2225
|
+
name: ".copy",
|
|
2226
|
+
description: "Copy last result to clipboard",
|
|
2227
|
+
handler: async () => {
|
|
2228
|
+
await this.copyLastResult();
|
|
2229
|
+
}
|
|
2230
|
+
}), this.commands.set(".highlight", {
|
|
2231
|
+
name: ".highlight",
|
|
2232
|
+
description: "Toggle syntax highlighting",
|
|
2233
|
+
usage: ".highlight on|off",
|
|
2234
|
+
handler: (t) => this.cmdHighlight(t)
|
|
2235
|
+
}), this.commands.set(".links", {
|
|
2236
|
+
name: ".links",
|
|
2237
|
+
description: "Toggle clickable URL detection",
|
|
2238
|
+
usage: ".links on|off",
|
|
2239
|
+
handler: (t) => this.cmdLinks(t)
|
|
2240
|
+
}), this.commands.set(".pagesize", {
|
|
2241
|
+
name: ".pagesize",
|
|
2242
|
+
description: "Enable pagination for large results (default: off)",
|
|
2243
|
+
usage: ".pagesize <number> (0 = disabled)",
|
|
2244
|
+
handler: (t) => this.cmdPageSize(t)
|
|
2245
|
+
}), this.commands.set(".reset", {
|
|
2246
|
+
name: ".reset",
|
|
2247
|
+
description: "Reset database and all settings to defaults",
|
|
2248
|
+
handler: () => this.cmdReset()
|
|
2249
|
+
}), this.commands.set(".prompt", {
|
|
2250
|
+
name: ".prompt",
|
|
2251
|
+
description: "Get or set the command prompt",
|
|
2252
|
+
usage: ".prompt [primary [continuation]]",
|
|
2253
|
+
handler: (t) => this.cmdPrompt(t)
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
/**
|
|
2257
|
+
* Displays the command prompt (primary or continuation based on state).
|
|
2258
|
+
*
|
|
2259
|
+
* Sets terminal state to 'idle' if not currently collecting multi-line SQL.
|
|
2260
|
+
*/
|
|
2261
|
+
showPrompt() {
|
|
2262
|
+
const t = this.state === "collecting" ? this.continuationPrompt : this.prompt;
|
|
2263
|
+
this.inputBuffer.setPromptLength(t.length), this.write(c(t, b)), this.state !== "collecting" && this.setState("idle");
|
|
2264
|
+
}
|
|
2265
|
+
/**
|
|
2266
|
+
* Handles raw terminal input data.
|
|
2267
|
+
*
|
|
2268
|
+
* Routes input to the appropriate handler based on terminal state
|
|
2269
|
+
* (executing, paginating, or normal input mode). Processes escape
|
|
2270
|
+
* sequences separately from regular character input.
|
|
2271
|
+
*
|
|
2272
|
+
* @param data - The raw input data from the terminal
|
|
2273
|
+
*/
|
|
2274
|
+
handleInput(t) {
|
|
2275
|
+
if (this.state !== "executing") {
|
|
2276
|
+
if (this.state === "paginating") {
|
|
2277
|
+
this.handlePaginationInput(t);
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
2280
|
+
if (t.startsWith("\x1B[")) {
|
|
2281
|
+
this.handleEscapeSequence(t);
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
for (const e of t)
|
|
2285
|
+
this.handleChar(e);
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
/**
|
|
2289
|
+
* Handles input during pagination mode.
|
|
2290
|
+
*
|
|
2291
|
+
* Supports: [n]ext page, [p]revious page, [q]uit, page number entry,
|
|
2292
|
+
* and arrow keys for navigation.
|
|
2293
|
+
*
|
|
2294
|
+
* @param data - The input character or escape sequence
|
|
2295
|
+
*/
|
|
2296
|
+
async handlePaginationInput(t) {
|
|
2297
|
+
const e = Math.ceil(this.totalRows / this.pageSize), s = t.toLowerCase();
|
|
2298
|
+
if (s === "n" || t === "\x1B[B") {
|
|
2299
|
+
this.currentPage < e - 1 ? (this.currentPage++, await this.executePaginatedQuery()) : this.writeln(u("Already on last page"));
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2302
|
+
if (s === "p" || t === "\x1B[A") {
|
|
2303
|
+
this.currentPage > 0 ? (this.currentPage--, await this.executePaginatedQuery()) : this.writeln(u("Already on first page"));
|
|
2304
|
+
return;
|
|
2305
|
+
}
|
|
2306
|
+
if (s === "q" || s === "\x1B" || s === "") {
|
|
2307
|
+
this.writeln(""), this.exitPagination(), this.writeln(""), this.showPrompt();
|
|
2308
|
+
return;
|
|
2309
|
+
}
|
|
2310
|
+
if (s === "\r" || s === `
|
|
2311
|
+
`) {
|
|
2312
|
+
const n = this.inputBuffer.getContent().trim();
|
|
2313
|
+
if (n) {
|
|
2314
|
+
const r = parseInt(n, 10);
|
|
2315
|
+
!isNaN(r) && r >= 1 && r <= e ? (this.currentPage = r - 1, this.inputBuffer.clear(), await this.executePaginatedQuery()) : (this.writeln(c(`Invalid page number. Enter 1-${e}`, g)), this.inputBuffer.clear());
|
|
2316
|
+
}
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
2319
|
+
if (/^\d$/.test(s)) {
|
|
2320
|
+
this.write(this.inputBuffer.insert(s));
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
if (s === "" || s === "\b") {
|
|
2324
|
+
this.write(this.inputBuffer.backspace());
|
|
2325
|
+
return;
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
/**
|
|
2329
|
+
* Processes a single character of user input.
|
|
2330
|
+
*
|
|
2331
|
+
* Handles special characters (Enter, Backspace, Tab, Ctrl sequences)
|
|
2332
|
+
* and regular printable characters including Unicode.
|
|
2333
|
+
*
|
|
2334
|
+
* @param char - The single character to process
|
|
2335
|
+
*/
|
|
2336
|
+
handleChar(t) {
|
|
2337
|
+
const e = t.charCodeAt(0);
|
|
2338
|
+
switch (t) {
|
|
2339
|
+
case "\r":
|
|
2340
|
+
// Enter
|
|
2341
|
+
case `
|
|
2342
|
+
`:
|
|
2343
|
+
this.handleEnter();
|
|
2344
|
+
return;
|
|
2345
|
+
case "":
|
|
2346
|
+
// Backspace
|
|
2347
|
+
case "\b":
|
|
2348
|
+
this.write(this.inputBuffer.backspace());
|
|
2349
|
+
return;
|
|
2350
|
+
case "\x1B":
|
|
2351
|
+
return;
|
|
2352
|
+
// Will be handled by escape sequence
|
|
2353
|
+
case " ":
|
|
2354
|
+
this.handleTab();
|
|
2355
|
+
return;
|
|
2356
|
+
case "":
|
|
2357
|
+
this.handleCtrlC();
|
|
2358
|
+
return;
|
|
2359
|
+
case "":
|
|
2360
|
+
this.handlePaste();
|
|
2361
|
+
return;
|
|
2362
|
+
case "":
|
|
2363
|
+
this.write(this.inputBuffer.moveToStart());
|
|
2364
|
+
return;
|
|
2365
|
+
case "":
|
|
2366
|
+
this.write(this.inputBuffer.moveToEnd());
|
|
2367
|
+
return;
|
|
2368
|
+
case "\v":
|
|
2369
|
+
this.write(this.inputBuffer.clearToEnd());
|
|
2370
|
+
return;
|
|
2371
|
+
case "":
|
|
2372
|
+
this.write(this.inputBuffer.clearLine());
|
|
2373
|
+
return;
|
|
2374
|
+
}
|
|
2375
|
+
t !== "\x1B" && (e >= 32 && e < 127 ? (this.write(this.inputBuffer.insert(t)), this.isHighlightTrigger(t) && this.debouncedHighlight()) : e >= 128 && this.write(this.inputBuffer.insert(t)));
|
|
2376
|
+
}
|
|
2377
|
+
/**
|
|
2378
|
+
* Checks if a character should trigger syntax highlighting redraw.
|
|
2379
|
+
*
|
|
2380
|
+
* @param char - The character to check
|
|
2381
|
+
* @returns True if the character is a highlighting trigger (space, semicolon, parentheses, comma)
|
|
2382
|
+
*/
|
|
2383
|
+
isHighlightTrigger(t) {
|
|
2384
|
+
return t === " " || t === ";" || t === "(" || t === ")" || t === ",";
|
|
2385
|
+
}
|
|
2386
|
+
/**
|
|
2387
|
+
* Processes VT100 escape sequences for special keys.
|
|
2388
|
+
*
|
|
2389
|
+
* Handles arrow keys (up/down for history, left/right for cursor),
|
|
2390
|
+
* Home, End, and Delete keys.
|
|
2391
|
+
*
|
|
2392
|
+
* @param seq - The escape sequence string (e.g., '\x1b[A' for Arrow Up)
|
|
2393
|
+
*/
|
|
2394
|
+
handleEscapeSequence(t) {
|
|
2395
|
+
switch (t) {
|
|
2396
|
+
case "\x1B[A":
|
|
2397
|
+
this.handleArrowUp();
|
|
2398
|
+
break;
|
|
2399
|
+
case "\x1B[B":
|
|
2400
|
+
this.handleArrowDown();
|
|
2401
|
+
break;
|
|
2402
|
+
case "\x1B[C":
|
|
2403
|
+
this.write(this.inputBuffer.moveRight());
|
|
2404
|
+
break;
|
|
2405
|
+
case "\x1B[D":
|
|
2406
|
+
this.write(this.inputBuffer.moveLeft());
|
|
2407
|
+
break;
|
|
2408
|
+
case "\x1B[H":
|
|
2409
|
+
this.write(this.inputBuffer.moveToStart());
|
|
2410
|
+
break;
|
|
2411
|
+
case "\x1B[F":
|
|
2412
|
+
this.write(this.inputBuffer.moveToEnd());
|
|
2413
|
+
break;
|
|
2414
|
+
case "\x1B[3~":
|
|
2415
|
+
this.write(this.inputBuffer.delete());
|
|
2416
|
+
break;
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
/**
|
|
2420
|
+
* Handles the Enter key press.
|
|
2421
|
+
*
|
|
2422
|
+
* For dot commands, executes immediately. For SQL, collects lines until
|
|
2423
|
+
* a semicolon terminates the statement, then executes.
|
|
2424
|
+
*/
|
|
2425
|
+
async handleEnter() {
|
|
2426
|
+
const t = this.inputBuffer.getContent();
|
|
2427
|
+
if (this.writeln(""), t.trim() === "" && this.state !== "collecting") {
|
|
2428
|
+
this.showPrompt();
|
|
2429
|
+
return;
|
|
2430
|
+
}
|
|
2431
|
+
if (t.trim().startsWith(".") && this.state !== "collecting") {
|
|
2432
|
+
this.writeln(""), await this.history.add(t.trim()), await this.executeCommand(t.trim()), this.inputBuffer.clear(), this.writeln(""), this.showPrompt();
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2435
|
+
this.collectedSQL.push(t);
|
|
2436
|
+
const e = this.collectedSQL.join(`
|
|
2437
|
+
`).trim();
|
|
2438
|
+
e.endsWith(";") ? (this.writeln(""), await this.executeSQL(e), this.collectedSQL = [], this.state !== "paginating" && this.setState("idle")) : this.setState("collecting"), this.inputBuffer.clear(), this.state !== "paginating" && (e.endsWith(";") && this.writeln(""), this.showPrompt());
|
|
2439
|
+
}
|
|
2440
|
+
/**
|
|
2441
|
+
* Navigates to the previous command in history (Arrow Up).
|
|
2442
|
+
*/
|
|
2443
|
+
handleArrowUp() {
|
|
2444
|
+
const t = this.history.previous(this.inputBuffer.getContent());
|
|
2445
|
+
t !== null && (this.write(this.inputBuffer.clearLine()), this.inputBuffer.setContent(t), this.write(this.getHighlightedSQL(t)));
|
|
2446
|
+
}
|
|
2447
|
+
/**
|
|
2448
|
+
* Navigates to the next command in history (Arrow Down).
|
|
2449
|
+
*/
|
|
2450
|
+
handleArrowDown() {
|
|
2451
|
+
const t = this.history.next();
|
|
2452
|
+
t !== null && (this.write(this.inputBuffer.clearLine()), this.inputBuffer.setContent(t), this.write(this.getHighlightedSQL(t)));
|
|
2453
|
+
}
|
|
2454
|
+
/**
|
|
2455
|
+
* Handles Tab key for auto-completion.
|
|
2456
|
+
*
|
|
2457
|
+
* Provides suggestions for SQL keywords, table names, and functions.
|
|
2458
|
+
* Single match is applied directly; multiple matches are displayed.
|
|
2459
|
+
*/
|
|
2460
|
+
async handleTab() {
|
|
2461
|
+
const t = this.inputBuffer.getContent(), e = this.inputBuffer.getCursorPos(), s = await this.database.getCompletions(t, e);
|
|
2462
|
+
if (s.length !== 0)
|
|
2463
|
+
if (s.length === 1)
|
|
2464
|
+
this.write(this.inputBuffer.replaceWordBeforeCursor(s[0].value)), this.redrawLineHighlighted();
|
|
2465
|
+
else {
|
|
2466
|
+
this.writeln("");
|
|
2467
|
+
const n = s.map((h) => {
|
|
2468
|
+
const d = h.type === "keyword" ? U : h.type === "table" ? b : h.type === "function" ? N : F;
|
|
2469
|
+
return c(h.value, d);
|
|
2470
|
+
}).join(" ");
|
|
2471
|
+
this.writeln(n);
|
|
2472
|
+
const r = this.findCommonPrefix(s.map((h) => h.value)), o = this.inputBuffer.getWordBeforeCursor();
|
|
2473
|
+
r.length > o.length && this.write(this.inputBuffer.replaceWordBeforeCursor(r));
|
|
2474
|
+
const l = this.state === "collecting" ? this.continuationPrompt : this.prompt;
|
|
2475
|
+
this.write(c(l, b)), this.write(this.getHighlightedSQL(this.inputBuffer.getContent()));
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
/**
|
|
2479
|
+
* Finds the longest common prefix among an array of strings.
|
|
2480
|
+
*
|
|
2481
|
+
* Used for auto-completion to expand to the longest unambiguous prefix.
|
|
2482
|
+
*
|
|
2483
|
+
* @param strings - Array of strings to find common prefix for
|
|
2484
|
+
* @returns The longest common prefix (case-insensitive comparison)
|
|
2485
|
+
*/
|
|
2486
|
+
findCommonPrefix(t) {
|
|
2487
|
+
if (t.length === 0) return "";
|
|
2488
|
+
if (t.length === 1) return t[0];
|
|
2489
|
+
let e = t[0];
|
|
2490
|
+
for (let s = 1; s < t.length; s++)
|
|
2491
|
+
for (; !t[s].toLowerCase().startsWith(e.toLowerCase()); )
|
|
2492
|
+
if (e = e.slice(0, -1), e === "") return "";
|
|
2493
|
+
return e;
|
|
2494
|
+
}
|
|
2495
|
+
/**
|
|
2496
|
+
* Handles Ctrl+C to cancel current input or multi-line collection.
|
|
2497
|
+
*/
|
|
2498
|
+
handleCtrlC() {
|
|
2499
|
+
this.writeln("^C"), this.inputBuffer.clear(), this.collectedSQL = [], this.setState("idle"), this.showPrompt();
|
|
2500
|
+
}
|
|
2501
|
+
/**
|
|
2502
|
+
* Handles Ctrl+V to paste content from clipboard.
|
|
2503
|
+
*
|
|
2504
|
+
* Inserts text character-by-character, skipping newlines.
|
|
2505
|
+
*/
|
|
2506
|
+
async handlePaste() {
|
|
2507
|
+
const t = await Ft();
|
|
2508
|
+
if (t)
|
|
2509
|
+
for (const e of t)
|
|
2510
|
+
e === `
|
|
2511
|
+
` || e === "\r" || this.write(this.inputBuffer.insert(e));
|
|
2512
|
+
}
|
|
2513
|
+
/**
|
|
2514
|
+
* Copy last query result to clipboard
|
|
2515
|
+
*/
|
|
2516
|
+
async copyLastResult() {
|
|
2517
|
+
if (!this.lastQueryResult)
|
|
2518
|
+
return this.writeln(u("No query result to copy")), !1;
|
|
2519
|
+
let t;
|
|
2520
|
+
switch (this.outputMode) {
|
|
2521
|
+
case "csv":
|
|
2522
|
+
t = O(this.lastQueryResult.columns, this.lastQueryResult.rows);
|
|
2523
|
+
break;
|
|
2524
|
+
case "tsv":
|
|
2525
|
+
t = $(this.lastQueryResult.columns, this.lastQueryResult.rows);
|
|
2526
|
+
break;
|
|
2527
|
+
case "json":
|
|
2528
|
+
t = D(this.lastQueryResult.columns, this.lastQueryResult.rows);
|
|
2529
|
+
break;
|
|
2530
|
+
default:
|
|
2531
|
+
t = k(this.lastQueryResult.columns, this.lastQueryResult.rows);
|
|
2532
|
+
}
|
|
2533
|
+
const e = await J(t);
|
|
2534
|
+
return e ? this.writeln(c("Result copied to clipboard", b)) : this.writeln(c("Failed to copy to clipboard", g)), e;
|
|
2535
|
+
}
|
|
2536
|
+
/**
|
|
2537
|
+
* Executes a dot command (e.g., .help, .tables, .schema).
|
|
2538
|
+
*
|
|
2539
|
+
* Parses the command and arguments, looks up the handler, and executes it.
|
|
2540
|
+
* Emits 'commandExecute' event on execution and 'error' on failure.
|
|
2541
|
+
*
|
|
2542
|
+
* @param input - The full command string including arguments
|
|
2543
|
+
*/
|
|
2544
|
+
async executeCommand(t) {
|
|
2545
|
+
const { command: e, args: s } = Qt(t), n = this.commands.get(e);
|
|
2546
|
+
if (!n) {
|
|
2547
|
+
this.writeln(c(`Unknown command: ${e}`, g)), this.writeln("Type .help for available commands"), this.emit("error", { message: `Unknown command: ${e}`, source: "command" });
|
|
2548
|
+
return;
|
|
2549
|
+
}
|
|
2550
|
+
this.emit("commandExecute", { command: e, args: s });
|
|
2551
|
+
try {
|
|
2552
|
+
await n.handler(s, this);
|
|
2553
|
+
} catch (r) {
|
|
2554
|
+
const o = r instanceof Error ? r.message : String(r);
|
|
2555
|
+
this.writeln(c(`Error: ${o}`, g)), this.emit("error", { message: o, source: "command" });
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
/**
|
|
2559
|
+
* Executes a SQL query and displays the results.
|
|
2560
|
+
*
|
|
2561
|
+
* This method executes the provided SQL statement against the DuckDB database,
|
|
2562
|
+
* displays the results in the configured output format (table, CSV, or JSON),
|
|
2563
|
+
* and adds the query to the command history.
|
|
2564
|
+
*
|
|
2565
|
+
* For large result sets (when pagination is enabled via `.pagesize`), the method
|
|
2566
|
+
* will automatically paginate the results and enter pagination mode.
|
|
2567
|
+
*
|
|
2568
|
+
* @param sql - The SQL statement to execute (should end with a semicolon)
|
|
2569
|
+
* @returns A promise that resolves to the query result, or null if an error occurred
|
|
2570
|
+
*
|
|
2571
|
+
* @fires queryStart - Emitted before query execution begins
|
|
2572
|
+
* @fires queryEnd - Emitted after query execution completes (success or failure)
|
|
2573
|
+
* @fires error - Emitted if the query fails
|
|
2574
|
+
*
|
|
2575
|
+
* @example Execute a SELECT query
|
|
2576
|
+
* ```typescript
|
|
2577
|
+
* const result = await terminal.executeSQL('SELECT * FROM users;');
|
|
2578
|
+
* if (result) {
|
|
2579
|
+
* console.log(`Retrieved ${result.rowCount} rows in ${result.duration}ms`);
|
|
2580
|
+
* console.log('Columns:', result.columns);
|
|
2581
|
+
* console.log('First row:', result.rows[0]);
|
|
2582
|
+
* }
|
|
2583
|
+
* ```
|
|
2584
|
+
*
|
|
2585
|
+
* @example Execute DDL statements
|
|
2586
|
+
* ```typescript
|
|
2587
|
+
* await terminal.executeSQL('CREATE TABLE products (id INTEGER, name VARCHAR);');
|
|
2588
|
+
* await terminal.executeSQL("INSERT INTO products VALUES (1, 'Widget');");
|
|
2589
|
+
* ```
|
|
2590
|
+
*/
|
|
2591
|
+
async executeSQL(t) {
|
|
2592
|
+
const e = this.queryQueue.then(() => this.executeSQLInternal(t));
|
|
2593
|
+
return this.queryQueue = e.catch(() => null), e;
|
|
2594
|
+
}
|
|
2595
|
+
/**
|
|
2596
|
+
* Internal SQL execution logic.
|
|
2597
|
+
*
|
|
2598
|
+
* @internal
|
|
2599
|
+
* @param sql - The SQL statement to execute
|
|
2600
|
+
* @returns The query result, or null on error
|
|
2601
|
+
*/
|
|
2602
|
+
async executeSQLInternal(t) {
|
|
2603
|
+
this.setState("executing");
|
|
2604
|
+
const e = performance.now();
|
|
2605
|
+
this.emit("queryStart", { sql: t });
|
|
2606
|
+
try {
|
|
2607
|
+
await this.history.add(t);
|
|
2608
|
+
const s = t.trim().replace(/;+$/, ""), n = /^\s*SELECT\s/i.test(s), r = /\b(LIMIT|OFFSET)\b/i.test(s);
|
|
2609
|
+
if (this.pageSize > 0 && n && !r) {
|
|
2610
|
+
const h = `SELECT COUNT(*) as cnt FROM (${s}) AS _count_subquery`, f = (await this.database.executeQuery(h)).rows[0]?.[0], m = typeof f == "bigint" ? Number(f) : f ?? 0;
|
|
2611
|
+
if (m > this.pageSize)
|
|
2612
|
+
return this.paginationQuery = s, this.totalRows = m, this.currentPage = 0, await this.executePaginatedQuery();
|
|
2613
|
+
}
|
|
2614
|
+
const o = await this.database.executeQuery(t), l = performance.now() - e;
|
|
2615
|
+
return this.lastQueryResult = o, o.columns.length > 0 && this.displayResult(o), this.showTimer && this.writeln(
|
|
2616
|
+
u(`Time: ${o.duration.toFixed(2)}ms`)
|
|
2617
|
+
), this.emit("queryEnd", { sql: t, result: o, duration: l }), o;
|
|
2618
|
+
} catch (s) {
|
|
2619
|
+
const n = s instanceof Error ? s.message : String(s), r = performance.now() - e;
|
|
2620
|
+
if (this.writeln(c(`Error: ${n}`, g)), t.includes(`
|
|
2621
|
+
`) || t.length > 80) {
|
|
2622
|
+
const o = t.length > 200 ? t.substring(0, 200) + "..." : t;
|
|
2623
|
+
this.writeln(u(` Query: ${o.replace(/\n/g, " ")}`));
|
|
2624
|
+
}
|
|
2625
|
+
return this.emit("queryEnd", { sql: t, result: null, error: n, duration: r }), this.emit("error", { message: n, source: "query" }), null;
|
|
2626
|
+
} finally {
|
|
2627
|
+
this.state !== "paginating" && this.setState("idle");
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
/**
|
|
2631
|
+
* Executes the current paginated query for the current page.
|
|
2632
|
+
*
|
|
2633
|
+
* Adds LIMIT/OFFSET to the stored query and displays results with
|
|
2634
|
+
* pagination controls.
|
|
2635
|
+
*
|
|
2636
|
+
* @returns The query result for the current page, or null on error
|
|
2637
|
+
*/
|
|
2638
|
+
async executePaginatedQuery() {
|
|
2639
|
+
if (!this.paginationQuery) return null;
|
|
2640
|
+
const t = this.currentPage * this.pageSize, e = `${this.paginationQuery} LIMIT ${this.pageSize} OFFSET ${t}`;
|
|
2641
|
+
try {
|
|
2642
|
+
const s = await this.database.executeQuery(e);
|
|
2643
|
+
this.lastQueryResult = s, s.columns.length > 0 && this.displayResult(s);
|
|
2644
|
+
const n = Math.ceil(this.totalRows / this.pageSize), r = t + 1, o = Math.min(t + this.pageSize, this.totalRows);
|
|
2645
|
+
return this.writeln(""), this.writeln(
|
|
2646
|
+
u(`Showing rows ${r}-${o} of ${this.totalRows} (page ${this.currentPage + 1}/${n})`)
|
|
2647
|
+
), this.writeln(
|
|
2648
|
+
c(" [n]ext [p]rev [q]uit or enter page number", S)
|
|
2649
|
+
), this.setState("paginating"), s;
|
|
2650
|
+
} catch (s) {
|
|
2651
|
+
const n = s instanceof Error ? s.message : String(s);
|
|
2652
|
+
if (this.writeln(c(`Error: ${n}`, g)), e.length > 80) {
|
|
2653
|
+
const r = e.length > 200 ? e.substring(0, 200) + "..." : e;
|
|
2654
|
+
this.writeln(u(` Query: ${r.replace(/\n/g, " ")}`));
|
|
2655
|
+
}
|
|
2656
|
+
return this.exitPagination(), null;
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
/**
|
|
2660
|
+
* Exits pagination mode and resets pagination state.
|
|
2661
|
+
*/
|
|
2662
|
+
exitPagination() {
|
|
2663
|
+
this.paginationQuery = null, this.currentPage = 0, this.totalRows = 0, this.setState("idle");
|
|
2664
|
+
}
|
|
2665
|
+
/**
|
|
2666
|
+
* Displays a query result in the current output mode.
|
|
2667
|
+
*
|
|
2668
|
+
* Formats and writes the result as table, CSV, TSV, or JSON based on
|
|
2669
|
+
* the current outputMode setting.
|
|
2670
|
+
*
|
|
2671
|
+
* @param result - The query result to display
|
|
2672
|
+
*/
|
|
2673
|
+
displayResult(t) {
|
|
2674
|
+
switch (this.outputMode) {
|
|
2675
|
+
case "csv":
|
|
2676
|
+
this.writeln(O(t.columns, t.rows));
|
|
2677
|
+
break;
|
|
2678
|
+
case "tsv":
|
|
2679
|
+
this.writeln($(t.columns, t.rows));
|
|
2680
|
+
break;
|
|
2681
|
+
case "json":
|
|
2682
|
+
this.writeln(D(t.columns, t.rows));
|
|
2683
|
+
break;
|
|
2684
|
+
case "table":
|
|
2685
|
+
default:
|
|
2686
|
+
this.writeln(k(t.columns, t.rows));
|
|
2687
|
+
break;
|
|
2688
|
+
}
|
|
2689
|
+
this.writeln(
|
|
2690
|
+
u(
|
|
2691
|
+
`${t.rowCount} row${t.rowCount !== 1 ? "s" : ""}`
|
|
2692
|
+
)
|
|
2693
|
+
);
|
|
2694
|
+
}
|
|
2695
|
+
// Command handlers
|
|
2696
|
+
cmdHelp() {
|
|
2697
|
+
this.writeln(y("Available commands:")), this.writeln("");
|
|
2698
|
+
for (const t of this.commands.values()) {
|
|
2699
|
+
const e = t.usage ? ` ${u(t.usage)}` : "";
|
|
2700
|
+
this.writeln(` ${c(t.name, S)} ${t.description}${e}`);
|
|
2701
|
+
}
|
|
2702
|
+
this.writeln(""), this.writeln("SQL statements must end with a semicolon (;)");
|
|
2703
|
+
}
|
|
2704
|
+
async cmdTables() {
|
|
2705
|
+
const t = await this.database.getTables();
|
|
2706
|
+
if (t.length === 0) {
|
|
2707
|
+
this.writeln(u("No tables found"));
|
|
2708
|
+
return;
|
|
2709
|
+
}
|
|
2710
|
+
for (const e of t)
|
|
2711
|
+
this.writeln(e);
|
|
2712
|
+
}
|
|
2713
|
+
async cmdSchema(t) {
|
|
2714
|
+
if (t.length === 0) {
|
|
2715
|
+
this.writeln("Usage: .schema <table_name>");
|
|
2716
|
+
return;
|
|
2717
|
+
}
|
|
2718
|
+
const e = await this.database.getTableSchema(t[0]);
|
|
2719
|
+
if (e.length === 0) {
|
|
2720
|
+
this.writeln(u(`Table not found: ${t[0]}`));
|
|
2721
|
+
return;
|
|
2722
|
+
}
|
|
2723
|
+
for (const s of e)
|
|
2724
|
+
this.writeln(` ${s.name} ${u(s.type)}`);
|
|
2725
|
+
}
|
|
2726
|
+
cmdTimer(t) {
|
|
2727
|
+
if (t.length === 0) {
|
|
2728
|
+
this.writeln(`Timer is ${this.showTimer ? "on" : "off"}`);
|
|
2729
|
+
return;
|
|
2730
|
+
}
|
|
2731
|
+
const e = t[0].toLowerCase();
|
|
2732
|
+
e === "on" ? (this.showTimer = !0, this.writeln("Timer is now on")) : e === "off" ? (this.showTimer = !1, this.writeln("Timer is now off")) : this.writeln("Usage: .timer on|off");
|
|
2733
|
+
}
|
|
2734
|
+
cmdMode(t) {
|
|
2735
|
+
if (t.length === 0) {
|
|
2736
|
+
this.writeln(`Output mode: ${this.outputMode}`);
|
|
2737
|
+
return;
|
|
2738
|
+
}
|
|
2739
|
+
const e = t[0].toLowerCase();
|
|
2740
|
+
e === "table" || e === "csv" || e === "tsv" || e === "json" ? (this.outputMode = e, this.writeln(`Output mode set to ${e}`)) : this.writeln("Usage: .mode table|csv|tsv|json");
|
|
2741
|
+
}
|
|
2742
|
+
cmdTheme(t) {
|
|
2743
|
+
if (t.length === 0) {
|
|
2744
|
+
const s = this.customTheme ? this.customTheme.name : this.currentThemeName;
|
|
2745
|
+
this.writeln(`Theme: ${s}`);
|
|
2746
|
+
return;
|
|
2747
|
+
}
|
|
2748
|
+
const e = t[0].toLowerCase();
|
|
2749
|
+
e === "dark" || e === "light" ? this.setTheme(e) : this.writeln("Usage: .theme dark|light");
|
|
2750
|
+
}
|
|
2751
|
+
cmdExamples() {
|
|
2752
|
+
this.writeln(y("Example queries:")), this.writeln(""), this.writeln(c(" -- Create a table", w)), this.writeln(" " + this.getHighlightedSQL("CREATE TABLE users (id INTEGER, name VARCHAR);")), this.writeln(""), this.writeln(c(" -- Insert data", w)), this.writeln(" " + this.getHighlightedSQL("INSERT INTO users VALUES (1, 'Alice'), (2, 'Bob');")), this.writeln(""), this.writeln(c(" -- Query data", w)), this.writeln(" " + this.getHighlightedSQL("SELECT * FROM users WHERE name LIKE 'A%';")), this.writeln(""), this.writeln(c(" -- Use built-in functions", w)), this.writeln(" " + this.getHighlightedSQL("SELECT range(10), current_timestamp;"));
|
|
2753
|
+
}
|
|
2754
|
+
async cmdFiles(t) {
|
|
2755
|
+
const e = t[0]?.toLowerCase() ?? "list";
|
|
2756
|
+
if (e === "list") {
|
|
2757
|
+
if (this.loadedFiles.size === 0) {
|
|
2758
|
+
this.writeln(u("No files loaded")), this.writeln(u("Use .open or drag-and-drop to add files"));
|
|
2759
|
+
return;
|
|
2760
|
+
}
|
|
2761
|
+
this.writeln(y("Loaded files:"));
|
|
2762
|
+
let s = 1;
|
|
2763
|
+
for (const [n, r] of this.loadedFiles)
|
|
2764
|
+
this.writeln(` ${u(`${s}.`)} ${n} ${u(`(${I(r.size)})`)}`), s++;
|
|
2765
|
+
} else if (e === "add")
|
|
2766
|
+
await this.cmdOpen();
|
|
2767
|
+
else if (e === "remove" || e === "rm") {
|
|
2768
|
+
const s = t.slice(1).join(" ");
|
|
2769
|
+
if (!s) {
|
|
2770
|
+
this.writeln("Usage: .files remove <filename|index>");
|
|
2771
|
+
return;
|
|
2772
|
+
}
|
|
2773
|
+
const n = parseInt(s, 10);
|
|
2774
|
+
let r = null;
|
|
2775
|
+
if (!isNaN(n) && n >= 1) {
|
|
2776
|
+
const o = Array.from(this.loadedFiles.keys());
|
|
2777
|
+
if (n <= o.length)
|
|
2778
|
+
r = o[n - 1];
|
|
2779
|
+
else {
|
|
2780
|
+
this.writeln(c(`Invalid index: ${n}. Use .files list to see available files.`, "red"));
|
|
2781
|
+
return;
|
|
2782
|
+
}
|
|
2783
|
+
} else if (this.loadedFiles.has(s))
|
|
2784
|
+
r = s;
|
|
2785
|
+
else {
|
|
2786
|
+
this.writeln(c(`File not found: ${s}`, "red"));
|
|
2787
|
+
return;
|
|
2788
|
+
}
|
|
2789
|
+
if (r)
|
|
2790
|
+
try {
|
|
2791
|
+
await this.database.dropFile(r), this.loadedFiles.delete(r), this.writeln(c(`Removed: ${r}`, "green"));
|
|
2792
|
+
} catch (o) {
|
|
2793
|
+
this.writeln(c(`Error removing file: ${o}`, "red"));
|
|
2794
|
+
}
|
|
2795
|
+
} else
|
|
2796
|
+
this.writeln("Usage: .files [list|add|remove <name|index>]");
|
|
2797
|
+
}
|
|
2798
|
+
async cmdOpen() {
|
|
2799
|
+
this.writeln(u("Opening file picker..."));
|
|
2800
|
+
const t = await K({
|
|
2801
|
+
multiple: !0,
|
|
2802
|
+
accept: ".csv,.parquet,.json,.db,.duckdb"
|
|
2803
|
+
});
|
|
2804
|
+
if (t.length === 0) {
|
|
2805
|
+
this.writeln(u("No files selected"));
|
|
2806
|
+
return;
|
|
2807
|
+
}
|
|
2808
|
+
for (const e of t)
|
|
2809
|
+
await this.loadFile(e);
|
|
2810
|
+
}
|
|
2811
|
+
cmdHighlight(t) {
|
|
2812
|
+
if (t.length === 0) {
|
|
2813
|
+
this.writeln(`Syntax highlighting is ${this.syntaxHighlighting ? "on" : "off"}`);
|
|
2814
|
+
return;
|
|
2815
|
+
}
|
|
2816
|
+
const e = t[0].toLowerCase();
|
|
2817
|
+
e === "on" ? (this.syntaxHighlighting = !0, this.writeln("Syntax highlighting is now on")) : e === "off" ? (this.syntaxHighlighting = !1, this.writeln("Syntax highlighting is now off")) : this.writeln("Usage: .highlight on|off");
|
|
2818
|
+
}
|
|
2819
|
+
cmdLinks(t) {
|
|
2820
|
+
if (t.length === 0) {
|
|
2821
|
+
this.writeln(`URL link detection is ${this.linkProvider.isEnabled() ? "on" : "off"}`);
|
|
2822
|
+
return;
|
|
2823
|
+
}
|
|
2824
|
+
const e = t[0].toLowerCase();
|
|
2825
|
+
e === "on" ? (this.linkProvider.setEnabled(!0), this.writeln("URL link detection is now on")) : e === "off" ? (this.linkProvider.setEnabled(!1), this.writeln("URL link detection is now off")) : this.writeln("Usage: .links on|off");
|
|
2826
|
+
}
|
|
2827
|
+
cmdPageSize(t) {
|
|
2828
|
+
if (t.length === 0) {
|
|
2829
|
+
this.pageSize === 0 ? this.writeln("Pagination is disabled (showing all rows)") : this.writeln(`Page size: ${this.pageSize} rows`);
|
|
2830
|
+
return;
|
|
2831
|
+
}
|
|
2832
|
+
const e = parseInt(t[0], 10);
|
|
2833
|
+
if (isNaN(e) || e < 0) {
|
|
2834
|
+
this.writeln("Usage: .pagesize <number> (0 = no pagination)");
|
|
2835
|
+
return;
|
|
2836
|
+
}
|
|
2837
|
+
this.pageSize = e, e === 0 ? this.writeln("Pagination disabled (will show all rows)") : this.writeln(`Page size set to ${e} rows`);
|
|
2838
|
+
}
|
|
2839
|
+
async cmdReset() {
|
|
2840
|
+
try {
|
|
2841
|
+
const t = await this.database.getTables(), e = t.length;
|
|
2842
|
+
for (const n of t)
|
|
2843
|
+
await this.database.executeQuery(`DROP TABLE IF EXISTS "${n}";`);
|
|
2844
|
+
const s = this.loadedFiles.size;
|
|
2845
|
+
for (const n of this.loadedFiles.keys())
|
|
2846
|
+
try {
|
|
2847
|
+
await this.database.dropFile(n);
|
|
2848
|
+
} catch {
|
|
2849
|
+
}
|
|
2850
|
+
this.loadedFiles.clear(), this.lastQueryResult = null, this.history.reset(), this.showTimer = !1, this.outputMode = "table", this.syntaxHighlighting = !0, this.pageSize = 0, this.linkProvider.setEnabled(!0), this.prompt = Q, this.continuationPrompt = W, this.paginationQuery = null, this.currentPage = 0, this.totalRows = 0, this.writeln(
|
|
2851
|
+
c(
|
|
2852
|
+
`Reset complete: dropped ${e} table${e !== 1 ? "s" : ""}, cleared ${s} file${s !== 1 ? "s" : ""}, settings restored to defaults`,
|
|
2853
|
+
b
|
|
2854
|
+
)
|
|
2855
|
+
);
|
|
2856
|
+
} catch (t) {
|
|
2857
|
+
const e = t instanceof Error ? t.message : String(t);
|
|
2858
|
+
this.writeln(c(`Error during reset: ${e}`, g));
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
cmdPrompt(t) {
|
|
2862
|
+
if (t.length === 0) {
|
|
2863
|
+
this.writeln(`Primary prompt: "${this.prompt}"`), this.writeln(`Continuation prompt: "${this.continuationPrompt}"`);
|
|
2864
|
+
return;
|
|
2865
|
+
}
|
|
2866
|
+
const e = t[0];
|
|
2867
|
+
this.prompt = e, t.length >= 2 ? (this.continuationPrompt = t[1], this.writeln(`Prompts set to "${this.prompt}" and "${this.continuationPrompt}"`)) : this.writeln(`Primary prompt set to "${this.prompt}"`);
|
|
2868
|
+
}
|
|
2869
|
+
// ==================== TerminalInterface Implementation ====================
|
|
2870
|
+
/**
|
|
2871
|
+
* Writes text to the terminal without a trailing newline.
|
|
2872
|
+
*
|
|
2873
|
+
* Use this method for inline output where you don't want to start a new line.
|
|
2874
|
+
* The text is written directly to the terminal without any processing.
|
|
2875
|
+
*
|
|
2876
|
+
* @param text - The text to write to the terminal
|
|
2877
|
+
*
|
|
2878
|
+
* @example
|
|
2879
|
+
* ```typescript
|
|
2880
|
+
* terminal.write('Loading');
|
|
2881
|
+
* terminal.write('...');
|
|
2882
|
+
* terminal.write(' Done!\n');
|
|
2883
|
+
* ```
|
|
2884
|
+
*/
|
|
2885
|
+
write(t) {
|
|
2886
|
+
this.terminalAdapter.write(t);
|
|
2887
|
+
}
|
|
2888
|
+
/**
|
|
2889
|
+
* Writes text to the terminal followed by a newline.
|
|
2890
|
+
*
|
|
2891
|
+
* The text is processed for clickable URLs (if link detection is enabled)
|
|
2892
|
+
* and newlines are normalized to CRLF for proper terminal display.
|
|
2893
|
+
*
|
|
2894
|
+
* @param text - The text to write to the terminal
|
|
2895
|
+
*
|
|
2896
|
+
* @example
|
|
2897
|
+
* ```typescript
|
|
2898
|
+
* terminal.writeln('Query completed successfully!');
|
|
2899
|
+
* terminal.writeln('Visit https://duckdb.org for documentation');
|
|
2900
|
+
* ```
|
|
2901
|
+
*/
|
|
2902
|
+
writeln(t) {
|
|
2903
|
+
const s = this.linkProvider.process(t).replace(/\r?\n/g, `\r
|
|
2904
|
+
`);
|
|
2905
|
+
this.terminalAdapter.writeln(s);
|
|
2906
|
+
}
|
|
2907
|
+
/**
|
|
2908
|
+
* Clears the terminal screen.
|
|
2909
|
+
*
|
|
2910
|
+
* This removes all content from the terminal display and moves the cursor
|
|
2911
|
+
* to the top-left corner.
|
|
2912
|
+
*
|
|
2913
|
+
* @example
|
|
2914
|
+
* ```typescript
|
|
2915
|
+
* terminal.clear();
|
|
2916
|
+
* terminal.writeln('Screen cleared!');
|
|
2917
|
+
* ```
|
|
2918
|
+
*/
|
|
2919
|
+
clear() {
|
|
2920
|
+
this.terminalAdapter.clear();
|
|
2921
|
+
}
|
|
2922
|
+
/**
|
|
2923
|
+
* Sets the terminal color theme.
|
|
2924
|
+
*
|
|
2925
|
+
* You can set a built-in theme ('dark' or 'light') or provide a custom
|
|
2926
|
+
* theme object with your own colors. Built-in theme preferences are
|
|
2927
|
+
* persisted to localStorage.
|
|
2928
|
+
*
|
|
2929
|
+
* @param theme - The theme to apply: 'dark', 'light', or a custom Theme object
|
|
2930
|
+
*
|
|
2931
|
+
* @fires themeChange - Emitted after the theme is changed
|
|
2932
|
+
*
|
|
2933
|
+
* @example Set a built-in theme
|
|
2934
|
+
* ```typescript
|
|
2935
|
+
* terminal.setTheme('light');
|
|
2936
|
+
* ```
|
|
2937
|
+
*
|
|
2938
|
+
* @example Set a custom theme
|
|
2939
|
+
* ```typescript
|
|
2940
|
+
* terminal.setTheme({
|
|
2941
|
+
* name: 'my-theme',
|
|
2942
|
+
* colors: {
|
|
2943
|
+
* background: '#1a1b26',
|
|
2944
|
+
* foreground: '#a9b1d6',
|
|
2945
|
+
* cursor: '#c0caf5',
|
|
2946
|
+
* // ... other color properties
|
|
2947
|
+
* },
|
|
2948
|
+
* });
|
|
2949
|
+
* ```
|
|
2950
|
+
*/
|
|
2951
|
+
setTheme(t) {
|
|
2952
|
+
const e = this.getCurrentThemeObject();
|
|
2953
|
+
typeof t == "object" ? (this.customTheme = t, this.currentThemeName = "custom", this.terminalAdapter.setTheme(t), this.writeln(`Theme set to ${t.name}`)) : (this.customTheme = null, this.currentThemeName = t, this.terminalAdapter.setTheme(G(t)), ie(t), this.writeln(`Theme set to ${t}`));
|
|
2954
|
+
const s = this.getCurrentThemeObject();
|
|
2955
|
+
this.emit("themeChange", { theme: s, previous: e });
|
|
2956
|
+
const n = this.currentThemeName === "light" || this.customTheme?.name === "light";
|
|
2957
|
+
document.body.classList.toggle("light", n), document.body.classList.toggle("dark", !n);
|
|
2958
|
+
}
|
|
2959
|
+
/**
|
|
2960
|
+
* Gets the current theme mode.
|
|
2961
|
+
*
|
|
2962
|
+
* Returns 'dark' or 'light' based on the current theme. For custom themes,
|
|
2963
|
+
* this returns 'light' if the theme name is 'light', otherwise 'dark'.
|
|
2964
|
+
*
|
|
2965
|
+
* @returns The current theme mode: 'dark' or 'light'
|
|
2966
|
+
*
|
|
2967
|
+
* @example
|
|
2968
|
+
* ```typescript
|
|
2969
|
+
* const mode = terminal.getTheme();
|
|
2970
|
+
* console.log(`Current theme mode: ${mode}`);
|
|
2971
|
+
* ```
|
|
2972
|
+
*/
|
|
2973
|
+
getTheme() {
|
|
2974
|
+
return this.currentThemeName === "custom" && this.customTheme ? this.customTheme.name === "light" ? "light" : "dark" : this.currentThemeName;
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
function Ce(i) {
|
|
2978
|
+
const t = /* @__PURE__ */ new Map();
|
|
2979
|
+
return t.set(".help", {
|
|
2980
|
+
name: ".help",
|
|
2981
|
+
description: "Show available commands",
|
|
2982
|
+
handler: () => re(t, i)
|
|
2983
|
+
}), t.set(".clear", {
|
|
2984
|
+
name: ".clear",
|
|
2985
|
+
description: "Clear the terminal",
|
|
2986
|
+
handler: () => i.clear()
|
|
2987
|
+
}), t.set(".tables", {
|
|
2988
|
+
name: ".tables",
|
|
2989
|
+
description: "List all tables",
|
|
2990
|
+
handler: () => ne(i)
|
|
2991
|
+
}), t.set(".schema", {
|
|
2992
|
+
name: ".schema",
|
|
2993
|
+
description: "Show table schema",
|
|
2994
|
+
usage: ".schema <table_name>",
|
|
2995
|
+
handler: (e) => oe(e, i)
|
|
2996
|
+
}), t.set(".timer", {
|
|
2997
|
+
name: ".timer",
|
|
2998
|
+
description: "Toggle query timing",
|
|
2999
|
+
usage: ".timer on|off",
|
|
3000
|
+
handler: (e) => ae(e, i)
|
|
3001
|
+
}), t.set(".mode", {
|
|
3002
|
+
name: ".mode",
|
|
3003
|
+
description: "Set output mode",
|
|
3004
|
+
usage: ".mode table|csv|tsv|json",
|
|
3005
|
+
handler: (e) => le(e, i)
|
|
3006
|
+
}), t.set(".theme", {
|
|
3007
|
+
name: ".theme",
|
|
3008
|
+
description: "Set color theme (clears screen)",
|
|
3009
|
+
usage: ".theme dark|light",
|
|
3010
|
+
handler: (e) => he(e, i)
|
|
3011
|
+
}), t.set(".examples", {
|
|
3012
|
+
name: ".examples",
|
|
3013
|
+
description: "Show example queries",
|
|
3014
|
+
handler: () => ce(i)
|
|
3015
|
+
}), t.set(".files", {
|
|
3016
|
+
name: ".files",
|
|
3017
|
+
description: "Manage loaded files",
|
|
3018
|
+
usage: ".files [list|add|remove <name|index>]",
|
|
3019
|
+
handler: (e) => ue(e, i)
|
|
3020
|
+
}), t.set(".open", {
|
|
3021
|
+
name: ".open",
|
|
3022
|
+
description: "Open a file picker to load files",
|
|
3023
|
+
handler: () => tt(i)
|
|
3024
|
+
}), t.set(".copy", {
|
|
3025
|
+
name: ".copy",
|
|
3026
|
+
description: "Copy last result to clipboard",
|
|
3027
|
+
handler: () => we(i)
|
|
3028
|
+
}), t.set(".highlight", {
|
|
3029
|
+
name: ".highlight",
|
|
3030
|
+
description: "Toggle syntax highlighting",
|
|
3031
|
+
usage: ".highlight on|off",
|
|
3032
|
+
handler: (e) => de(e, i)
|
|
3033
|
+
}), t.set(".links", {
|
|
3034
|
+
name: ".links",
|
|
3035
|
+
description: "Toggle clickable URL detection",
|
|
3036
|
+
usage: ".links on|off",
|
|
3037
|
+
handler: (e) => fe(e, i)
|
|
3038
|
+
}), t.set(".pagesize", {
|
|
3039
|
+
name: ".pagesize",
|
|
3040
|
+
description: "Enable pagination for large results (default: off)",
|
|
3041
|
+
usage: ".pagesize <number> (0 = disabled)",
|
|
3042
|
+
handler: (e) => me(e, i)
|
|
3043
|
+
}), t.set(".reset", {
|
|
3044
|
+
name: ".reset",
|
|
3045
|
+
description: "Reset database and all settings to defaults",
|
|
3046
|
+
handler: () => ge(i)
|
|
3047
|
+
}), t.set(".prompt", {
|
|
3048
|
+
name: ".prompt",
|
|
3049
|
+
description: "Get or set the command prompt",
|
|
3050
|
+
usage: ".prompt [primary [continuation]]",
|
|
3051
|
+
handler: (e) => pe(e, i)
|
|
3052
|
+
}), t;
|
|
3053
|
+
}
|
|
3054
|
+
function re(i, t) {
|
|
3055
|
+
t.writeln(y("Available commands:")), t.writeln("");
|
|
3056
|
+
for (const e of i.values()) {
|
|
3057
|
+
const s = e.usage ? ` ${u(e.usage)}` : "";
|
|
3058
|
+
t.writeln(` ${c(e.name, S)} ${e.description}${s}`);
|
|
3059
|
+
}
|
|
3060
|
+
t.writeln(""), t.writeln("SQL statements must end with a semicolon (;)");
|
|
3061
|
+
}
|
|
3062
|
+
async function ne(i) {
|
|
3063
|
+
const t = await i.getDatabase().getTables();
|
|
3064
|
+
if (t.length === 0) {
|
|
3065
|
+
i.writeln(u("No tables found"));
|
|
3066
|
+
return;
|
|
3067
|
+
}
|
|
3068
|
+
for (const e of t)
|
|
3069
|
+
i.writeln(e);
|
|
3070
|
+
}
|
|
3071
|
+
async function oe(i, t) {
|
|
3072
|
+
if (i.length === 0) {
|
|
3073
|
+
t.writeln("Usage: .schema <table_name>");
|
|
3074
|
+
return;
|
|
3075
|
+
}
|
|
3076
|
+
const e = await t.getDatabase().getTableSchema(i[0]);
|
|
3077
|
+
if (e.length === 0) {
|
|
3078
|
+
t.writeln(u(`Table not found: ${i[0]}`));
|
|
3079
|
+
return;
|
|
3080
|
+
}
|
|
3081
|
+
for (const s of e)
|
|
3082
|
+
t.writeln(` ${s.name} ${u(s.type)}`);
|
|
3083
|
+
}
|
|
3084
|
+
function ae(i, t) {
|
|
3085
|
+
if (i.length === 0) {
|
|
3086
|
+
t.writeln(`Timer is ${t.getShowTimer() ? "on" : "off"}`);
|
|
3087
|
+
return;
|
|
3088
|
+
}
|
|
3089
|
+
const e = i[0].toLowerCase();
|
|
3090
|
+
e === "on" ? (t.setShowTimer(!0), t.writeln("Timer is now on")) : e === "off" ? (t.setShowTimer(!1), t.writeln("Timer is now off")) : t.writeln("Usage: .timer on|off");
|
|
3091
|
+
}
|
|
3092
|
+
function le(i, t) {
|
|
3093
|
+
if (i.length === 0) {
|
|
3094
|
+
t.writeln(`Output mode: ${t.getOutputMode()}`);
|
|
3095
|
+
return;
|
|
3096
|
+
}
|
|
3097
|
+
const e = i[0].toLowerCase();
|
|
3098
|
+
e === "table" || e === "csv" || e === "tsv" || e === "json" ? (t.setOutputMode(e), t.writeln(`Output mode set to ${e}`)) : t.writeln("Usage: .mode table|csv|tsv|json");
|
|
3099
|
+
}
|
|
3100
|
+
function he(i, t) {
|
|
3101
|
+
if (i.length === 0) {
|
|
3102
|
+
t.writeln(`Theme: ${t.getThemeName()}`);
|
|
3103
|
+
return;
|
|
3104
|
+
}
|
|
3105
|
+
const e = i[0].toLowerCase();
|
|
3106
|
+
e === "dark" || e === "light" ? t.setTheme(e) : t.writeln("Usage: .theme dark|light");
|
|
3107
|
+
}
|
|
3108
|
+
function ce(i) {
|
|
3109
|
+
i.writeln(y("Example queries:")), i.writeln(""), i.writeln(c(" -- Create a table", w)), i.writeln(" " + i.getHighlightedSQL("CREATE TABLE users (id INTEGER, name VARCHAR);")), i.writeln(""), i.writeln(c(" -- Insert data", w)), i.writeln(" " + i.getHighlightedSQL("INSERT INTO users VALUES (1, 'Alice'), (2, 'Bob');")), i.writeln(""), i.writeln(c(" -- Query data", w)), i.writeln(" " + i.getHighlightedSQL("SELECT * FROM users WHERE name LIKE 'A%';")), i.writeln(""), i.writeln(c(" -- Use built-in functions", w)), i.writeln(" " + i.getHighlightedSQL("SELECT range(10), current_timestamp;"));
|
|
3110
|
+
}
|
|
3111
|
+
async function ue(i, t) {
|
|
3112
|
+
const e = i[0]?.toLowerCase() ?? "list", s = t.getLoadedFiles();
|
|
3113
|
+
if (e === "list") {
|
|
3114
|
+
if (s.size === 0) {
|
|
3115
|
+
t.writeln(u("No files loaded")), t.writeln(u("Use .open or drag-and-drop to add files"));
|
|
3116
|
+
return;
|
|
3117
|
+
}
|
|
3118
|
+
t.writeln(y("Loaded files:"));
|
|
3119
|
+
let n = 1;
|
|
3120
|
+
for (const [r, o] of s)
|
|
3121
|
+
t.writeln(` ${u(`${n}.`)} ${r} ${u(`(${I(o.size)})`)}`), n++;
|
|
3122
|
+
} else if (e === "add")
|
|
3123
|
+
await tt(t);
|
|
3124
|
+
else if (e === "remove" || e === "rm") {
|
|
3125
|
+
const n = i.slice(1).join(" ");
|
|
3126
|
+
if (!n) {
|
|
3127
|
+
t.writeln("Usage: .files remove <filename|index>");
|
|
3128
|
+
return;
|
|
3129
|
+
}
|
|
3130
|
+
const r = parseInt(n, 10);
|
|
3131
|
+
let o = null;
|
|
3132
|
+
if (!isNaN(r) && r >= 1) {
|
|
3133
|
+
const l = Array.from(s.keys());
|
|
3134
|
+
if (r <= l.length)
|
|
3135
|
+
o = l[r - 1];
|
|
3136
|
+
else {
|
|
3137
|
+
t.writeln(c(`Invalid index: ${r}. Use .files list to see available files.`, "red"));
|
|
3138
|
+
return;
|
|
3139
|
+
}
|
|
3140
|
+
} else if (s.has(n))
|
|
3141
|
+
o = n;
|
|
3142
|
+
else {
|
|
3143
|
+
t.writeln(c(`File not found: ${n}`, "red"));
|
|
3144
|
+
return;
|
|
3145
|
+
}
|
|
3146
|
+
if (o)
|
|
3147
|
+
try {
|
|
3148
|
+
await t.removeFile(o), t.writeln(c(`Removed: ${o}`, "green"));
|
|
3149
|
+
} catch (l) {
|
|
3150
|
+
t.writeln(c(`Error removing file: ${l}`, "red"));
|
|
3151
|
+
}
|
|
3152
|
+
} else
|
|
3153
|
+
t.writeln("Usage: .files [list|add|remove <name|index>]");
|
|
3154
|
+
}
|
|
3155
|
+
async function tt(i) {
|
|
3156
|
+
i.writeln(u("Opening file picker..."));
|
|
3157
|
+
const t = await K({
|
|
3158
|
+
multiple: !0,
|
|
3159
|
+
accept: ".csv,.parquet,.json,.db,.duckdb"
|
|
3160
|
+
});
|
|
3161
|
+
if (t.length === 0) {
|
|
3162
|
+
i.writeln(u("No files selected"));
|
|
3163
|
+
return;
|
|
3164
|
+
}
|
|
3165
|
+
for (const e of t)
|
|
3166
|
+
await i.loadFile(e);
|
|
3167
|
+
}
|
|
3168
|
+
function de(i, t) {
|
|
3169
|
+
if (i.length === 0) {
|
|
3170
|
+
t.writeln(`Syntax highlighting is ${t.getSyntaxHighlighting() ? "on" : "off"}`);
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
3173
|
+
const e = i[0].toLowerCase();
|
|
3174
|
+
e === "on" ? (t.setSyntaxHighlighting(!0), t.writeln("Syntax highlighting is now on")) : e === "off" ? (t.setSyntaxHighlighting(!1), t.writeln("Syntax highlighting is now off")) : t.writeln("Usage: .highlight on|off");
|
|
3175
|
+
}
|
|
3176
|
+
function fe(i, t) {
|
|
3177
|
+
const e = t.getLinkProvider();
|
|
3178
|
+
if (i.length === 0) {
|
|
3179
|
+
t.writeln(`URL link detection is ${e.isEnabled() ? "on" : "off"}`);
|
|
3180
|
+
return;
|
|
3181
|
+
}
|
|
3182
|
+
const s = i[0].toLowerCase();
|
|
3183
|
+
s === "on" ? (e.setEnabled(!0), t.writeln("URL link detection is now on")) : s === "off" ? (e.setEnabled(!1), t.writeln("URL link detection is now off")) : t.writeln("Usage: .links on|off");
|
|
3184
|
+
}
|
|
3185
|
+
function me(i, t) {
|
|
3186
|
+
if (i.length === 0) {
|
|
3187
|
+
const s = t.getPageSize();
|
|
3188
|
+
s === 0 ? t.writeln("Pagination is disabled (showing all rows)") : t.writeln(`Page size: ${s} rows`);
|
|
3189
|
+
return;
|
|
3190
|
+
}
|
|
3191
|
+
const e = parseInt(i[0], 10);
|
|
3192
|
+
if (isNaN(e) || e < 0) {
|
|
3193
|
+
t.writeln("Usage: .pagesize <number> (0 = no pagination)");
|
|
3194
|
+
return;
|
|
3195
|
+
}
|
|
3196
|
+
t.setPageSize(e), e === 0 ? t.writeln("Pagination disabled (will show all rows)") : t.writeln(`Page size set to ${e} rows`);
|
|
3197
|
+
}
|
|
3198
|
+
async function ge(i) {
|
|
3199
|
+
await i.resetState();
|
|
3200
|
+
}
|
|
3201
|
+
function pe(i, t) {
|
|
3202
|
+
if (i.length === 0) {
|
|
3203
|
+
t.writeln(`Primary prompt: "${t.getPrompt()}"`), t.writeln(`Continuation prompt: "${t.getContinuationPrompt()}"`);
|
|
3204
|
+
return;
|
|
3205
|
+
}
|
|
3206
|
+
const e = i[0];
|
|
3207
|
+
i.length >= 2 ? (t.setPrompts(e, i[1]), t.writeln(`Prompts set to "${e}" and "${i[1]}"`)) : (t.setPrompts(e), t.writeln(`Primary prompt set to "${e}"`));
|
|
3208
|
+
}
|
|
3209
|
+
async function we(i) {
|
|
3210
|
+
const t = i.getLastQueryResult();
|
|
3211
|
+
if (!t) {
|
|
3212
|
+
i.writeln(u("No query result to copy"));
|
|
3213
|
+
return;
|
|
3214
|
+
}
|
|
3215
|
+
const e = i.getOutputMode();
|
|
3216
|
+
let s;
|
|
3217
|
+
switch (e) {
|
|
3218
|
+
case "csv":
|
|
3219
|
+
s = O(t.columns, t.rows);
|
|
3220
|
+
break;
|
|
3221
|
+
case "tsv":
|
|
3222
|
+
s = $(t.columns, t.rows);
|
|
3223
|
+
break;
|
|
3224
|
+
case "json":
|
|
3225
|
+
s = D(t.columns, t.rows);
|
|
3226
|
+
break;
|
|
3227
|
+
default:
|
|
3228
|
+
s = k(t.columns, t.rows);
|
|
3229
|
+
}
|
|
3230
|
+
await J(s) ? i.writeln(c(`Copied ${t.rowCount} rows to clipboard (${e} format)`, b)) : i.writeln(c("Failed to copy to clipboard", g));
|
|
3231
|
+
}
|
|
3232
|
+
class Ie {
|
|
3233
|
+
constructor(t) {
|
|
3234
|
+
a(this, "state", {
|
|
3235
|
+
query: null,
|
|
3236
|
+
currentPage: 0,
|
|
3237
|
+
totalRows: 0,
|
|
3238
|
+
pageSize: 0,
|
|
3239
|
+
isActive: !1
|
|
3240
|
+
});
|
|
3241
|
+
a(this, "ctx");
|
|
3242
|
+
this.ctx = t;
|
|
3243
|
+
}
|
|
3244
|
+
/**
|
|
3245
|
+
* Initialize pagination for a query
|
|
3246
|
+
*/
|
|
3247
|
+
start(t, e, s) {
|
|
3248
|
+
this.state = {
|
|
3249
|
+
query: t,
|
|
3250
|
+
currentPage: 0,
|
|
3251
|
+
totalRows: e,
|
|
3252
|
+
pageSize: s,
|
|
3253
|
+
isActive: !0
|
|
3254
|
+
};
|
|
3255
|
+
}
|
|
3256
|
+
/**
|
|
3257
|
+
* Exit pagination mode
|
|
3258
|
+
*/
|
|
3259
|
+
exit() {
|
|
3260
|
+
this.state = {
|
|
3261
|
+
query: null,
|
|
3262
|
+
currentPage: 0,
|
|
3263
|
+
totalRows: 0,
|
|
3264
|
+
pageSize: 0,
|
|
3265
|
+
isActive: !1
|
|
3266
|
+
};
|
|
3267
|
+
}
|
|
3268
|
+
/**
|
|
3269
|
+
* Check if pagination is currently active
|
|
3270
|
+
*/
|
|
3271
|
+
isActive() {
|
|
3272
|
+
return this.state.isActive;
|
|
3273
|
+
}
|
|
3274
|
+
/**
|
|
3275
|
+
* Get current state
|
|
3276
|
+
*/
|
|
3277
|
+
getState() {
|
|
3278
|
+
return this.state;
|
|
3279
|
+
}
|
|
3280
|
+
/**
|
|
3281
|
+
* Get the total number of pages
|
|
3282
|
+
*/
|
|
3283
|
+
getTotalPages() {
|
|
3284
|
+
return this.state.pageSize === 0 ? 0 : Math.ceil(this.state.totalRows / this.state.pageSize);
|
|
3285
|
+
}
|
|
3286
|
+
/**
|
|
3287
|
+
* Get current page (1-indexed for display)
|
|
3288
|
+
*/
|
|
3289
|
+
getCurrentPageDisplay() {
|
|
3290
|
+
return this.state.currentPage + 1;
|
|
3291
|
+
}
|
|
3292
|
+
/**
|
|
3293
|
+
* Execute the paginated query for the current page
|
|
3294
|
+
*/
|
|
3295
|
+
async executeCurrentPage() {
|
|
3296
|
+
if (!this.state.query) return;
|
|
3297
|
+
const t = this.state.currentPage * this.state.pageSize, e = `${this.state.query} LIMIT ${this.state.pageSize} OFFSET ${t}`;
|
|
3298
|
+
try {
|
|
3299
|
+
const s = await this.ctx.getDatabase().executeQuery(e);
|
|
3300
|
+
this.ctx.displayResult(s, !1), this.showNavigationHint();
|
|
3301
|
+
} catch (s) {
|
|
3302
|
+
const n = s instanceof Error ? s.message : String(s);
|
|
3303
|
+
this.ctx.writeln(c(`Error: ${n}`, g));
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
/**
|
|
3307
|
+
* Show navigation hint
|
|
3308
|
+
*/
|
|
3309
|
+
showNavigationHint() {
|
|
3310
|
+
const t = this.getTotalPages();
|
|
3311
|
+
this.ctx.writeln(""), this.ctx.writeln(
|
|
3312
|
+
u(
|
|
3313
|
+
`Page ${this.getCurrentPageDisplay()}/${t} (${this.state.totalRows} rows) - n:next p:prev 1-${t}:goto q:quit`
|
|
3314
|
+
)
|
|
3315
|
+
);
|
|
3316
|
+
}
|
|
3317
|
+
/**
|
|
3318
|
+
* Handle user input during pagination
|
|
3319
|
+
* @returns true if the input was handled, false otherwise
|
|
3320
|
+
*/
|
|
3321
|
+
async handleInput(t) {
|
|
3322
|
+
if (!this.state.isActive) return !1;
|
|
3323
|
+
const e = this.getTotalPages(), s = t.toLowerCase();
|
|
3324
|
+
if (s === "n" || t === "\x1B[B")
|
|
3325
|
+
return this.state.currentPage < e - 1 ? (this.state.currentPage++, await this.executeCurrentPage()) : this.ctx.writeln(u("Already on last page")), !0;
|
|
3326
|
+
if (s === "p" || t === "\x1B[A")
|
|
3327
|
+
return this.state.currentPage > 0 ? (this.state.currentPage--, await this.executeCurrentPage()) : this.ctx.writeln(u("Already on first page")), !0;
|
|
3328
|
+
if (s === "q" || s === "\x1B" || s === "")
|
|
3329
|
+
return this.ctx.writeln(""), this.exit(), !0;
|
|
3330
|
+
if (s === "\r" || s === `
|
|
3331
|
+
`) {
|
|
3332
|
+
const n = this.ctx.getInputContent().trim();
|
|
3333
|
+
if (n) {
|
|
3334
|
+
const r = parseInt(n, 10);
|
|
3335
|
+
!isNaN(r) && r >= 1 && r <= e ? (this.state.currentPage = r - 1, this.ctx.clearInput(), await this.executeCurrentPage()) : (this.ctx.writeln(c(`Invalid page number. Enter 1-${e}`, g)), this.ctx.clearInput());
|
|
3336
|
+
}
|
|
3337
|
+
return !0;
|
|
3338
|
+
}
|
|
3339
|
+
return /^\d$/.test(s) ? (this.ctx.write(this.ctx.insertChar(s)), !0) : s === "" || s === "\b" ? (this.ctx.write(this.ctx.backspace()), !0) : !1;
|
|
3340
|
+
}
|
|
3341
|
+
/**
|
|
3342
|
+
* Check if a query should use pagination
|
|
3343
|
+
* @param sql The SQL query
|
|
3344
|
+
* @param rowCount The total row count
|
|
3345
|
+
* @param pageSize The configured page size (0 = disabled)
|
|
3346
|
+
* @returns Whether pagination should be enabled
|
|
3347
|
+
*/
|
|
3348
|
+
static shouldPaginate(t, e, s) {
|
|
3349
|
+
return !(s === 0 || e <= s || !t.trim().toUpperCase().startsWith("SELECT") || /\b(LIMIT|OFFSET)\b/i.test(t));
|
|
3350
|
+
}
|
|
3351
|
+
/**
|
|
3352
|
+
* Strip trailing semicolon from SQL for pagination
|
|
3353
|
+
*/
|
|
3354
|
+
static prepareQuery(t) {
|
|
3355
|
+
return t.replace(/;\s*$/, "");
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
3358
|
+
async function be(i) {
|
|
3359
|
+
const t = new se(i);
|
|
3360
|
+
return await t.start(), t;
|
|
3361
|
+
}
|
|
3362
|
+
async function Pe(i) {
|
|
3363
|
+
return be(i);
|
|
3364
|
+
}
|
|
3365
|
+
export {
|
|
3366
|
+
dt as BOLD,
|
|
3367
|
+
ft as DIM,
|
|
3368
|
+
ut as Database,
|
|
3369
|
+
se as DuckDBTerminal,
|
|
3370
|
+
U as FG_BLUE,
|
|
3371
|
+
S as FG_CYAN,
|
|
3372
|
+
b as FG_GREEN,
|
|
3373
|
+
g as FG_RED,
|
|
3374
|
+
F as FG_WHITE,
|
|
3375
|
+
N as FG_YELLOW,
|
|
3376
|
+
Pt as HistoryStore,
|
|
3377
|
+
Ct as InputBuffer,
|
|
3378
|
+
Xt as LinkProvider,
|
|
3379
|
+
Ie as PaginationHandler,
|
|
3380
|
+
v as RESET,
|
|
3381
|
+
ct as TerminalAdapter,
|
|
3382
|
+
y as bold,
|
|
3383
|
+
c as colorize,
|
|
3384
|
+
Le as containsURL,
|
|
3385
|
+
J as copyToClipboard,
|
|
3386
|
+
Ce as createCommands,
|
|
3387
|
+
be as createTerminal,
|
|
3388
|
+
qt as darkTheme,
|
|
3389
|
+
u as dim,
|
|
3390
|
+
Pe as embed,
|
|
3391
|
+
Re as extractURLs,
|
|
3392
|
+
O as formatCSV,
|
|
3393
|
+
D as formatJSON,
|
|
3394
|
+
$ as formatTSV,
|
|
3395
|
+
k as formatTable,
|
|
3396
|
+
ee as getSavedTheme,
|
|
3397
|
+
G as getTheme,
|
|
3398
|
+
H as highlightSQL,
|
|
3399
|
+
ye as isClipboardAvailable,
|
|
3400
|
+
Se as isSQLComplete,
|
|
3401
|
+
Ae as isValidURL,
|
|
3402
|
+
te as lightTheme,
|
|
3403
|
+
Vt as linkifyText,
|
|
3404
|
+
Ft as readFromClipboard,
|
|
3405
|
+
ie as saveTheme,
|
|
3406
|
+
Z as tokenize
|
|
3407
|
+
};
|