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)
- Query Sharing - Share SQL queries via URL for collaboration
- AI-Powered SQL - Generate SQL from natural language using a text-to-sql proxy
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 |
|---|---|
.ai <question> |
Generate SQL from natural language (shorthand) |
.ai query <question> |
Generate SQL from natural language |
.ai provider list |
List available AI providers |
.ai provider set <name> |
Set the AI provider |
.ai endpoint get |
Show current proxy endpoint |
.ai endpoint set <url> |
Set proxy endpoint URL |
.chart [options] |
Show interactive chart of last query result |
.clear |
Clear the terminal |
.clearhistory |
Clear command history |
.copy |
Copy last query results to clipboard |
.download [filename] |
Download last result as file (format based on mode) |
.examples |
Show example queries |
.files [list|add|remove] |
Manage loaded files (list, add, or remove) |
.help |
Show available commands |
.highlight on|off |
Toggle syntax highlighting |
.links on|off |
Toggle clickable URL detection |
.mode table|csv|tsv|json |
Set output format |
.open |
Open file picker to load data files |
.pagesize <n> |
Set pagination size (0 to disable) |
.prompt [primary [cont]] |
Get or set the command prompt |
.reset |
Reset database and all settings to defaults |
.schema <table> |
Show table schema |
.share |
Open sharing modal to share queries via URL |
.tables |
List all tables |
.theme dark|light |
Switch color theme (clears screen) |
.timer on|off |
Toggle query timing |
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 |
Query Sharing
Share SQL queries via URL to collaborate with others. When someone opens a shared link, the queries execute automatically.
Opening the Share Modal
- Press
Ctrl+Shift+S(orCmd+Shift+Son Mac) - Click the share button in the sidebar (desktop) or mobile action bar
Selecting Queries
- The modal shows your recent SQL queries (dot commands are excluded)
- Click or press
Spaceto select/deselect queries - The character count shows how much of the 2000 character URL limit is used
- Selected queries will execute in order (oldest first) when the link is opened
URL Format
Shared URLs use a hash-based format:
https://terminal.sql-workbench.com/#$queries=v1,ENCODED_QUERY_1,ENCODED_QUERY_2,...
- Each query is URL-safe Base64 encoded
- Maximum URL length is 2000 characters for browser compatibility
- URLs are versioned (v1) for future compatibility
Keyboard Shortcuts in Share Modal
| Key | Action |
|---|---|
Space |
Select/deselect query |
↑ / ↓ |
Navigate query list |
Enter |
Copy shareable link |
Escape |
Close modal |
AI-Powered SQL Generation
Generate SQL queries from natural language using a text-to-sql proxy. This feature requires running the Text-to-SQL Proxy locally or on a server.
Running the Text-to-SQL Proxy
Download precompiled binaries from the releases page for your platform (Linux, macOS, Windows).
Quick start:
# Download and extract the binary for your platform, then run: ./text-to-sql-proxy # Or with a specific provider API key: ANTHROPIC_API_KEY=your-key ./text-to-sql-proxy OPENAI_API_KEY=your-key ./text-to-sql-proxy
The proxy starts on http://localhost:4000 by default. See the proxy documentation for more configuration options.
Setup
- Download and run the Text-to-SQL Proxy
- The proxy runs on
http://localhost:4000by default - Create tables in your database (the AI needs schema context)
Basic Usage
-- Create some tables first CREATE TABLE users (id INTEGER, name VARCHAR, email VARCHAR); CREATE TABLE orders (id INTEGER, user_id INTEGER, total DECIMAL, created_at DATE); -- Generate SQL from natural language .ai show all users -- Or use the explicit query subcommand .ai query find orders over 100 dollars from last month
The generated SQL is displayed with syntax highlighting and then executed automatically.
AI Commands
| Command | Description |
|---|---|
.ai <question> |
Generate SQL from natural language (shorthand) |
.ai query <question> |
Generate SQL from natural language |
.ai provider list |
List available AI providers from the proxy |
.ai provider set <name> |
Set the AI provider (e.g., claude, openai) |
.ai endpoint get |
Show current proxy endpoint URL |
.ai endpoint set <url> |
Change the proxy endpoint URL |
Configuration
Settings are persisted in localStorage:
- Endpoint: Default is
http://localhost:4000 - Provider: Default is
claude
-- Check current endpoint .ai endpoint get -- Change to a different proxy .ai endpoint set http://my-proxy:8000 -- List available providers .ai provider list -- Switch provider .ai provider set openai
Requirements
- Tables or views must exist in the database for the AI to generate meaningful SQL
- The text-to-sql proxy must be running and accessible
- If no tables exist, you'll see:
"Please create tables or views to be able to use the Text-to-SQL features" - If the proxy is unavailable, you'll see:
"Please install and run the Text-to-SQL Proxy"
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