@lunaperegrina/meowdb 0.0.1
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 +39 -0
- package/dist/bundle.js +1137 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# meowdb CLI
|
|
2
|
+
|
|
3
|
+
```text
|
|
4
|
+
/\_/\\
|
|
5
|
+
( o.o ) meow meow database explorer ~
|
|
6
|
+
> ^ <
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
A tiny, cute terminal app to explore PostgreSQL databases quickly. (´。• ᵕ •。`)
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- Save multiple PostgreSQL connections and keep one active.
|
|
14
|
+
- Add and switch databases with slash commands: `/add`, `/list`, `/tables`.
|
|
15
|
+
- Browse tables from the active database in a split-view UI.
|
|
16
|
+
- Preview table rows (up to 50 by default) with horizontal/vertical scrolling.
|
|
17
|
+
- Navigate with keyboard and mouse wheel/click support.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g meowdb
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Development
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm run dev
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Run
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
meowdb
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
(=^-ω-^=) happy querying!
|
|
39
|
+
```
|
package/dist/bundle.js
ADDED
|
@@ -0,0 +1,1137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.tsx
|
|
4
|
+
import { render } from "ink";
|
|
5
|
+
|
|
6
|
+
// src/app.tsx
|
|
7
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
8
|
+
import { Box, Text, useInput, useStdout } from "ink";
|
|
9
|
+
|
|
10
|
+
// src/storage/databases.ts
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { promises as fs } from "node:fs";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
var CONFIG_DIRECTORY = path.join(os.homedir(), ".config", "meowdb");
|
|
16
|
+
var DATABASES_FILE = path.join(CONFIG_DIRECTORY, "databases.json");
|
|
17
|
+
var createDefaultState = () => ({
|
|
18
|
+
activeDatabaseId: null,
|
|
19
|
+
databases: []
|
|
20
|
+
});
|
|
21
|
+
var isObject = (value) => typeof value === "object" && value !== null;
|
|
22
|
+
var isDatabaseEntry = (value) => {
|
|
23
|
+
if (!isObject(value)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
return typeof value.id === "string" && typeof value.name === "string" && typeof value.postgresUrl === "string" && typeof value.createdAt === "string" && typeof value.updatedAt === "string";
|
|
27
|
+
};
|
|
28
|
+
var normalizeState = (value) => {
|
|
29
|
+
if (!isObject(value) || !Array.isArray(value.databases)) {
|
|
30
|
+
throw new Error("Arquivo de databases inv\xE1lido.");
|
|
31
|
+
}
|
|
32
|
+
const databases = value.databases.filter(isDatabaseEntry);
|
|
33
|
+
const activeDatabaseId = typeof value.activeDatabaseId === "string" ? value.activeDatabaseId : null;
|
|
34
|
+
const hasActiveDatabase = activeDatabaseId !== null && databases.some((database) => database.id === activeDatabaseId);
|
|
35
|
+
return {
|
|
36
|
+
activeDatabaseId: hasActiveDatabase ? activeDatabaseId : null,
|
|
37
|
+
databases
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
var ensureStorage = async () => {
|
|
41
|
+
await fs.mkdir(CONFIG_DIRECTORY, { recursive: true });
|
|
42
|
+
try {
|
|
43
|
+
await fs.access(DATABASES_FILE);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
const nodeError = error;
|
|
46
|
+
if (nodeError.code !== "ENOENT") {
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
await fs.writeFile(DATABASES_FILE, `${JSON.stringify(createDefaultState(), null, 2)}
|
|
50
|
+
`, "utf8");
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
var readState = async () => {
|
|
54
|
+
await ensureStorage();
|
|
55
|
+
const rawContent = await fs.readFile(DATABASES_FILE, "utf8");
|
|
56
|
+
const parsedContent = JSON.parse(rawContent);
|
|
57
|
+
return normalizeState(parsedContent);
|
|
58
|
+
};
|
|
59
|
+
var writeState = async (state) => {
|
|
60
|
+
await ensureStorage();
|
|
61
|
+
await fs.writeFile(DATABASES_FILE, `${JSON.stringify(state, null, 2)}
|
|
62
|
+
`, "utf8");
|
|
63
|
+
};
|
|
64
|
+
var getState = async () => readState();
|
|
65
|
+
var addDatabase = async (input) => {
|
|
66
|
+
const state = await readState();
|
|
67
|
+
const name = input.name.trim();
|
|
68
|
+
const postgresUrl = input.postgresUrl.trim();
|
|
69
|
+
if (name.length === 0) {
|
|
70
|
+
throw new Error("Nome \xE9 obrigat\xF3rio.");
|
|
71
|
+
}
|
|
72
|
+
if (postgresUrl.length === 0) {
|
|
73
|
+
throw new Error("Postgres URL \xE9 obrigat\xF3ria.");
|
|
74
|
+
}
|
|
75
|
+
const hasValidPrefix = postgresUrl.startsWith("postgres://") || postgresUrl.startsWith("postgresql://");
|
|
76
|
+
if (!hasValidPrefix) {
|
|
77
|
+
throw new Error("Postgres URL deve come\xE7ar com postgres:// ou postgresql://.");
|
|
78
|
+
}
|
|
79
|
+
const duplicatedName = state.databases.some(
|
|
80
|
+
(database) => database.name.toLowerCase() === name.toLowerCase()
|
|
81
|
+
);
|
|
82
|
+
if (duplicatedName) {
|
|
83
|
+
throw new Error("J\xE1 existe uma database com esse nome.");
|
|
84
|
+
}
|
|
85
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
86
|
+
const newDatabase = {
|
|
87
|
+
id: randomUUID(),
|
|
88
|
+
name,
|
|
89
|
+
postgresUrl,
|
|
90
|
+
createdAt: timestamp,
|
|
91
|
+
updatedAt: timestamp
|
|
92
|
+
};
|
|
93
|
+
const nextState = {
|
|
94
|
+
activeDatabaseId: newDatabase.id,
|
|
95
|
+
databases: [...state.databases, newDatabase]
|
|
96
|
+
};
|
|
97
|
+
await writeState(nextState);
|
|
98
|
+
return nextState;
|
|
99
|
+
};
|
|
100
|
+
var setActiveDatabase = async (databaseId) => {
|
|
101
|
+
const state = await readState();
|
|
102
|
+
const hasDatabase = state.databases.some((database) => database.id === databaseId);
|
|
103
|
+
if (!hasDatabase) {
|
|
104
|
+
throw new Error("Database selecionada n\xE3o existe.");
|
|
105
|
+
}
|
|
106
|
+
const nextState = {
|
|
107
|
+
...state,
|
|
108
|
+
activeDatabaseId: databaseId
|
|
109
|
+
};
|
|
110
|
+
await writeState(nextState);
|
|
111
|
+
return nextState;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// src/storage/postgres.ts
|
|
115
|
+
import { Client } from "pg";
|
|
116
|
+
var DEFAULT_ROWS_LIMIT = 50;
|
|
117
|
+
var normalizeLimit = (limit) => {
|
|
118
|
+
if (!Number.isFinite(limit)) {
|
|
119
|
+
return DEFAULT_ROWS_LIMIT;
|
|
120
|
+
}
|
|
121
|
+
return Math.max(1, Math.floor(limit));
|
|
122
|
+
};
|
|
123
|
+
var escapeIdentifier = (value) => `"${value.replaceAll('"', '""')}"`;
|
|
124
|
+
var withClient = async (postgresUrl, callback) => {
|
|
125
|
+
const client = new Client({ connectionString: postgresUrl });
|
|
126
|
+
await client.connect();
|
|
127
|
+
try {
|
|
128
|
+
return await callback(client);
|
|
129
|
+
} finally {
|
|
130
|
+
await client.end();
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
var listTables = async (postgresUrl) => withClient(postgresUrl, async (client) => {
|
|
134
|
+
const result = await client.query(
|
|
135
|
+
`SELECT table_schema, table_name
|
|
136
|
+
FROM information_schema.tables
|
|
137
|
+
WHERE table_type = 'BASE TABLE'
|
|
138
|
+
AND table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
139
|
+
ORDER BY table_schema ASC, table_name ASC`
|
|
140
|
+
);
|
|
141
|
+
return result.rows.map((row) => ({
|
|
142
|
+
schema: row.table_schema,
|
|
143
|
+
name: row.table_name,
|
|
144
|
+
qualifiedName: `${row.table_schema}.${row.table_name}`
|
|
145
|
+
}));
|
|
146
|
+
});
|
|
147
|
+
var listTableRows = async (postgresUrl, schema, table, limit = DEFAULT_ROWS_LIMIT) => withClient(postgresUrl, async (client) => {
|
|
148
|
+
const normalizedLimit = normalizeLimit(limit);
|
|
149
|
+
const schemaIdentifier = escapeIdentifier(schema);
|
|
150
|
+
const tableIdentifier = escapeIdentifier(table);
|
|
151
|
+
const query = `SELECT * FROM ${schemaIdentifier}.${tableIdentifier} LIMIT $1`;
|
|
152
|
+
const result = await client.query(query, [normalizedLimit]);
|
|
153
|
+
const columns = result.fields.map((field) => field.name);
|
|
154
|
+
const rows = result.rows.map((row) => {
|
|
155
|
+
const normalizedRow = {};
|
|
156
|
+
for (const column of columns) {
|
|
157
|
+
normalizedRow[column] = row[column];
|
|
158
|
+
}
|
|
159
|
+
return normalizedRow;
|
|
160
|
+
});
|
|
161
|
+
return {
|
|
162
|
+
columns,
|
|
163
|
+
rows,
|
|
164
|
+
limit: normalizedLimit
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// src/app.tsx
|
|
169
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
170
|
+
var FALLBACK_COLUMNS = 80;
|
|
171
|
+
var FALLBACK_ROWS = 24;
|
|
172
|
+
var STATUS_BAR_HEIGHT = 1;
|
|
173
|
+
var INPUT_BAR_HEIGHT = 5;
|
|
174
|
+
var MODAL_HEIGHT = 9;
|
|
175
|
+
var SPLIT_GAP = 2;
|
|
176
|
+
var SIDEBAR_MIN_WIDTH = 24;
|
|
177
|
+
var SIDEBAR_MAX_WIDTH = 44;
|
|
178
|
+
var CONTENT_MIN_WIDTH = 20;
|
|
179
|
+
var ROWS_PREVIEW_LIMIT = 50;
|
|
180
|
+
var ROW_NUMBER_WIDTH = 4;
|
|
181
|
+
var CELL_MIN_WIDTH = 8;
|
|
182
|
+
var CELL_MAX_WIDTH = 24;
|
|
183
|
+
var CELL_SEPARATOR = " | ";
|
|
184
|
+
var MAIN_BACKGROUND = "#0B1020";
|
|
185
|
+
var SURFACE_BACKGROUND = "#131A2A";
|
|
186
|
+
var PRIMARY_TEXT = "#F3F6FC";
|
|
187
|
+
var SECONDARY_TEXT = "#8C97B2";
|
|
188
|
+
var getTerminalSize = (stdout) => ({
|
|
189
|
+
columns: stdout.columns && stdout.columns > 0 ? stdout.columns : FALLBACK_COLUMNS,
|
|
190
|
+
rows: stdout.rows && stdout.rows > 0 ? stdout.rows : FALLBACK_ROWS
|
|
191
|
+
});
|
|
192
|
+
var getSplitPaneSizes = (terminalColumns, hasActiveDatabase) => {
|
|
193
|
+
const mainWidth = Math.max(24, terminalColumns - 2);
|
|
194
|
+
if (!hasActiveDatabase) {
|
|
195
|
+
return {
|
|
196
|
+
mainWidth,
|
|
197
|
+
sidebarWidth: 0,
|
|
198
|
+
contentWidth: mainWidth
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
const preferredSidebar = Math.floor(mainWidth * 0.34);
|
|
202
|
+
const boundedSidebar = Math.min(
|
|
203
|
+
SIDEBAR_MAX_WIDTH,
|
|
204
|
+
Math.max(SIDEBAR_MIN_WIDTH, preferredSidebar)
|
|
205
|
+
);
|
|
206
|
+
const maxSidebarForContent = Math.max(
|
|
207
|
+
SIDEBAR_MIN_WIDTH,
|
|
208
|
+
mainWidth - CONTENT_MIN_WIDTH - SPLIT_GAP
|
|
209
|
+
);
|
|
210
|
+
const sidebarWidth = Math.min(boundedSidebar, maxSidebarForContent);
|
|
211
|
+
const contentWidth = Math.max(
|
|
212
|
+
CONTENT_MIN_WIDTH,
|
|
213
|
+
mainWidth - sidebarWidth - SPLIT_GAP
|
|
214
|
+
);
|
|
215
|
+
return {
|
|
216
|
+
mainWidth,
|
|
217
|
+
sidebarWidth,
|
|
218
|
+
contentWidth
|
|
219
|
+
};
|
|
220
|
+
};
|
|
221
|
+
var slashCommands = [
|
|
222
|
+
{ id: "add", label: "add", description: "Add database" },
|
|
223
|
+
{ id: "list", label: "list", description: "List databases" },
|
|
224
|
+
{ id: "tables", label: "tables", description: "Reload tables for active database" }
|
|
225
|
+
];
|
|
226
|
+
var isNavigationKey = (key) => Boolean(
|
|
227
|
+
key.tab || key.upArrow || key.downArrow || key.leftArrow || key.rightArrow || key.pageUp || key.pageDown || key.escape
|
|
228
|
+
);
|
|
229
|
+
var normalizeInput = (value) => value.replaceAll(/\r?\n/g, "");
|
|
230
|
+
var ESCAPE_INPUT = "\x1B";
|
|
231
|
+
var ENABLE_MOUSE_TRACKING = "\x1B[?1000h\x1B[?1006h";
|
|
232
|
+
var DISABLE_MOUSE_TRACKING = "\x1B[?1000l\x1B[?1002l\x1B[?1003l\x1B[?1005l\x1B[?1006l\x1B[?1015l";
|
|
233
|
+
var SGR_MOUSE_PACKET_PATTERN = /(?:\u001B)?\[<(\d+);(\d+);(\d+)([Mm])/g;
|
|
234
|
+
var wrapIndex = (index, total) => {
|
|
235
|
+
if (total <= 0) {
|
|
236
|
+
return 0;
|
|
237
|
+
}
|
|
238
|
+
if (index < 0) {
|
|
239
|
+
return total - 1;
|
|
240
|
+
}
|
|
241
|
+
if (index >= total) {
|
|
242
|
+
return 0;
|
|
243
|
+
}
|
|
244
|
+
return index;
|
|
245
|
+
};
|
|
246
|
+
var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
247
|
+
var parseSgrMousePacket = (buttonCodeRaw, xRaw, yRaw, marker) => {
|
|
248
|
+
const buttonCode = Number.parseInt(buttonCodeRaw, 10);
|
|
249
|
+
const x = Number.parseInt(xRaw, 10);
|
|
250
|
+
const y = Number.parseInt(yRaw, 10);
|
|
251
|
+
if (Number.isNaN(buttonCode) || Number.isNaN(x) || Number.isNaN(y) || x <= 0 || y <= 0) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
if ((buttonCode & 64) === 64) {
|
|
255
|
+
return {
|
|
256
|
+
type: (buttonCode & 1) === 1 ? "wheelDown" : "wheelUp",
|
|
257
|
+
x: x - 1,
|
|
258
|
+
y: y - 1
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const isButtonPress = marker === "M";
|
|
262
|
+
const isMotion = (buttonCode & 32) === 32;
|
|
263
|
+
const isLeftButton = (buttonCode & 3) === 0;
|
|
264
|
+
if (isButtonPress && !isMotion && isLeftButton) {
|
|
265
|
+
return {
|
|
266
|
+
type: "leftClick",
|
|
267
|
+
x: x - 1,
|
|
268
|
+
y: y - 1
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
272
|
+
};
|
|
273
|
+
var parseSgrMouseInput = (input) => {
|
|
274
|
+
const events = [];
|
|
275
|
+
let hasPacket = false;
|
|
276
|
+
let consumedLength = 0;
|
|
277
|
+
SGR_MOUSE_PACKET_PATTERN.lastIndex = 0;
|
|
278
|
+
let match = SGR_MOUSE_PACKET_PATTERN.exec(input);
|
|
279
|
+
while (match) {
|
|
280
|
+
if (match.index !== consumedLength) {
|
|
281
|
+
return {
|
|
282
|
+
consumed: false,
|
|
283
|
+
events: []
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
hasPacket = true;
|
|
287
|
+
consumedLength += match[0].length;
|
|
288
|
+
const parsedEvent = parseSgrMousePacket(match[1], match[2], match[3], match[4]);
|
|
289
|
+
if (parsedEvent) {
|
|
290
|
+
events.push(parsedEvent);
|
|
291
|
+
}
|
|
292
|
+
match = SGR_MOUSE_PACKET_PATTERN.exec(input);
|
|
293
|
+
}
|
|
294
|
+
if (!hasPacket || consumedLength !== input.length) {
|
|
295
|
+
return {
|
|
296
|
+
consumed: false,
|
|
297
|
+
events: []
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
consumed: true,
|
|
302
|
+
events
|
|
303
|
+
};
|
|
304
|
+
};
|
|
305
|
+
var truncateText = (value, maxLength) => {
|
|
306
|
+
if (maxLength <= 1) {
|
|
307
|
+
return value.length > 0 ? "\u2026" : "";
|
|
308
|
+
}
|
|
309
|
+
if (value.length <= maxLength) {
|
|
310
|
+
return value;
|
|
311
|
+
}
|
|
312
|
+
return `${value.slice(0, maxLength - 1)}\u2026`;
|
|
313
|
+
};
|
|
314
|
+
var padCell = (value, width) => truncateText(value, width).padEnd(width, " ");
|
|
315
|
+
var formatCellValue = (value) => {
|
|
316
|
+
if (value === null) {
|
|
317
|
+
return "null";
|
|
318
|
+
}
|
|
319
|
+
if (value === void 0) {
|
|
320
|
+
return "undefined";
|
|
321
|
+
}
|
|
322
|
+
if (typeof value === "string") {
|
|
323
|
+
return value.replaceAll(/\r?\n/g, " ");
|
|
324
|
+
}
|
|
325
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
326
|
+
return String(value);
|
|
327
|
+
}
|
|
328
|
+
if (value instanceof Date) {
|
|
329
|
+
return value.toISOString();
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
const serialized = JSON.stringify(value);
|
|
333
|
+
if (serialized === void 0) {
|
|
334
|
+
return String(value);
|
|
335
|
+
}
|
|
336
|
+
return serialized.replaceAll(/\r?\n/g, " ");
|
|
337
|
+
} catch {
|
|
338
|
+
return String(value);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
var getErrorMessage = (error) => {
|
|
342
|
+
if (error instanceof Error) {
|
|
343
|
+
return error.message;
|
|
344
|
+
}
|
|
345
|
+
return "Erro desconhecido.";
|
|
346
|
+
};
|
|
347
|
+
var getRowsViewport = (terminalColumns, totalColumns, requestedOffset) => {
|
|
348
|
+
if (totalColumns <= 0) {
|
|
349
|
+
return {
|
|
350
|
+
cellWidth: CELL_MIN_WIDTH,
|
|
351
|
+
visibleColumnCount: 0,
|
|
352
|
+
maxOffset: 0,
|
|
353
|
+
offset: 0
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
const innerWidth = Math.max(24, terminalColumns - 8);
|
|
357
|
+
const availableForCells = Math.max(
|
|
358
|
+
1,
|
|
359
|
+
innerWidth - ROW_NUMBER_WIDTH - CELL_SEPARATOR.length
|
|
360
|
+
);
|
|
361
|
+
const maxVisibleColumnCount = Math.max(
|
|
362
|
+
1,
|
|
363
|
+
Math.floor(
|
|
364
|
+
(availableForCells + CELL_SEPARATOR.length) / (CELL_MIN_WIDTH + CELL_SEPARATOR.length)
|
|
365
|
+
)
|
|
366
|
+
);
|
|
367
|
+
const visibleColumnCount = Math.min(totalColumns, maxVisibleColumnCount);
|
|
368
|
+
const separatorsWidth = CELL_SEPARATOR.length * Math.max(0, visibleColumnCount - 1);
|
|
369
|
+
const cellWidth = Math.min(
|
|
370
|
+
CELL_MAX_WIDTH,
|
|
371
|
+
Math.max(
|
|
372
|
+
CELL_MIN_WIDTH,
|
|
373
|
+
Math.floor((availableForCells - separatorsWidth) / visibleColumnCount)
|
|
374
|
+
)
|
|
375
|
+
);
|
|
376
|
+
const maxOffset = Math.max(0, totalColumns - visibleColumnCount);
|
|
377
|
+
const offset = Math.min(Math.max(0, requestedOffset), maxOffset);
|
|
378
|
+
return {
|
|
379
|
+
cellWidth,
|
|
380
|
+
visibleColumnCount,
|
|
381
|
+
maxOffset,
|
|
382
|
+
offset
|
|
383
|
+
};
|
|
384
|
+
};
|
|
385
|
+
var INITIAL_DATABASE_STATE = {
|
|
386
|
+
activeDatabaseId: null,
|
|
387
|
+
databases: []
|
|
388
|
+
};
|
|
389
|
+
function App() {
|
|
390
|
+
const { stdout } = useStdout();
|
|
391
|
+
const [messages, setMessages] = useState([]);
|
|
392
|
+
const [draft, setDraft] = useState("");
|
|
393
|
+
const [mode, setMode] = useState("chat");
|
|
394
|
+
const [slashQuery, setSlashQuery] = useState("");
|
|
395
|
+
const [slashIndex, setSlashIndex] = useState(0);
|
|
396
|
+
const [formField, setFormField] = useState("name");
|
|
397
|
+
const [formName, setFormName] = useState("");
|
|
398
|
+
const [formPostgresUrl, setFormPostgresUrl] = useState("");
|
|
399
|
+
const [listIndex, setListIndex] = useState(0);
|
|
400
|
+
const [databaseState, setDatabaseState] = useState(INITIAL_DATABASE_STATE);
|
|
401
|
+
const [isLoadingDatabases, setIsLoadingDatabases] = useState(true);
|
|
402
|
+
const [isSavingDatabase, setIsSavingDatabase] = useState(false);
|
|
403
|
+
const [isSettingActive, setIsSettingActive] = useState(false);
|
|
404
|
+
const [tables, setTables] = useState([]);
|
|
405
|
+
const [tablesIndex, setTablesIndex] = useState(0);
|
|
406
|
+
const [isLoadingTables, setIsLoadingTables] = useState(false);
|
|
407
|
+
const [tablesError, setTablesError] = useState(null);
|
|
408
|
+
const [selectedTable, setSelectedTable] = useState(null);
|
|
409
|
+
const [rowsPreview, setRowsPreview] = useState(null);
|
|
410
|
+
const [rowsError, setRowsError] = useState(null);
|
|
411
|
+
const [rowsColumnOffset, setRowsColumnOffset] = useState(0);
|
|
412
|
+
const [rowsWindowStart, setRowsWindowStart] = useState(0);
|
|
413
|
+
const [isLoadingRows, setIsLoadingRows] = useState(false);
|
|
414
|
+
const tablesLoadIdRef = useRef(0);
|
|
415
|
+
const rowsLoadIdRef = useRef(0);
|
|
416
|
+
const [terminalSize, setTerminalSize] = useState(() => getTerminalSize(stdout));
|
|
417
|
+
const rowsCount = rowsPreview?.rows.length ?? 0;
|
|
418
|
+
const rowsColumnCount = rowsPreview?.columns.length ?? 0;
|
|
419
|
+
useEffect(() => {
|
|
420
|
+
const handleResize = () => {
|
|
421
|
+
setTerminalSize(getTerminalSize(stdout));
|
|
422
|
+
};
|
|
423
|
+
stdout.on("resize", handleResize);
|
|
424
|
+
return () => {
|
|
425
|
+
stdout.off("resize", handleResize);
|
|
426
|
+
};
|
|
427
|
+
}, [stdout]);
|
|
428
|
+
useEffect(() => {
|
|
429
|
+
if (!stdout.isTTY) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const disableMouseTracking = () => {
|
|
433
|
+
stdout.write(DISABLE_MOUSE_TRACKING);
|
|
434
|
+
};
|
|
435
|
+
stdout.write(ENABLE_MOUSE_TRACKING);
|
|
436
|
+
process.on("beforeExit", disableMouseTracking);
|
|
437
|
+
process.on("exit", disableMouseTracking);
|
|
438
|
+
return () => {
|
|
439
|
+
process.off("beforeExit", disableMouseTracking);
|
|
440
|
+
process.off("exit", disableMouseTracking);
|
|
441
|
+
disableMouseTracking();
|
|
442
|
+
};
|
|
443
|
+
}, [stdout]);
|
|
444
|
+
useEffect(() => {
|
|
445
|
+
let mounted = true;
|
|
446
|
+
const bootstrapDatabaseState = async () => {
|
|
447
|
+
try {
|
|
448
|
+
const state = await getState();
|
|
449
|
+
if (!mounted) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
setDatabaseState(state);
|
|
453
|
+
} catch (error) {
|
|
454
|
+
if (!mounted) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
setMessages((previous) => [
|
|
458
|
+
...previous,
|
|
459
|
+
`Erro ao carregar databases: ${getErrorMessage(error)}`
|
|
460
|
+
]);
|
|
461
|
+
} finally {
|
|
462
|
+
if (mounted) {
|
|
463
|
+
setIsLoadingDatabases(false);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
void bootstrapDatabaseState();
|
|
468
|
+
return () => {
|
|
469
|
+
mounted = false;
|
|
470
|
+
};
|
|
471
|
+
}, []);
|
|
472
|
+
const pushMessage = (message) => {
|
|
473
|
+
setMessages((previous) => [...previous, message]);
|
|
474
|
+
};
|
|
475
|
+
const filteredCommands = useMemo(() => {
|
|
476
|
+
const query = slashQuery.trim().toLowerCase();
|
|
477
|
+
if (query.length === 0) {
|
|
478
|
+
return slashCommands;
|
|
479
|
+
}
|
|
480
|
+
return slashCommands.filter(
|
|
481
|
+
(command) => command.label.toLowerCase().includes(query)
|
|
482
|
+
);
|
|
483
|
+
}, [slashQuery]);
|
|
484
|
+
useEffect(() => {
|
|
485
|
+
setSlashIndex((previous) => wrapIndex(previous, filteredCommands.length));
|
|
486
|
+
}, [filteredCommands.length]);
|
|
487
|
+
useEffect(() => {
|
|
488
|
+
setListIndex((previous) => wrapIndex(previous, databaseState.databases.length));
|
|
489
|
+
}, [databaseState.databases.length]);
|
|
490
|
+
useEffect(() => {
|
|
491
|
+
setTablesIndex((previous) => wrapIndex(previous, tables.length));
|
|
492
|
+
}, [tables.length]);
|
|
493
|
+
useEffect(() => {
|
|
494
|
+
if (rowsColumnCount === 0) {
|
|
495
|
+
if (rowsColumnOffset !== 0) {
|
|
496
|
+
setRowsColumnOffset(0);
|
|
497
|
+
}
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const splitPaneSizes2 = getSplitPaneSizes(
|
|
501
|
+
terminalSize.columns,
|
|
502
|
+
databaseState.activeDatabaseId !== null
|
|
503
|
+
);
|
|
504
|
+
const viewport = getRowsViewport(
|
|
505
|
+
splitPaneSizes2.contentWidth,
|
|
506
|
+
rowsColumnCount,
|
|
507
|
+
rowsColumnOffset
|
|
508
|
+
);
|
|
509
|
+
if (viewport.offset !== rowsColumnOffset) {
|
|
510
|
+
setRowsColumnOffset(viewport.offset);
|
|
511
|
+
}
|
|
512
|
+
}, [
|
|
513
|
+
databaseState.activeDatabaseId,
|
|
514
|
+
rowsColumnCount,
|
|
515
|
+
rowsColumnOffset,
|
|
516
|
+
terminalSize.columns
|
|
517
|
+
]);
|
|
518
|
+
const activeDatabase = useMemo(
|
|
519
|
+
() => databaseState.databases.find(
|
|
520
|
+
(database) => database.id === databaseState.activeDatabaseId
|
|
521
|
+
) ?? null,
|
|
522
|
+
[databaseState]
|
|
523
|
+
);
|
|
524
|
+
const resetRowsState = useCallback(() => {
|
|
525
|
+
rowsLoadIdRef.current += 1;
|
|
526
|
+
setSelectedTable(null);
|
|
527
|
+
setRowsPreview(null);
|
|
528
|
+
setRowsError(null);
|
|
529
|
+
setRowsColumnOffset(0);
|
|
530
|
+
setRowsWindowStart(0);
|
|
531
|
+
setIsLoadingRows(false);
|
|
532
|
+
}, []);
|
|
533
|
+
const loadTablesForDatabase = useCallback(
|
|
534
|
+
async (database) => {
|
|
535
|
+
const loadId = tablesLoadIdRef.current + 1;
|
|
536
|
+
tablesLoadIdRef.current = loadId;
|
|
537
|
+
setTables([]);
|
|
538
|
+
setTablesIndex(0);
|
|
539
|
+
setTablesError(null);
|
|
540
|
+
resetRowsState();
|
|
541
|
+
if (!database) {
|
|
542
|
+
setIsLoadingTables(false);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
setIsLoadingTables(true);
|
|
546
|
+
try {
|
|
547
|
+
const nextTables = await listTables(database.postgresUrl);
|
|
548
|
+
if (tablesLoadIdRef.current !== loadId) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
setTables(nextTables);
|
|
552
|
+
setTablesIndex(0);
|
|
553
|
+
} catch (error) {
|
|
554
|
+
if (tablesLoadIdRef.current !== loadId) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
setTablesError(getErrorMessage(error));
|
|
558
|
+
} finally {
|
|
559
|
+
if (tablesLoadIdRef.current === loadId) {
|
|
560
|
+
setIsLoadingTables(false);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
},
|
|
564
|
+
[resetRowsState]
|
|
565
|
+
);
|
|
566
|
+
useEffect(() => {
|
|
567
|
+
void loadTablesForDatabase(activeDatabase);
|
|
568
|
+
}, [activeDatabase, loadTablesForDatabase]);
|
|
569
|
+
const openListModal = () => {
|
|
570
|
+
const activeIndex = databaseState.databases.findIndex(
|
|
571
|
+
(database) => database.id === databaseState.activeDatabaseId
|
|
572
|
+
);
|
|
573
|
+
setListIndex(activeIndex >= 0 ? activeIndex : 0);
|
|
574
|
+
setMode("listModal");
|
|
575
|
+
setDraft("");
|
|
576
|
+
setSlashQuery("");
|
|
577
|
+
setSlashIndex(0);
|
|
578
|
+
};
|
|
579
|
+
const closeSlashMenu = () => {
|
|
580
|
+
setMode("chat");
|
|
581
|
+
setSlashQuery("");
|
|
582
|
+
setSlashIndex(0);
|
|
583
|
+
};
|
|
584
|
+
const loadRowsForTable = useCallback(async (table) => {
|
|
585
|
+
if (!activeDatabase) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
const loadId = rowsLoadIdRef.current + 1;
|
|
589
|
+
rowsLoadIdRef.current = loadId;
|
|
590
|
+
setSelectedTable(table);
|
|
591
|
+
setRowsPreview(null);
|
|
592
|
+
setRowsError(null);
|
|
593
|
+
setRowsColumnOffset(0);
|
|
594
|
+
setRowsWindowStart(0);
|
|
595
|
+
setIsLoadingRows(true);
|
|
596
|
+
try {
|
|
597
|
+
const preview = await listTableRows(
|
|
598
|
+
activeDatabase.postgresUrl,
|
|
599
|
+
table.schema,
|
|
600
|
+
table.name,
|
|
601
|
+
ROWS_PREVIEW_LIMIT
|
|
602
|
+
);
|
|
603
|
+
if (rowsLoadIdRef.current !== loadId) {
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
setRowsPreview(preview);
|
|
607
|
+
} catch (error) {
|
|
608
|
+
if (rowsLoadIdRef.current !== loadId) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
setRowsError(getErrorMessage(error));
|
|
612
|
+
} finally {
|
|
613
|
+
if (rowsLoadIdRef.current === loadId) {
|
|
614
|
+
setIsLoadingRows(false);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}, [activeDatabase]);
|
|
618
|
+
const loadRowsForSelectedTable = useCallback(async () => {
|
|
619
|
+
const table = tables[tablesIndex];
|
|
620
|
+
if (!table) {
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
await loadRowsForTable(table);
|
|
624
|
+
}, [loadRowsForTable, tables, tablesIndex]);
|
|
625
|
+
const selectSlashCommand = () => {
|
|
626
|
+
const selectedCommand = filteredCommands[slashIndex];
|
|
627
|
+
if (!selectedCommand) {
|
|
628
|
+
pushMessage("Nenhum comando encontrado.");
|
|
629
|
+
closeSlashMenu();
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (selectedCommand.id === "add") {
|
|
633
|
+
setMode("addForm");
|
|
634
|
+
setFormField("name");
|
|
635
|
+
setFormName("");
|
|
636
|
+
setFormPostgresUrl("");
|
|
637
|
+
setDraft("");
|
|
638
|
+
setSlashQuery("");
|
|
639
|
+
setSlashIndex(0);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (selectedCommand.id === "list") {
|
|
643
|
+
openListModal();
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
closeSlashMenu();
|
|
647
|
+
if (!activeDatabase) {
|
|
648
|
+
pushMessage("Nenhuma database ativa. Use /add ou /list para selecionar uma database.");
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
void loadTablesForDatabase(activeDatabase);
|
|
652
|
+
};
|
|
653
|
+
const submitAddForm = async () => {
|
|
654
|
+
if (isSavingDatabase) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
setIsSavingDatabase(true);
|
|
658
|
+
try {
|
|
659
|
+
const state = await addDatabase({
|
|
660
|
+
name: formName,
|
|
661
|
+
postgresUrl: formPostgresUrl
|
|
662
|
+
});
|
|
663
|
+
const createdDatabase = state.databases.find((database) => database.id === state.activeDatabaseId) ?? null;
|
|
664
|
+
setDatabaseState(state);
|
|
665
|
+
setMode("chat");
|
|
666
|
+
setFormField("name");
|
|
667
|
+
setFormName("");
|
|
668
|
+
setFormPostgresUrl("");
|
|
669
|
+
pushMessage(
|
|
670
|
+
createdDatabase ? `Database "${createdDatabase.name}" salva e ativada.` : "Database salva e ativada."
|
|
671
|
+
);
|
|
672
|
+
} catch (error) {
|
|
673
|
+
pushMessage(`Erro ao salvar database: ${getErrorMessage(error)}`);
|
|
674
|
+
} finally {
|
|
675
|
+
setIsSavingDatabase(false);
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
const activateSelectedDatabase = async () => {
|
|
679
|
+
if (isSettingActive || databaseState.databases.length === 0) {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const selectedDatabase = databaseState.databases[listIndex];
|
|
683
|
+
if (!selectedDatabase) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
setIsSettingActive(true);
|
|
687
|
+
try {
|
|
688
|
+
const state = await setActiveDatabase(selectedDatabase.id);
|
|
689
|
+
setDatabaseState(state);
|
|
690
|
+
setMode("chat");
|
|
691
|
+
pushMessage(`Database ativa: ${selectedDatabase.name}`);
|
|
692
|
+
} catch (error) {
|
|
693
|
+
pushMessage(`Erro ao ativar database: ${getErrorMessage(error)}`);
|
|
694
|
+
} finally {
|
|
695
|
+
setIsSettingActive(false);
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
useInput((input, key) => {
|
|
699
|
+
const parsedMouseInput = parseSgrMouseInput(input);
|
|
700
|
+
if (parsedMouseInput.consumed) {
|
|
701
|
+
if (mode !== "chat" || !hasActiveDatabase) {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
const splitPaneX = 1;
|
|
705
|
+
const splitPaneY = 1;
|
|
706
|
+
const sidebarXStart = splitPaneX;
|
|
707
|
+
const sidebarXEnd = sidebarXStart + splitPaneSizes.sidebarWidth;
|
|
708
|
+
const tablesListYStart = splitPaneY + 1;
|
|
709
|
+
const tablesListYEnd = tablesListYStart + visibleTables.length;
|
|
710
|
+
const contentXStart = sidebarXEnd + SPLIT_GAP;
|
|
711
|
+
const contentXEnd = contentXStart + splitPaneSizes.contentWidth;
|
|
712
|
+
const contentYStart = splitPaneY + 1;
|
|
713
|
+
const contentYEnd = contentYStart + Math.max(0, mainViewportRows - 1);
|
|
714
|
+
for (const mouseEvent of parsedMouseInput.events) {
|
|
715
|
+
const isWithinSidebar = mouseEvent.x >= sidebarXStart && mouseEvent.x < sidebarXEnd && mouseEvent.y >= splitPaneY && mouseEvent.y < splitPaneY + mainViewportRows;
|
|
716
|
+
if (isWithinSidebar) {
|
|
717
|
+
if ((mouseEvent.type === "wheelUp" || mouseEvent.type === "wheelDown") && !isLoadingTables && tables.length > 0) {
|
|
718
|
+
const delta = mouseEvent.type === "wheelDown" ? 1 : -1;
|
|
719
|
+
setTablesIndex((previous) => clamp(previous + delta, 0, tables.length - 1));
|
|
720
|
+
}
|
|
721
|
+
if (mouseEvent.type === "leftClick" && !isLoadingTables && mouseEvent.y >= tablesListYStart && mouseEvent.y < tablesListYEnd) {
|
|
722
|
+
const relativeIndex = mouseEvent.y - tablesListYStart;
|
|
723
|
+
const absoluteIndex = tablesWindowStart + relativeIndex;
|
|
724
|
+
const table = tables[absoluteIndex];
|
|
725
|
+
if (table) {
|
|
726
|
+
setTablesIndex(absoluteIndex);
|
|
727
|
+
void loadRowsForTable(table);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
const isWithinContent = mouseEvent.x >= contentXStart && mouseEvent.x < contentXEnd && mouseEvent.y >= contentYStart && mouseEvent.y < contentYEnd;
|
|
733
|
+
if (isWithinContent && (mouseEvent.type === "wheelUp" || mouseEvent.type === "wheelDown") && rowsPreview && !isLoadingRows) {
|
|
734
|
+
const delta = mouseEvent.type === "wheelDown" ? 1 : -1;
|
|
735
|
+
setRowsWindowStart(
|
|
736
|
+
(previous) => clamp(previous + delta, 0, maxRowsWindowStart)
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
const isEscapePressed = key.escape || input === ESCAPE_INPUT;
|
|
743
|
+
if (isEscapePressed) {
|
|
744
|
+
if (mode === "slashMenu") {
|
|
745
|
+
closeSlashMenu();
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
if (mode === "addForm") {
|
|
749
|
+
setMode("chat");
|
|
750
|
+
setFormField("name");
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (mode === "listModal") {
|
|
754
|
+
setMode("chat");
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (key.ctrl || key.meta) {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
const cleanedInput = normalizeInput(input);
|
|
762
|
+
if (mode === "slashMenu") {
|
|
763
|
+
if (key.return) {
|
|
764
|
+
selectSlashCommand();
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (key.upArrow) {
|
|
768
|
+
setSlashIndex((previous) => wrapIndex(previous - 1, filteredCommands.length));
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
if (key.downArrow || key.tab) {
|
|
772
|
+
setSlashIndex((previous) => wrapIndex(previous + 1, filteredCommands.length));
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
if (key.backspace || key.delete) {
|
|
776
|
+
if (slashQuery.length === 0) {
|
|
777
|
+
closeSlashMenu();
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
setSlashQuery((previous) => previous.slice(0, -1));
|
|
781
|
+
setSlashIndex(0);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
if (isNavigationKey(key)) {
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
if (cleanedInput.length > 0 && cleanedInput !== "/") {
|
|
788
|
+
setSlashQuery((previous) => previous + cleanedInput);
|
|
789
|
+
setSlashIndex(0);
|
|
790
|
+
}
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
if (mode === "addForm") {
|
|
794
|
+
if (key.tab || key.upArrow || key.downArrow) {
|
|
795
|
+
setFormField((previous) => previous === "name" ? "postgresUrl" : "name");
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
if (key.return) {
|
|
799
|
+
void submitAddForm();
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
if (key.backspace || key.delete) {
|
|
803
|
+
if (formField === "name") {
|
|
804
|
+
setFormName((previous) => previous.slice(0, -1));
|
|
805
|
+
} else {
|
|
806
|
+
setFormPostgresUrl((previous) => previous.slice(0, -1));
|
|
807
|
+
}
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
if (isNavigationKey(key)) {
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
if (cleanedInput.length > 0) {
|
|
814
|
+
if (formField === "name") {
|
|
815
|
+
setFormName((previous) => previous + cleanedInput);
|
|
816
|
+
} else {
|
|
817
|
+
setFormPostgresUrl((previous) => previous + cleanedInput);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
if (mode === "listModal") {
|
|
823
|
+
if (key.upArrow) {
|
|
824
|
+
setListIndex((previous) => wrapIndex(previous - 1, databaseState.databases.length));
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
if (key.downArrow || key.tab) {
|
|
828
|
+
setListIndex((previous) => wrapIndex(previous + 1, databaseState.databases.length));
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
if (key.return) {
|
|
832
|
+
void activateSelectedDatabase();
|
|
833
|
+
}
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (key.upArrow) {
|
|
837
|
+
if (!isLoadingTables && tables.length > 0) {
|
|
838
|
+
setTablesIndex((previous) => wrapIndex(previous - 1, tables.length));
|
|
839
|
+
}
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
if (key.downArrow || key.tab) {
|
|
843
|
+
if (!isLoadingTables && tables.length > 0) {
|
|
844
|
+
setTablesIndex((previous) => wrapIndex(previous + 1, tables.length));
|
|
845
|
+
}
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
if (key.leftArrow) {
|
|
849
|
+
if (rowsPreview) {
|
|
850
|
+
setRowsColumnOffset((previous) => Math.max(0, previous - 1));
|
|
851
|
+
}
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
if (key.rightArrow) {
|
|
855
|
+
if (rowsPreview) {
|
|
856
|
+
setRowsColumnOffset((previous) => Math.min(rowsViewport.maxOffset, previous + 1));
|
|
857
|
+
}
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
if (input === "/" && draft.length === 0) {
|
|
861
|
+
setMode("slashMenu");
|
|
862
|
+
setSlashQuery("");
|
|
863
|
+
setSlashIndex(0);
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
if (key.return) {
|
|
867
|
+
const message = draft.trim();
|
|
868
|
+
if (message.length > 0) {
|
|
869
|
+
setMessages((previous) => [...previous, message]);
|
|
870
|
+
setDraft("");
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
if (!isLoadingRows && !isLoadingTables && activeDatabase && tables.length > 0) {
|
|
874
|
+
void loadRowsForSelectedTable();
|
|
875
|
+
}
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
if (key.backspace || key.delete) {
|
|
879
|
+
setDraft((previous) => previous.slice(0, -1));
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
if (key.escape || key.pageUp || key.pageDown) {
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
if (cleanedInput.length > 0) {
|
|
886
|
+
setDraft((previous) => previous + cleanedInput);
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
const modalRows = mode === "slashMenu" || mode === "addForm" || mode === "listModal" ? MODAL_HEIGHT : 0;
|
|
890
|
+
const mainViewportRows = Math.max(
|
|
891
|
+
1,
|
|
892
|
+
terminalSize.rows - INPUT_BAR_HEIGHT - STATUS_BAR_HEIGHT - modalRows - 2
|
|
893
|
+
);
|
|
894
|
+
const visibleMessages = messages.slice(-mainViewportRows);
|
|
895
|
+
const hasActiveDatabase = activeDatabase !== null;
|
|
896
|
+
const splitPaneSizes = useMemo(
|
|
897
|
+
() => getSplitPaneSizes(terminalSize.columns, hasActiveDatabase),
|
|
898
|
+
[hasActiveDatabase, terminalSize.columns]
|
|
899
|
+
);
|
|
900
|
+
const inputLabel = mode === "chat" ? `\u203A ${draft}` : mode === "slashMenu" ? `\u203A /${slashQuery}` : mode === "addForm" ? "\u203A preenchendo formul\xE1rio de database" : "\u203A selecionando database ativa";
|
|
901
|
+
const dbIndicator = isLoadingDatabases ? "loading..." : activeDatabase ? `${activeDatabase.name}` : "none";
|
|
902
|
+
const listEntries = databaseState.databases;
|
|
903
|
+
const listMaxUrlLength = Math.max(12, terminalSize.columns - 18);
|
|
904
|
+
const tableNameMaxLength = Math.max(10, splitPaneSizes.sidebarWidth - 4);
|
|
905
|
+
const selectedSidebarTable = tables[tablesIndex] ?? null;
|
|
906
|
+
const rowsViewport = useMemo(() => {
|
|
907
|
+
if (!rowsPreview) {
|
|
908
|
+
return {
|
|
909
|
+
cellWidth: CELL_MIN_WIDTH,
|
|
910
|
+
visibleColumnCount: 0,
|
|
911
|
+
maxOffset: 0,
|
|
912
|
+
offset: 0,
|
|
913
|
+
visibleColumns: []
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
const viewport = getRowsViewport(
|
|
917
|
+
splitPaneSizes.contentWidth,
|
|
918
|
+
rowsPreview.columns.length,
|
|
919
|
+
rowsColumnOffset
|
|
920
|
+
);
|
|
921
|
+
return {
|
|
922
|
+
...viewport,
|
|
923
|
+
visibleColumns: rowsPreview.columns.slice(
|
|
924
|
+
viewport.offset,
|
|
925
|
+
viewport.offset + viewport.visibleColumnCount
|
|
926
|
+
)
|
|
927
|
+
};
|
|
928
|
+
}, [rowsPreview, rowsColumnOffset, splitPaneSizes.contentWidth]);
|
|
929
|
+
const rowsHeaderLine = useMemo(() => {
|
|
930
|
+
if (rowsViewport.visibleColumns.length === 0) {
|
|
931
|
+
return "";
|
|
932
|
+
}
|
|
933
|
+
const headerColumns = rowsViewport.visibleColumns.map((column) => padCell(column, rowsViewport.cellWidth)).join(CELL_SEPARATOR);
|
|
934
|
+
return `${padCell("row", ROW_NUMBER_WIDTH)}${CELL_SEPARATOR}${headerColumns}`;
|
|
935
|
+
}, [rowsViewport]);
|
|
936
|
+
const rowsWindowSize = Math.max(1, mainViewportRows - 6);
|
|
937
|
+
const maxRowsWindowStart = Math.max(0, rowsCount - rowsWindowSize);
|
|
938
|
+
useEffect(() => {
|
|
939
|
+
setRowsWindowStart((previous) => clamp(previous, 0, maxRowsWindowStart));
|
|
940
|
+
}, [maxRowsWindowStart]);
|
|
941
|
+
const visibleRows = rowsPreview ? rowsPreview.rows.slice(rowsWindowStart, rowsWindowStart + rowsWindowSize) : [];
|
|
942
|
+
const visibleRowsStart = visibleRows.length > 0 ? rowsWindowStart + 1 : 0;
|
|
943
|
+
const visibleRowsEnd = rowsWindowStart + visibleRows.length;
|
|
944
|
+
const tablesWindowSize = Math.max(1, mainViewportRows - 4);
|
|
945
|
+
const maxTablesWindowStart = Math.max(0, tables.length - tablesWindowSize);
|
|
946
|
+
const tablesWindowStart = Math.min(
|
|
947
|
+
maxTablesWindowStart,
|
|
948
|
+
Math.max(0, tablesIndex - Math.floor(tablesWindowSize / 2))
|
|
949
|
+
);
|
|
950
|
+
const visibleTables = tables.slice(
|
|
951
|
+
tablesWindowStart,
|
|
952
|
+
tablesWindowStart + tablesWindowSize
|
|
953
|
+
);
|
|
954
|
+
return /* @__PURE__ */ jsxs(
|
|
955
|
+
Box,
|
|
956
|
+
{
|
|
957
|
+
flexDirection: "column",
|
|
958
|
+
width: terminalSize.columns,
|
|
959
|
+
height: terminalSize.rows,
|
|
960
|
+
children: [
|
|
961
|
+
/* @__PURE__ */ jsx(
|
|
962
|
+
Box,
|
|
963
|
+
{
|
|
964
|
+
flexDirection: "column",
|
|
965
|
+
flexGrow: 1,
|
|
966
|
+
width: terminalSize.columns,
|
|
967
|
+
padding: 1,
|
|
968
|
+
overflow: "hidden",
|
|
969
|
+
backgroundColor: MAIN_BACKGROUND,
|
|
970
|
+
children: hasActiveDatabase ? /* @__PURE__ */ jsxs(Box, { flexDirection: "row", flexGrow: 1, overflow: "hidden", children: [
|
|
971
|
+
/* @__PURE__ */ jsxs(
|
|
972
|
+
Box,
|
|
973
|
+
{
|
|
974
|
+
flexDirection: "column",
|
|
975
|
+
width: splitPaneSizes.sidebarWidth,
|
|
976
|
+
overflow: "hidden",
|
|
977
|
+
children: [
|
|
978
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: PRIMARY_TEXT, children: `Tables \u2022 ${truncateText(activeDatabase?.name ?? "", Math.max(10, splitPaneSizes.sidebarWidth - 12))}` }),
|
|
979
|
+
isLoadingTables ? /* @__PURE__ */ jsx(Text, { color: SECONDARY_TEXT, children: "Carregando tabelas..." }) : tablesError ? /* @__PURE__ */ jsx(Text, { color: "red", children: `Erro ao carregar tabelas: ${tablesError}` }) : tables.length === 0 ? /* @__PURE__ */ jsx(Text, { color: SECONDARY_TEXT, children: "Nenhuma tabela encontrada." }) : visibleTables.map((table, index) => {
|
|
980
|
+
const absoluteIndex = tablesWindowStart + index;
|
|
981
|
+
const isSelected = absoluteIndex === tablesIndex;
|
|
982
|
+
return /* @__PURE__ */ jsx(Text, { color: isSelected ? "magenta" : PRIMARY_TEXT, children: `${isSelected ? "\u203A" : " "} ${truncateText(table.qualifiedName, tableNameMaxLength)}` }, table.qualifiedName);
|
|
983
|
+
}),
|
|
984
|
+
/* @__PURE__ */ jsx(Text, { color: SECONDARY_TEXT, children: tables.length > 0 ? `${tablesIndex + 1}/${tables.length} \u2022 enter/click carrega rows` : "use /tables para recarregar" })
|
|
985
|
+
]
|
|
986
|
+
}
|
|
987
|
+
),
|
|
988
|
+
/* @__PURE__ */ jsx(Box, { width: SPLIT_GAP }),
|
|
989
|
+
/* @__PURE__ */ jsxs(
|
|
990
|
+
Box,
|
|
991
|
+
{
|
|
992
|
+
flexDirection: "column",
|
|
993
|
+
flexGrow: 1,
|
|
994
|
+
width: splitPaneSizes.contentWidth,
|
|
995
|
+
overflow: "hidden",
|
|
996
|
+
children: [
|
|
997
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: PRIMARY_TEXT, children: selectedTable ? `Rows \u2022 ${truncateText(selectedTable.qualifiedName, Math.max(10, splitPaneSizes.contentWidth - 12))}` : selectedSidebarTable ? `Rows \u2022 ${truncateText(selectedSidebarTable.qualifiedName, Math.max(10, splitPaneSizes.contentWidth - 12))}` : "Rows" }),
|
|
998
|
+
isLoadingRows ? /* @__PURE__ */ jsx(Text, { color: SECONDARY_TEXT, children: "Carregando rows..." }) : rowsError ? /* @__PURE__ */ jsx(Text, { color: "red", children: `Erro ao carregar rows: ${rowsError}` }) : !selectedTable || !rowsPreview ? /* @__PURE__ */ jsx(Text, { color: SECONDARY_TEXT, children: "Clique ou selecione uma table na sidebar para carregar rows." }) : rowsViewport.visibleColumns.length === 0 ? /* @__PURE__ */ jsx(Text, { color: SECONDARY_TEXT, children: "N\xE3o h\xE1 colunas para exibir." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
999
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: rowsHeaderLine }),
|
|
1000
|
+
rowsPreview.rows.length === 0 ? /* @__PURE__ */ jsx(Text, { color: SECONDARY_TEXT, children: "Sem rows para exibir." }) : visibleRows.map((row, index) => {
|
|
1001
|
+
const absoluteIndex = rowsWindowStart + index;
|
|
1002
|
+
const rowLabel = padCell(String(absoluteIndex + 1), ROW_NUMBER_WIDTH);
|
|
1003
|
+
const rowCells = rowsViewport.visibleColumns.map((column) => padCell(formatCellValue(row[column]), rowsViewport.cellWidth)).join(CELL_SEPARATOR);
|
|
1004
|
+
const rowLine = `${rowLabel}${CELL_SEPARATOR}${rowCells}`;
|
|
1005
|
+
return /* @__PURE__ */ jsx(Text, { color: PRIMARY_TEXT, children: rowLine }, `${absoluteIndex}-${rowLine}`);
|
|
1006
|
+
}),
|
|
1007
|
+
/* @__PURE__ */ jsx(Text, { color: SECONDARY_TEXT, children: `rows ${visibleRowsStart}-${visibleRowsEnd}/${rowsCount} (limit ${rowsPreview.limit})` }),
|
|
1008
|
+
/* @__PURE__ */ jsx(Text, { color: SECONDARY_TEXT, children: `colunas ${rowsViewport.offset + 1}-${rowsViewport.offset + rowsViewport.visibleColumns.length}/${rowsPreview.columns.length}` })
|
|
1009
|
+
] }),
|
|
1010
|
+
/* @__PURE__ */ jsx(Text, { color: SECONDARY_TEXT, children: "\u2191/\u2193 tables \u2022 click carrega rows \u2022 wheel sidebar/rows \u2022 \u2190/\u2192 colunas" })
|
|
1011
|
+
]
|
|
1012
|
+
}
|
|
1013
|
+
)
|
|
1014
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1015
|
+
visibleMessages.length === 0 ? /* @__PURE__ */ jsx(Text, { color: SECONDARY_TEXT, children: "Digite / para abrir o slash menu." }) : null,
|
|
1016
|
+
visibleMessages.map((message, index) => /* @__PURE__ */ jsx(Text, { color: PRIMARY_TEXT, children: message }, `${index}-${message}`))
|
|
1017
|
+
] })
|
|
1018
|
+
}
|
|
1019
|
+
),
|
|
1020
|
+
mode === "slashMenu" ? /* @__PURE__ */ jsx(
|
|
1021
|
+
Box,
|
|
1022
|
+
{
|
|
1023
|
+
width: terminalSize.columns,
|
|
1024
|
+
paddingX: 1,
|
|
1025
|
+
paddingBottom: 1,
|
|
1026
|
+
backgroundColor: MAIN_BACKGROUND,
|
|
1027
|
+
children: /* @__PURE__ */ jsx(
|
|
1028
|
+
Box,
|
|
1029
|
+
{
|
|
1030
|
+
flexDirection: "column",
|
|
1031
|
+
borderStyle: "round",
|
|
1032
|
+
borderColor: "cyan",
|
|
1033
|
+
paddingX: 1,
|
|
1034
|
+
width: Math.max(24, terminalSize.columns - 2),
|
|
1035
|
+
height: MODAL_HEIGHT,
|
|
1036
|
+
children: filteredCommands.length === 0 ? /* @__PURE__ */ jsx(Text, { color: "red", children: "Nenhum comando encontrado." }) : filteredCommands.map((command, index) => {
|
|
1037
|
+
const isSelected = index === slashIndex;
|
|
1038
|
+
return /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
|
|
1039
|
+
/* @__PURE__ */ jsx(Text, { color: isSelected ? "cyan" : PRIMARY_TEXT, children: `${isSelected ? "\u203A" : " "} /${command.label}` }),
|
|
1040
|
+
/* @__PURE__ */ jsx(Text, { color: SECONDARY_TEXT, children: command.description })
|
|
1041
|
+
] }, command.id);
|
|
1042
|
+
})
|
|
1043
|
+
}
|
|
1044
|
+
)
|
|
1045
|
+
}
|
|
1046
|
+
) : null,
|
|
1047
|
+
mode === "addForm" ? /* @__PURE__ */ jsx(
|
|
1048
|
+
Box,
|
|
1049
|
+
{
|
|
1050
|
+
width: terminalSize.columns,
|
|
1051
|
+
paddingX: 1,
|
|
1052
|
+
paddingBottom: 1,
|
|
1053
|
+
backgroundColor: MAIN_BACKGROUND,
|
|
1054
|
+
children: /* @__PURE__ */ jsxs(
|
|
1055
|
+
Box,
|
|
1056
|
+
{
|
|
1057
|
+
flexDirection: "column",
|
|
1058
|
+
borderStyle: "round",
|
|
1059
|
+
borderColor: "green",
|
|
1060
|
+
paddingX: 1,
|
|
1061
|
+
width: Math.max(24, terminalSize.columns - 2),
|
|
1062
|
+
height: MODAL_HEIGHT,
|
|
1063
|
+
children: [
|
|
1064
|
+
/* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
|
|
1065
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: PRIMARY_TEXT, children: "Add database" }),
|
|
1066
|
+
/* @__PURE__ */ jsx(Text, { color: SECONDARY_TEXT, children: "esc" })
|
|
1067
|
+
] }),
|
|
1068
|
+
/* @__PURE__ */ jsx(Text, { color: formField === "name" ? "green" : PRIMARY_TEXT, children: `name: ${formField === "name" ? "\u203A" : " "} ${formName.length > 0 ? formName : "..."}` }),
|
|
1069
|
+
/* @__PURE__ */ jsx(Text, { color: formField === "postgresUrl" ? "green" : PRIMARY_TEXT, children: `postgresURL: ${formField === "postgresUrl" ? "\u203A" : " "} ${formPostgresUrl.length > 0 ? formPostgresUrl : "..."}` }),
|
|
1070
|
+
/* @__PURE__ */ jsx(Text, { color: SECONDARY_TEXT, children: "tab alterna campo \u2022 enter salva \u2022 esc cancela" }),
|
|
1071
|
+
/* @__PURE__ */ jsx(Text, { color: SECONDARY_TEXT, children: isSavingDatabase ? "Salvando database..." : "URL aceita: postgres:// ou postgresql://" })
|
|
1072
|
+
]
|
|
1073
|
+
}
|
|
1074
|
+
)
|
|
1075
|
+
}
|
|
1076
|
+
) : null,
|
|
1077
|
+
mode === "listModal" ? /* @__PURE__ */ jsx(
|
|
1078
|
+
Box,
|
|
1079
|
+
{
|
|
1080
|
+
width: terminalSize.columns,
|
|
1081
|
+
paddingX: 1,
|
|
1082
|
+
paddingBottom: 1,
|
|
1083
|
+
backgroundColor: MAIN_BACKGROUND,
|
|
1084
|
+
children: /* @__PURE__ */ jsxs(
|
|
1085
|
+
Box,
|
|
1086
|
+
{
|
|
1087
|
+
flexDirection: "column",
|
|
1088
|
+
borderStyle: "round",
|
|
1089
|
+
borderColor: "yellow",
|
|
1090
|
+
paddingX: 1,
|
|
1091
|
+
width: Math.max(24, terminalSize.columns - 2),
|
|
1092
|
+
height: MODAL_HEIGHT,
|
|
1093
|
+
children: [
|
|
1094
|
+
/* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
|
|
1095
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: PRIMARY_TEXT, children: "Databases" }),
|
|
1096
|
+
/* @__PURE__ */ jsx(Text, { color: SECONDARY_TEXT, children: "esc" })
|
|
1097
|
+
] }),
|
|
1098
|
+
listEntries.length === 0 ? /* @__PURE__ */ jsx(Text, { color: SECONDARY_TEXT, children: "Nenhuma database cadastrada." }) : listEntries.map((database, index) => {
|
|
1099
|
+
const isSelected = index === listIndex;
|
|
1100
|
+
const isActive = database.id === databaseState.activeDatabaseId;
|
|
1101
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
1102
|
+
/* @__PURE__ */ jsx(Text, { color: isSelected ? "yellow" : PRIMARY_TEXT, children: `${isSelected ? "\u203A" : " "} ${database.name}${isActive ? " (active)" : ""}` }),
|
|
1103
|
+
/* @__PURE__ */ jsx(Text, { color: SECONDARY_TEXT, children: ` ${truncateText(database.postgresUrl, listMaxUrlLength)}` })
|
|
1104
|
+
] }, database.id);
|
|
1105
|
+
}),
|
|
1106
|
+
/* @__PURE__ */ jsx(Text, { color: SECONDARY_TEXT, children: isSettingActive && "Ativando database..." })
|
|
1107
|
+
]
|
|
1108
|
+
}
|
|
1109
|
+
)
|
|
1110
|
+
}
|
|
1111
|
+
) : null,
|
|
1112
|
+
/* @__PURE__ */ jsxs(
|
|
1113
|
+
Box,
|
|
1114
|
+
{
|
|
1115
|
+
width: terminalSize.columns,
|
|
1116
|
+
height: INPUT_BAR_HEIGHT,
|
|
1117
|
+
padding: 1,
|
|
1118
|
+
backgroundColor: SURFACE_BACKGROUND,
|
|
1119
|
+
flexDirection: "column",
|
|
1120
|
+
justifyContent: "space-between",
|
|
1121
|
+
children: [
|
|
1122
|
+
/* @__PURE__ */ jsx(Text, { color: PRIMARY_TEXT, children: inputLabel }),
|
|
1123
|
+
/* @__PURE__ */ jsxs(Text, { color: PRIMARY_TEXT, children: [
|
|
1124
|
+
/* @__PURE__ */ jsx(Text, { color: "green", children: "Database " }),
|
|
1125
|
+
dbIndicator
|
|
1126
|
+
] })
|
|
1127
|
+
]
|
|
1128
|
+
}
|
|
1129
|
+
)
|
|
1130
|
+
]
|
|
1131
|
+
}
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// src/index.tsx
|
|
1136
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
1137
|
+
render(/* @__PURE__ */ jsx2(App, {}));
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lunaperegrina/meowdb",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"bin": "dist/bundle.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=16"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "rm -rf dist && tsc --noEmit && esbuild src/index.tsx --bundle --platform=node --format=esm --packages=external --outfile=dist/bundle.js",
|
|
12
|
+
"local": "npm run build && npm link",
|
|
13
|
+
"dev": "bun --hot src/index.tsx"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@types/react": "^19.2.14",
|
|
20
|
+
"ink": "^6.8.0",
|
|
21
|
+
"pg": "^8.20.0",
|
|
22
|
+
"react": "^19.2.4"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^25.5.0",
|
|
26
|
+
"@types/pg": "^8.18.0",
|
|
27
|
+
"esbuild": "^0.27.4",
|
|
28
|
+
"ts-node": "^10.9.2",
|
|
29
|
+
"typescript": "^5.9.3"
|
|
30
|
+
}
|
|
31
|
+
}
|