DuckDB Terminal
A browser-based SQL Terminal for DuckDB powered by Ghostty terminal emulator.
Try it live
The latest version is always deployed to https://terminal.sql-workbench.com.
API Docs
The TypeScript API Docs can be found at https://tobilg.github.io/duckdb-terminal.
Features
- Full SQL REPL - Execute SQL queries with multi-line support
- Command History - Navigate previous commands with arrow keys (persisted in IndexedDB)
- Auto-Complete - Tab completion for SQL keywords, table names, and functions
- Multiple Output Modes - Table, CSV, TSV, or JSON output formats
- Clipboard Support - Copy query results to clipboard in any output format
- Result Pagination - Navigate large result sets page by page
- Syntax Highlighting - Color-coded SQL keywords, strings, and numbers
- Clickable URLs - Automatically detect and make URLs clickable in results
- File Loading - Load CSV, Parquet, and JSON files via drag-and-drop or file picker
- Dark/Light Themes - Switchable themes with custom theme support
- Customizable Prompts - Configure primary and continuation prompts
- Dot Commands - Terminal commands like
.help,.tables,.schema - Query Timing - Optional execution time display
- Persistent Storage - Optional OPFS storage for data persistence
- Interactive Charts - Visualize query results with auto-detected chart types (line, bar, scatter, histogram)
Architecture
┌─────────────────────────────────────────────────────────────────────────────┐
│ Browser │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ DuckDB Terminal │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────────┐ │ │
│ │ │ Terminal │ │ │
│ │ │ │ │ │
│ │ │ - REPL (input, output, history) │ │ │
│ │ │ - Command parsing (SQL, dot commands, multi-line) │ │ │
│ │ │ - Result formatting (table, CSV, TSV, JSON) │ │ │
│ │ │ - Syntax highlighting │ │ │
│ │ └───────┬─────────────────────┬─────────────────────────┬─────────┘ │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ │ │ │
│ │ ┌───────────────┐ ┌─────────────────────┐ │ │ │
│ │ │ Terminal │ │ Database │ │ │ │
│ │ │ Adapter │ │ │ │ │ │
│ │ │ │ │ - DuckDB WASM │ │ │ │
│ │ │ - Ghostty │ │ wrapper │ │ │ │
│ │ │ - Themes │ │ - Query execution │ │ │ │
│ │ │ - Keyboard │ │ - Auto-complete │ │ │ │
│ │ │ - Mobile │ │ - File loading │ │ │ │
│ │ └───────┬───────┘ └─────────┬───────────┘ │ │ │
│ │ │ │ │ │ │
│ └──────────┼─────────────────────┼─────────────────────────┼────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ Ghostty Web │ │ DuckDB WASM │ │ IndexedDB │ │
│ │ (npm package) │ │ (Web Worker) │ │ (Command History) │ │
│ │ │ │ │ │ │ │
│ │ - Canvas rendering │ │ - SQL engine │ └─────────────────────┘ │
│ │ - VT100 emulation │ │ - Query processing │ │
│ └────────────────────┘ └──────────┬──────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ OPFS │ │
│ │ (Database Storage) │ │
│ └─────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Installation
npm install duckdb-terminal
Quick Start
As a Library
import { createTerminal } from 'duckdb-terminal'; const Terminal = await createTerminal({ container: '#terminal', theme: 'dark', });
As a Standalone App
git clone https://github.com/tobilg/duckdb-terminal.git
cd duckdb-terminal
npm install
npm run devOpen http://localhost:5173 in your browser.
Configuration
interface TerminalConfig { // Container element or CSS selector container: HTMLElement | string; // Font family (default: 'Fira Code', 'Cascadia Code', etc.) fontFamily?: string; // Font size in pixels (default: 14) fontSize?: number; // Theme: 'dark' | 'light' | Theme (default: 'dark') theme?: 'dark' | 'light' | Theme; // Storage: 'memory' | 'opfs' (default: 'memory') storage?: 'memory' | 'opfs'; // Database path for OPFS storage databasePath?: string; // Show welcome message (default: true) // When enabled, displays loading progress during DuckDB initialization welcomeMessage?: boolean; // Primary prompt string (default: '🦆 ') prompt?: string; // Continuation prompt for multi-line SQL (default: ' > ') continuationPrompt?: string; // Enable clickable URL detection (default: true) linkDetection?: boolean; // Scrollback buffer size in bytes (default: 10485760 = 10MB) scrollback?: number; // Enable interactive charts feature (default: false) enableCharts?: boolean; }
Terminal Commands
| Command | Description |
|---|---|
.help |
Show available commands |
.clear |
Clear the terminal |
.clearhistory |
Clear command history |
.tables |
List all tables |
.schema <table> |
Show table schema |
.timer on|off |
Toggle query timing |
.mode table|csv|tsv|json |
Set output format |
.copy |
Copy last query results to clipboard |
.download [filename] |
Download last result as file (format based on mode) |
.pagesize <n> |
Set pagination size (0 to disable) |
.theme dark|light |
Switch color theme (clears screen) |
.highlight on|off |
Toggle syntax highlighting |
.links on|off |
Toggle clickable URL detection |
.files [list|add|remove] |
Manage loaded files (list, add, or remove) |
.open |
Open file picker to load data files |
.prompt [primary [cont]] |
Get or set the command prompt |
.examples |
Show example queries |
.reset |
Reset database and all settings to defaults |
.chart [options] |
Show interactive chart of last query result |
Charts
The terminal includes an interactive charting feature powered by uPlot. Charts are displayed as an overlay on top of the terminal and support hover tooltips, legends, and PNG export.
Note: Charts must be enabled in the configuration with
enableCharts: true. The uPlot library (~50KB) is loaded from CDN on first use.
Basic Usage
-- Run a query, then visualize it SELECT date, revenue, cost FROM sales; .chart
Chart Type Auto-Detection
The chart type is automatically detected based on your data:
| Data Pattern | Chart Type | Example |
|---|---|---|
| Temporal + Numeric columns | Line | DATE + revenue → time series |
| Categorical + Numeric columns | Bar | category + total → bar chart |
| Two Numeric columns only | Scatter | x_value + y_value → scatter plot |
| Single Numeric column | Histogram | value → distribution histogram |
| Multiple Numeric columns | Line | col1, col2, col3 → multi-series line |
Command Options
| Option | Description | Example |
|---|---|---|
type=TYPE |
Force chart type: line, bar, scatter, histogram |
.chart type=bar |
x=COLUMN |
Specify X-axis column | .chart x=date |
y=COLUMN |
Specify Y-axis column(s), comma-separated | .chart y=revenue,cost |
export |
Export chart as PNG | .chart export |
Examples
-- Line chart: Time series with multiple series SELECT DATE '2024-01-01' + INTERVAL (i) DAY AS date, 100 + random() * 50 AS revenue, 80 + random() * 30 AS cost FROM generate_series(0, 11) AS t(i); .chart -- Bar chart: Categorical data SELECT category, SUM(amount) as total FROM (VALUES ('Electronics', 1500), ('Clothing', 800), ('Food', 1200), ('Books', 400), ('Toys', 600) ) AS t(category, amount) GROUP BY category; .chart -- Scatter plot: Two numeric columns SELECT random() * 100 AS x_value, random() * 100 AS y_value FROM generate_series(1, 50); .chart -- Histogram: Single numeric column distribution SELECT (random() * 100)::INTEGER AS value FROM generate_series(1, 200); .chart -- Multi-series bar chart SELECT region, SUM(CASE WHEN product = 'A' THEN sales ELSE 0 END) as product_a, SUM(CASE WHEN product = 'B' THEN sales ELSE 0 END) as product_b FROM (VALUES ('North', 'A', 120), ('North', 'B', 90), ('South', 'A', 80), ('South', 'B', 150), ('East', 'A', 200), ('East', 'B', 110), ('West', 'A', 95), ('West', 'B', 130) ) AS t(region, product, sales) GROUP BY region; .chart -- Line chart with numeric X axis (sine/cosine waves) SELECT i AS x, sin(i * 0.5) * 50 + 50 AS sine_wave, cos(i * 0.5) * 50 + 50 AS cosine_wave FROM generate_series(0, 20) AS t(i); .chart -- Force specific chart type and axes .chart type=line x=date y=revenue,cost
Chart Interaction
| Key | Action |
|---|---|
| Hover | Show tooltip with values at cursor position |
ESC |
Close the chart |
Ctrl+S / Cmd+S |
Export chart as PNG |
Keyboard Shortcuts
| Key | Action |
|---|---|
Enter |
Execute command/SQL |
Tab |
Auto-complete |
Backspace |
Delete character before cursor |
Delete |
Delete character at cursor |
↑ / ↓ |
Navigate history |
← / → |
Move cursor |
Home |
Move to start of line |
End |
Move to end of line |
Ctrl+A |
Move to start of line |
Ctrl+E |
Move to end of line |
Ctrl+K |
Clear from cursor to end of line |
Ctrl+U |
Clear entire line |
Ctrl+V |
Paste from clipboard |
Ctrl+C |
Cancel current input |
Pagination Mode
When viewing paginated results, the following keys are available:
| Key | Action |
|---|---|
n / ↓ / Enter |
Next page |
p / ↑ |
Previous page |
q / Escape / Ctrl+C |
Quit pagination |
Example Usage
-- Create a table CREATE TABLE users ( id INTEGER PRIMARY KEY, name VARCHAR, email VARCHAR ); -- Insert data INSERT INTO users VALUES (1, 'Alice', 'alice@example.com'), (2, 'Bob', 'bob@example.com'); -- Query data SELECT * FROM users WHERE name LIKE 'A%'; -- Use built-in functions SELECT range(10), current_timestamp;
Querying and Copying Results
The terminal supports four output formats and provides an easy way to copy query results to your clipboard.
Output Formats
Use the .mode command to switch between output formats:
-- Table format (default) - human-readable ASCII table .mode table SELECT * FROM users; +----+-------+-------------------+ | id | name | email | +----+-------+-------------------+ | 1 | Alice | alice@example.com | | 2 | Bob | bob@example.com | +----+-------+-------------------+ -- CSV format - comma-separated values .mode csv SELECT * FROM users; id,name,email 1,Alice,alice@example.com 2,Bob,bob@example.com -- TSV format - tab-separated values .mode tsv SELECT * FROM users; id name email 1 Alice alice@example.com 2 Bob bob@example.com -- JSON format - array of objects .mode json SELECT * FROM users; [ {"id": 1, "name": "Alice", "email": "alice@example.com"}, {"id": 2, "name": "Bob", "email": "bob@example.com"} ]
Copying Results
After running a query, use .copy to copy the results to your clipboard in the current output format:
-- Run your query SELECT * FROM users WHERE active = true; -- Copy results to clipboard (uses current .mode format) .copy
The copied content respects your current output mode, so you can:
- Use
.mode csvor.mode tsvthen.copyto paste into a spreadsheet - Use
.mode jsonthen.copyto paste into a JSON file or API tool - Use
.mode tablethen.copyto paste formatted output into documentation
Pagination for Large Results
For large result sets, enable pagination with .pagesize:
-- Enable pagination (50 rows per page) .pagesize 50 -- Run a query with many results SELECT * FROM large_table; -- Navigate pages: -- n or Enter - next page -- p - previous page -- 1-9 - jump to page number -- q - quit pagination
Set .pagesize 0 to disable pagination and show all results at once.
Note: Queries that already contain LIMIT or OFFSET clauses bypass pagination, giving you full control over result size.
Custom Themes
You can create custom themes by providing a Theme object instead of 'dark' or 'light':
import { createTerminal, type Theme, type ThemeColors } from 'duckdb-terminal'; // Define custom colors const myColors: ThemeColors = { background: '#1a1b26', // Terminal background foreground: '#a9b1d6', // Default text color cursor: '#c0caf5', // Cursor color selection: '#33467c', // Selection highlight // Standard ANSI colors black: '#15161e', red: '#f7768e', green: '#9ece6a', yellow: '#e0af68', blue: '#7aa2f7', magenta: '#bb9af7', cyan: '#7dcfff', white: '#a9b1d6', // Bright variants brightBlack: '#414868', brightRed: '#f7768e', brightGreen: '#9ece6a', brightYellow: '#e0af68', brightBlue: '#7aa2f7', brightMagenta: '#bb9af7', brightCyan: '#7dcfff', brightWhite: '#c0caf5', }; // Create theme object const tokyoNight: Theme = { name: 'tokyo-night', colors: myColors, }; // Use custom theme const Terminal = await createTerminal({ container: '#terminal', theme: tokyoNight, }); // You can also change theme at runtime Terminal.setTheme(tokyoNight);
ThemeColors Reference
| Property | Description | ANSI Code |
|---|---|---|
background |
Terminal background color | - |
foreground |
Default text color | - |
cursor |
Cursor color | - |
selection |
Text selection highlight | - |
black |
ANSI black | \x1b[30m |
red |
ANSI red | \x1b[31m |
green |
ANSI green | \x1b[32m |
yellow |
ANSI yellow | \x1b[33m |
blue |
ANSI blue | \x1b[34m |
magenta |
ANSI magenta | \x1b[35m |
cyan |
ANSI cyan | \x1b[36m |
white |
ANSI white | \x1b[37m |
brightBlack |
Bright black (gray) | \x1b[90m |
brightRed |
Bright red | \x1b[91m |
brightGreen |
Bright green | \x1b[92m |
brightYellow |
Bright yellow | \x1b[93m |
brightBlue |
Bright blue | \x1b[94m |
brightMagenta |
Bright magenta | \x1b[95m |
brightCyan |
Bright cyan | \x1b[96m |
brightWhite |
Bright white | \x1b[97m |
Built-in Themes
The library exports two built-in themes that you can use directly or extend:
import { darkTheme, lightTheme } from 'duckdb-terminal'; // Use directly const Terminal = await createTerminal({ container: '#terminal', theme: darkTheme, }); // Or extend const myTheme: Theme = { name: 'my-dark', colors: { ...darkTheme.colors, background: '#000000', // Override specific colors cursor: '#ff0000', }, };
API
createTerminal(config)
Creates and starts a DuckDB Terminal instance.
import { createTerminal } from 'duckdb-terminal'; const Terminal = await createTerminal({ container: document.getElementById('terminal'), theme: 'dark', }); // Write to terminal Terminal.write('Hello, World!'); Terminal.writeln('With newline'); // Execute SQL programmatically const result = await Terminal.executeSQL('SELECT 1+1 as answer'); console.log(result); // { columns: ['answer'], rows: [[2]], rowCount: 1, duration: 5 } // Change theme Terminal.setTheme('light'); // Clear terminal Terminal.clear(); // Clean up when done (removes event listeners, closes database) Terminal.destroy();
Events
The Terminal emits events that you can subscribe to for monitoring and integrating with your application:
import { createTerminal, type TerminalEvents } from 'duckdb-terminal'; const Terminal = await createTerminal({ container: '#terminal', theme: 'dark', }); // Subscribe to events Terminal.on('ready', () => { console.log('Terminal is ready!'); }); Terminal.on('queryStart', ({ sql }) => { console.log('Executing:', sql); }); Terminal.on('queryEnd', ({ sql, result, error, duration }) => { if (error) { console.error('Query failed:', error); } else { console.log(`Query completed in ${duration}ms, ${result?.rowCount} rows`); } }); Terminal.on('stateChange', ({ state, previous }) => { console.log(`State changed: ${previous} -> ${state}`); }); Terminal.on('themeChange', ({ theme, previous }) => { console.log(`Theme changed to: ${theme.name}`); }); Terminal.on('fileLoaded', ({ filename, size, type }) => { console.log(`Loaded file: ${filename} (${size} bytes)`); }); Terminal.on('commandExecute', ({ command, args }) => { console.log(`Command: ${command}`, args); }); Terminal.on('error', ({ message, source }) => { console.error(`Error from ${source}: ${message}`); }); // Unsubscribe using the returned function const unsubscribe = Terminal.on('queryEnd', handler); unsubscribe(); // Stop listening // Or use off() directly Terminal.off('queryEnd', handler);
Available Events
| Event | Payload | Description |
|---|---|---|
ready |
{} |
Terminal is fully initialized |
queryStart |
{ sql } |
SQL query execution started |
queryEnd |
{ sql, result, error?, duration } |
SQL query completed or failed |
stateChange |
{ state, previous } |
Terminal state changed (idle, collecting, executing, paginating) |
themeChange |
{ theme, previous } |
Theme was changed |
fileLoaded |
{ filename, size, type } |
File was loaded via drag-drop or picker |
commandExecute |
{ command, args } |
Dot command was executed |
error |
{ message, source } |
An error occurred |
Advanced Usage
import { Terminal, Database, TerminalAdapter, formatTable, formatCSV, formatJSON } from 'duckdb-terminal'; // Use Database directly const db = new Database({ storage: 'memory' }); await db.init(); const result = await db.executeQuery('SELECT 42'); // Use formatters console.log(formatTable(result.columns, result.rows)); console.log(formatCSV(result.columns, result.rows)); console.log(formatJSON(result.columns, result.rows)); // Get completions const suggestions = await db.getCompletions('SEL', 3);
Browser Requirements
- Modern browser with WebAssembly support
- SharedArrayBuffer (requires COOP/COEP headers)
For development, Vite automatically sets the required headers. For production, configure your server:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Building
# Development npm run dev # Production build npm run build # Run tests npm test # Type checking npm run typecheck
Bundle Outputs
| File | Format | Usage |
|---|---|---|
dist/duckdb-terminal.js |
ESM | Modern bundlers |
dist/duckdb-terminal.umd.cjs |
UMD | Script tags, legacy |
dist/*.d.ts |
TypeScript | Type definitions |
Dependencies
- @duckdb/duckdb-wasm - DuckDB WebAssembly build
- ghostty-web - Ghostty terminal emulator for web
License
MIT
Credits
- DuckDB - The in-process analytical database
- Ghostty - Fast, native terminal emulator
- ghostty-web - Web port by Coder