openduck-cli 0.1.0__tar.gz
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.
- openduck_cli-0.1.0/PKG-INFO +204 -0
- openduck_cli-0.1.0/README.md +186 -0
- openduck_cli-0.1.0/openduck/__init__.py +42 -0
- openduck_cli-0.1.0/openduck/__main__.py +6 -0
- openduck_cli-0.1.0/openduck/core/__init__.py +36 -0
- openduck_cli-0.1.0/openduck/core/clipboard.py +57 -0
- openduck_cli-0.1.0/openduck/core/config.py +108 -0
- openduck_cli-0.1.0/openduck/core/error_classifier.py +53 -0
- openduck_cli-0.1.0/openduck/core/sql_utils.py +63 -0
- openduck_cli-0.1.0/openduck/gui/__init__.py +5 -0
- openduck_cli-0.1.0/openduck/gui/app.py +802 -0
- openduck_cli-0.1.0/openduck/gui/dialogs/__init__.py +7 -0
- openduck_cli-0.1.0/openduck/gui/dialogs/add_connection_dialog.py +77 -0
- openduck_cli-0.1.0/openduck/gui/dialogs/export_dialog.py +109 -0
- openduck_cli-0.1.0/openduck/gui/dialogs/save_query_dialog.py +41 -0
- openduck_cli-0.1.0/openduck/gui/screens/__init__.py +6 -0
- openduck_cli-0.1.0/openduck/gui/screens/about.py +84 -0
- openduck_cli-0.1.0/openduck/gui/screens/matrix.py +90 -0
- openduck_cli-0.1.0/openduck/gui/styles.py +71 -0
- openduck_cli-0.1.0/openduck/gui/widgets/__init__.py +17 -0
- openduck_cli-0.1.0/openduck/gui/widgets/database_tree.py +106 -0
- openduck_cli-0.1.0/openduck/gui/widgets/file_tree.py +14 -0
- openduck_cli-0.1.0/openduck/gui/widgets/query_tab.py +63 -0
- openduck_cli-0.1.0/openduck/gui/widgets/resize_handle.py +36 -0
- openduck_cli-0.1.0/openduck/gui/widgets/results_table.py +54 -0
- openduck_cli-0.1.0/openduck/gui/widgets/sql_editor.py +207 -0
- openduck_cli-0.1.0/openduck/main.py +25 -0
- openduck_cli-0.1.0/openduck_cli.egg-info/PKG-INFO +204 -0
- openduck_cli-0.1.0/openduck_cli.egg-info/SOURCES.txt +37 -0
- openduck_cli-0.1.0/openduck_cli.egg-info/dependency_links.txt +1 -0
- openduck_cli-0.1.0/openduck_cli.egg-info/entry_points.txt +3 -0
- openduck_cli-0.1.0/openduck_cli.egg-info/requires.txt +11 -0
- openduck_cli-0.1.0/openduck_cli.egg-info/top_level.txt +1 -0
- openduck_cli-0.1.0/pyproject.toml +35 -0
- openduck_cli-0.1.0/setup.cfg +4 -0
- openduck_cli-0.1.0/tests/test.py +29 -0
- openduck_cli-0.1.0/tests/test_all_errors.py +98 -0
- openduck_cli-0.1.0/tests/test_save_load.py +171 -0
- openduck_cli-0.1.0/tests/test_unit.py +80 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openduck-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The Modern, Lightning-Fast Terminal SQL IDE & Data Explorer for DuckDB
|
|
5
|
+
Author: Bertatron
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: textual[syntax]>=0.50.0
|
|
10
|
+
Requires-Dist: duckdb>=0.9.0
|
|
11
|
+
Requires-Dist: pandas>=2.0.0
|
|
12
|
+
Requires-Dist: openpyxl>=3.1.0
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest; extra == "dev"
|
|
15
|
+
Requires-Dist: pyinstaller; extra == "dev"
|
|
16
|
+
Provides-Extra: db
|
|
17
|
+
Requires-Dist: pymssql; extra == "db"
|
|
18
|
+
|
|
19
|
+
# 🦆 OpenDuck
|
|
20
|
+
|
|
21
|
+
<p align="center">
|
|
22
|
+
<b>The Modern, Lightning-Fast Terminal SQL IDE & Data Explorer for DuckDB</b>
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
<p align="center">
|
|
26
|
+
<img src="https://img.shields.io/badge/Python-3.10%2B-blue?style=for-the-badge&logo=python&logoColor=white" alt="Python 3.10+" />
|
|
27
|
+
<img src="https://img.shields.io/badge/DuckDB-Fast%20Analytics-yellow?style=for-the-badge&logo=duckdb&logoColor=black" alt="DuckDB" />
|
|
28
|
+
<img src="https://img.shields.io/badge/TUI-Textual-green?style=for-the-badge" alt="Textual" />
|
|
29
|
+
<img src="https://img.shields.io/badge/License-MIT-orange?style=for-the-badge" alt="MIT License" />
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
<p align="center">
|
|
35
|
+
<img width="100%" alt="OpenDuck Interface" src="https://github.com/user-attachments/assets/4aea9df6-6f00-494d-a171-d76a9a3b8133" />
|
|
36
|
+
</p>
|
|
37
|
+
|
|
38
|
+
**OpenDuck** brings the power and speed of **DuckDB** straight to your terminal in an intuitive, multi-tab Text User Interface (TUI). Effortlessly browse files, inspect datasets, write queries with full syntax highlighting, sort and filter millions of rows in milliseconds, and export results — all without leaving your shell.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## ✨ Features
|
|
43
|
+
|
|
44
|
+
- ⚡ **Instant File Queries**: Click or select any file (`.csv`, `.parquet`, `.xlsx`, `.json`, `.duckdb`, `.sqlite`, `.arrow`) from the sidebar to instantly load and execute queries with zero boilerplates.
|
|
45
|
+
- 🎨 **Rich SQL Syntax Highlighting**: Tree-sitter powered VS Code Dark styling with real-time token highlighting for SQL keywords, strings, numbers, identifiers, and comments.
|
|
46
|
+
- 🚀 **Asynchronous & Non-Blocking**: Queries execute in dedicated background threads — the interface remains buttery smooth and never freezes, even on heavy multi-gigabyte queries.
|
|
47
|
+
- 📑 **Multi-Tab Workspace**: Work with multiple datasets simultaneously with tab navigation, close buttons (`✕`), and tab titles dynamically bound to files and queries.
|
|
48
|
+
- 📊 **Interactive Data Viewer**:
|
|
49
|
+
- Sort columns ascending/descending with clickable headers (safe null handling).
|
|
50
|
+
- Quick column filters.
|
|
51
|
+
- Live query execution runtime, row count, and memory consumption stats.
|
|
52
|
+
- 📋 **Terminal & System Clipboard Integration**:
|
|
53
|
+
- Copy highlighted cells or entire rows (`Ctrl+C`).
|
|
54
|
+
- Copy full query results as TSV (`Ctrl+A`).
|
|
55
|
+
- Native terminal copy & paste with multi-level Undo/Redo (`Cmd+Z` / `Ctrl+Z` / `Ctrl+Y`).
|
|
56
|
+
- Toggle SQL comments effortlessly (`F3` / `Cmd+/`).
|
|
57
|
+
- 💾 **One-Click Export**: Export results to `.csv` or `.xlsx` files with a single click.
|
|
58
|
+
- 📜 **History & Saved Queries**: Automatically preserves query history and lets you save reusable named queries with quick one-click recall into your editor.
|
|
59
|
+
- 🎛️ **Draggable Resizable Sidebar**: Adjust the width of your sidebar on the fly with mouse drag.
|
|
60
|
+
- 🛡️ **Intelligent Error Diagnostics**: Corrupted file, wrong delimiter, permission issue, or missing dataset? OpenDuck identifies the exact cause and displays human-readable actionable solutions.
|
|
61
|
+
- 🐇 **Easter Egg**: Secret Matrix rain screensaver!
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 📂 Supported File Formats
|
|
66
|
+
|
|
67
|
+
OpenDuck automatically recognises and formats queries for:
|
|
68
|
+
|
|
69
|
+
| Format | Extensions | Query Mode |
|
|
70
|
+
| :--- | :--- | :--- |
|
|
71
|
+
| **Parquet** | `.parquet` | Native direct query |
|
|
72
|
+
| **Arrow** | `.arrow` | Native direct query |
|
|
73
|
+
| **CSV & GZ** | `.csv`, `.csv.gz` | `read_csv_auto(...)` |
|
|
74
|
+
| **JSON & JSONL** | `.json`, `.jsonl` | `read_json_auto(...)` |
|
|
75
|
+
| **Excel** | `.xlsx`, `.xls` | In-memory table registration (`pandas` + `openpyxl`) |
|
|
76
|
+
| **SQLite** | `.sqlite`, `.sqlite3`, `.db` | Automatic SQLite extension attach |
|
|
77
|
+
| **DuckDB** | `.duckdb` | Database attach & table explore |
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 🚀 Quickstart
|
|
82
|
+
|
|
83
|
+
### 1. Installation
|
|
84
|
+
|
|
85
|
+
**Via PyPI:**
|
|
86
|
+
```bash
|
|
87
|
+
pip install openduck-cli
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**From source:**
|
|
91
|
+
```bash
|
|
92
|
+
git clone https://github.com/klandermans/openduck.git
|
|
93
|
+
cd openduck
|
|
94
|
+
pip install -r requirements.txt
|
|
95
|
+
# or: pip install -e .
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
> **Note:** `textual[syntax]` installs the required `tree-sitter` and `tree-sitter-sql` engines for syntax highlighting out of the box.
|
|
99
|
+
|
|
100
|
+
### 2. Run OpenDuck
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
python openduck.py
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Or make it available system-wide as a command:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
chmod +x openduck.sh
|
|
110
|
+
sudo cp openduck.sh /usr/local/bin/openduck
|
|
111
|
+
openduck
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## ⌨️ Keyboard & Mouse Shortcuts
|
|
117
|
+
|
|
118
|
+
| Shortcut | Description |
|
|
119
|
+
| :--- | :--- |
|
|
120
|
+
| `Ctrl+Enter` / `Cmd+Enter` | **Run Query** (runs selection if text is selected, or full editor) |
|
|
121
|
+
| `Ctrl+S` / `Cmd+S` | **Save Query** with custom name to `openduck.json` |
|
|
122
|
+
| `F3` / `Cmd+/` / `Ctrl+/` | **Toggle SQL Comment** (`--`) on current line or selection |
|
|
123
|
+
| `Ctrl+Z` / `Cmd+Z` | **Undo** last edit |
|
|
124
|
+
| `Ctrl+Y` / `Cmd+Shift+Z` | **Redo** undone edit |
|
|
125
|
+
| `Ctrl+W` / `Cmd+W` | **Close Current Tab** |
|
|
126
|
+
| `Ctrl+M` | **Toggle Mouse Mode** (switch between TUI mouse and native terminal text selection) |
|
|
127
|
+
| `Ctrl+C` / `Cmd+C` | **Copy** (Editor selection, or table row/cell to system clipboard) |
|
|
128
|
+
| `Ctrl+A` / `Cmd+A` | **Copy Table** (when focused on results) / Select all (in editor) |
|
|
129
|
+
| `F1` | **About Dialog** (click the duck for a surprise! 🕶️) |
|
|
130
|
+
| `Ctrl+Q` / `Cmd+Q` | **Quit** OpenDuck |
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## ⚙️ Configuration & Storage
|
|
135
|
+
|
|
136
|
+
OpenDuck automatically manages `openduck.json` in your working directory. It persists:
|
|
137
|
+
- The last 200 executed queries in chronological order.
|
|
138
|
+
- Your named saved queries.
|
|
139
|
+
- Database configurations.
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{
|
|
143
|
+
"history": [
|
|
144
|
+
{
|
|
145
|
+
"sql": "SELECT * FROM read_csv_auto('sales.csv') WHERE revenue > 1000 LIMIT 100;",
|
|
146
|
+
"timestamp": "2026-10-14T15:30:00.123456"
|
|
147
|
+
}
|
|
148
|
+
],
|
|
149
|
+
"saved_queries": [
|
|
150
|
+
{
|
|
151
|
+
"name": "Q3 Revenue by Region",
|
|
152
|
+
"sql": "SELECT region, SUM(amount) FROM read_parquet('data/*.parquet') GROUP BY region;",
|
|
153
|
+
"timestamp": "2026-10-14T15:32:00.654321"
|
|
154
|
+
}
|
|
155
|
+
],
|
|
156
|
+
"connections": []
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 💡 Tips & Tricks
|
|
163
|
+
|
|
164
|
+
- **Click to Query**: Clicking any file in the left sidebar automatically populates the query into your active tab, updates the tab name, and executes the preview.
|
|
165
|
+
- **Run Selection Only**: Highlight any part of your SQL script and press `Ctrl+Enter` to run only the highlighted snippet — just like in DBeaver, DataGrip, or VS Code!
|
|
166
|
+
- **Selective Comments**: Select multiple lines and press `F3` or `Cmd+/` to comment or uncomment SQL blocks simultaneously.
|
|
167
|
+
- **Instant Clipboard Sharing**: Results copied via `Ctrl+C` or `Ctrl+A` are immediately synced to your macOS (`pbcopy`), Linux (`wl-copy`/`xclip`), or Windows clipboard.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## 📁 Project Structure
|
|
172
|
+
|
|
173
|
+
```text
|
|
174
|
+
openduck/
|
|
175
|
+
├── openduck/ # Core Python package
|
|
176
|
+
│ ├── __init__.py # Package entry and exports
|
|
177
|
+
│ ├── __main__.py # CLI invocation (python -m openduck)
|
|
178
|
+
│ ├── main.py # Application bootstrap & logging setup
|
|
179
|
+
│ ├── core/ # Core logic & backend
|
|
180
|
+
│ │ ├── config.py # Config persistence, query history, saved queries
|
|
181
|
+
│ │ ├── clipboard.py # Cross-platform system clipboard integration
|
|
182
|
+
│ │ ├── sql_utils.py # SQL formatting, file-to-query mapper, comments
|
|
183
|
+
│ │ └── error_classifier.py # Diagnostic error classifier
|
|
184
|
+
│ └── gui/ # Textual user interface
|
|
185
|
+
│ ├── app.py # Main DuckCLI application
|
|
186
|
+
│ ├── styles.py # Textual CSS stylesheets
|
|
187
|
+
│ ├── screens/ # Modal screens (About, Matrix screensaver)
|
|
188
|
+
│ ├── dialogs/ # Modal dialogs (Save Query, Export, Add DB)
|
|
189
|
+
│ └── widgets/ # Custom widgets (Editor, ResultsTable, Trees, etc.)
|
|
190
|
+
├── docs/ # Documentation & guides
|
|
191
|
+
├── scripts/ # Build scripts & packaging specs
|
|
192
|
+
├── tests/ # Automated test suite
|
|
193
|
+
├── samples/ # Sample datasets for testing & exploration
|
|
194
|
+
├── openduck.py # Root executable script
|
|
195
|
+
├── openduck.sh # Shell launcher script
|
|
196
|
+
└── pyproject.toml # Python package configuration
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## 📜 License
|
|
202
|
+
|
|
203
|
+
Distributed under the **MIT License**. See `LICENSE` for details.
|
|
204
|
+
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# 🦆 OpenDuck
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<b>The Modern, Lightning-Fast Terminal SQL IDE & Data Explorer for DuckDB</b>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<img src="https://img.shields.io/badge/Python-3.10%2B-blue?style=for-the-badge&logo=python&logoColor=white" alt="Python 3.10+" />
|
|
9
|
+
<img src="https://img.shields.io/badge/DuckDB-Fast%20Analytics-yellow?style=for-the-badge&logo=duckdb&logoColor=black" alt="DuckDB" />
|
|
10
|
+
<img src="https://img.shields.io/badge/TUI-Textual-green?style=for-the-badge" alt="Textual" />
|
|
11
|
+
<img src="https://img.shields.io/badge/License-MIT-orange?style=for-the-badge" alt="MIT License" />
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<p align="center">
|
|
17
|
+
<img width="100%" alt="OpenDuck Interface" src="https://github.com/user-attachments/assets/4aea9df6-6f00-494d-a171-d76a9a3b8133" />
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
**OpenDuck** brings the power and speed of **DuckDB** straight to your terminal in an intuitive, multi-tab Text User Interface (TUI). Effortlessly browse files, inspect datasets, write queries with full syntax highlighting, sort and filter millions of rows in milliseconds, and export results — all without leaving your shell.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## ✨ Features
|
|
25
|
+
|
|
26
|
+
- ⚡ **Instant File Queries**: Click or select any file (`.csv`, `.parquet`, `.xlsx`, `.json`, `.duckdb`, `.sqlite`, `.arrow`) from the sidebar to instantly load and execute queries with zero boilerplates.
|
|
27
|
+
- 🎨 **Rich SQL Syntax Highlighting**: Tree-sitter powered VS Code Dark styling with real-time token highlighting for SQL keywords, strings, numbers, identifiers, and comments.
|
|
28
|
+
- 🚀 **Asynchronous & Non-Blocking**: Queries execute in dedicated background threads — the interface remains buttery smooth and never freezes, even on heavy multi-gigabyte queries.
|
|
29
|
+
- 📑 **Multi-Tab Workspace**: Work with multiple datasets simultaneously with tab navigation, close buttons (`✕`), and tab titles dynamically bound to files and queries.
|
|
30
|
+
- 📊 **Interactive Data Viewer**:
|
|
31
|
+
- Sort columns ascending/descending with clickable headers (safe null handling).
|
|
32
|
+
- Quick column filters.
|
|
33
|
+
- Live query execution runtime, row count, and memory consumption stats.
|
|
34
|
+
- 📋 **Terminal & System Clipboard Integration**:
|
|
35
|
+
- Copy highlighted cells or entire rows (`Ctrl+C`).
|
|
36
|
+
- Copy full query results as TSV (`Ctrl+A`).
|
|
37
|
+
- Native terminal copy & paste with multi-level Undo/Redo (`Cmd+Z` / `Ctrl+Z` / `Ctrl+Y`).
|
|
38
|
+
- Toggle SQL comments effortlessly (`F3` / `Cmd+/`).
|
|
39
|
+
- 💾 **One-Click Export**: Export results to `.csv` or `.xlsx` files with a single click.
|
|
40
|
+
- 📜 **History & Saved Queries**: Automatically preserves query history and lets you save reusable named queries with quick one-click recall into your editor.
|
|
41
|
+
- 🎛️ **Draggable Resizable Sidebar**: Adjust the width of your sidebar on the fly with mouse drag.
|
|
42
|
+
- 🛡️ **Intelligent Error Diagnostics**: Corrupted file, wrong delimiter, permission issue, or missing dataset? OpenDuck identifies the exact cause and displays human-readable actionable solutions.
|
|
43
|
+
- 🐇 **Easter Egg**: Secret Matrix rain screensaver!
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 📂 Supported File Formats
|
|
48
|
+
|
|
49
|
+
OpenDuck automatically recognises and formats queries for:
|
|
50
|
+
|
|
51
|
+
| Format | Extensions | Query Mode |
|
|
52
|
+
| :--- | :--- | :--- |
|
|
53
|
+
| **Parquet** | `.parquet` | Native direct query |
|
|
54
|
+
| **Arrow** | `.arrow` | Native direct query |
|
|
55
|
+
| **CSV & GZ** | `.csv`, `.csv.gz` | `read_csv_auto(...)` |
|
|
56
|
+
| **JSON & JSONL** | `.json`, `.jsonl` | `read_json_auto(...)` |
|
|
57
|
+
| **Excel** | `.xlsx`, `.xls` | In-memory table registration (`pandas` + `openpyxl`) |
|
|
58
|
+
| **SQLite** | `.sqlite`, `.sqlite3`, `.db` | Automatic SQLite extension attach |
|
|
59
|
+
| **DuckDB** | `.duckdb` | Database attach & table explore |
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 🚀 Quickstart
|
|
64
|
+
|
|
65
|
+
### 1. Installation
|
|
66
|
+
|
|
67
|
+
**Via PyPI:**
|
|
68
|
+
```bash
|
|
69
|
+
pip install openduck-cli
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**From source:**
|
|
73
|
+
```bash
|
|
74
|
+
git clone https://github.com/klandermans/openduck.git
|
|
75
|
+
cd openduck
|
|
76
|
+
pip install -r requirements.txt
|
|
77
|
+
# or: pip install -e .
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
> **Note:** `textual[syntax]` installs the required `tree-sitter` and `tree-sitter-sql` engines for syntax highlighting out of the box.
|
|
81
|
+
|
|
82
|
+
### 2. Run OpenDuck
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
python openduck.py
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Or make it available system-wide as a command:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
chmod +x openduck.sh
|
|
92
|
+
sudo cp openduck.sh /usr/local/bin/openduck
|
|
93
|
+
openduck
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## ⌨️ Keyboard & Mouse Shortcuts
|
|
99
|
+
|
|
100
|
+
| Shortcut | Description |
|
|
101
|
+
| :--- | :--- |
|
|
102
|
+
| `Ctrl+Enter` / `Cmd+Enter` | **Run Query** (runs selection if text is selected, or full editor) |
|
|
103
|
+
| `Ctrl+S` / `Cmd+S` | **Save Query** with custom name to `openduck.json` |
|
|
104
|
+
| `F3` / `Cmd+/` / `Ctrl+/` | **Toggle SQL Comment** (`--`) on current line or selection |
|
|
105
|
+
| `Ctrl+Z` / `Cmd+Z` | **Undo** last edit |
|
|
106
|
+
| `Ctrl+Y` / `Cmd+Shift+Z` | **Redo** undone edit |
|
|
107
|
+
| `Ctrl+W` / `Cmd+W` | **Close Current Tab** |
|
|
108
|
+
| `Ctrl+M` | **Toggle Mouse Mode** (switch between TUI mouse and native terminal text selection) |
|
|
109
|
+
| `Ctrl+C` / `Cmd+C` | **Copy** (Editor selection, or table row/cell to system clipboard) |
|
|
110
|
+
| `Ctrl+A` / `Cmd+A` | **Copy Table** (when focused on results) / Select all (in editor) |
|
|
111
|
+
| `F1` | **About Dialog** (click the duck for a surprise! 🕶️) |
|
|
112
|
+
| `Ctrl+Q` / `Cmd+Q` | **Quit** OpenDuck |
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## ⚙️ Configuration & Storage
|
|
117
|
+
|
|
118
|
+
OpenDuck automatically manages `openduck.json` in your working directory. It persists:
|
|
119
|
+
- The last 200 executed queries in chronological order.
|
|
120
|
+
- Your named saved queries.
|
|
121
|
+
- Database configurations.
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"history": [
|
|
126
|
+
{
|
|
127
|
+
"sql": "SELECT * FROM read_csv_auto('sales.csv') WHERE revenue > 1000 LIMIT 100;",
|
|
128
|
+
"timestamp": "2026-10-14T15:30:00.123456"
|
|
129
|
+
}
|
|
130
|
+
],
|
|
131
|
+
"saved_queries": [
|
|
132
|
+
{
|
|
133
|
+
"name": "Q3 Revenue by Region",
|
|
134
|
+
"sql": "SELECT region, SUM(amount) FROM read_parquet('data/*.parquet') GROUP BY region;",
|
|
135
|
+
"timestamp": "2026-10-14T15:32:00.654321"
|
|
136
|
+
}
|
|
137
|
+
],
|
|
138
|
+
"connections": []
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 💡 Tips & Tricks
|
|
145
|
+
|
|
146
|
+
- **Click to Query**: Clicking any file in the left sidebar automatically populates the query into your active tab, updates the tab name, and executes the preview.
|
|
147
|
+
- **Run Selection Only**: Highlight any part of your SQL script and press `Ctrl+Enter` to run only the highlighted snippet — just like in DBeaver, DataGrip, or VS Code!
|
|
148
|
+
- **Selective Comments**: Select multiple lines and press `F3` or `Cmd+/` to comment or uncomment SQL blocks simultaneously.
|
|
149
|
+
- **Instant Clipboard Sharing**: Results copied via `Ctrl+C` or `Ctrl+A` are immediately synced to your macOS (`pbcopy`), Linux (`wl-copy`/`xclip`), or Windows clipboard.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 📁 Project Structure
|
|
154
|
+
|
|
155
|
+
```text
|
|
156
|
+
openduck/
|
|
157
|
+
├── openduck/ # Core Python package
|
|
158
|
+
│ ├── __init__.py # Package entry and exports
|
|
159
|
+
│ ├── __main__.py # CLI invocation (python -m openduck)
|
|
160
|
+
│ ├── main.py # Application bootstrap & logging setup
|
|
161
|
+
│ ├── core/ # Core logic & backend
|
|
162
|
+
│ │ ├── config.py # Config persistence, query history, saved queries
|
|
163
|
+
│ │ ├── clipboard.py # Cross-platform system clipboard integration
|
|
164
|
+
│ │ ├── sql_utils.py # SQL formatting, file-to-query mapper, comments
|
|
165
|
+
│ │ └── error_classifier.py # Diagnostic error classifier
|
|
166
|
+
│ └── gui/ # Textual user interface
|
|
167
|
+
│ ├── app.py # Main DuckCLI application
|
|
168
|
+
│ ├── styles.py # Textual CSS stylesheets
|
|
169
|
+
│ ├── screens/ # Modal screens (About, Matrix screensaver)
|
|
170
|
+
│ ├── dialogs/ # Modal dialogs (Save Query, Export, Add DB)
|
|
171
|
+
│ └── widgets/ # Custom widgets (Editor, ResultsTable, Trees, etc.)
|
|
172
|
+
├── docs/ # Documentation & guides
|
|
173
|
+
├── scripts/ # Build scripts & packaging specs
|
|
174
|
+
├── tests/ # Automated test suite
|
|
175
|
+
├── samples/ # Sample datasets for testing & exploration
|
|
176
|
+
├── openduck.py # Root executable script
|
|
177
|
+
├── openduck.sh # Shell launcher script
|
|
178
|
+
└── pyproject.toml # Python package configuration
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## 📜 License
|
|
184
|
+
|
|
185
|
+
Distributed under the **MIT License**. See `LICENSE` for details.
|
|
186
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""OpenDuck - The Modern Terminal SQL IDE & Data Explorer for DuckDB."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from openduck.core.config import (
|
|
6
|
+
CWD,
|
|
7
|
+
CONFIG_FILE,
|
|
8
|
+
MAX_UI_ROWS,
|
|
9
|
+
DUCKDB_EXTENSIONS,
|
|
10
|
+
add_to_history,
|
|
11
|
+
delete_connection,
|
|
12
|
+
load_config,
|
|
13
|
+
save_config,
|
|
14
|
+
save_connection,
|
|
15
|
+
save_query,
|
|
16
|
+
)
|
|
17
|
+
from openduck.core.error_classifier import classify_file_error
|
|
18
|
+
from openduck.core.sql_utils import is_duckdb_file, sql_for_file, toggle_sql_comment_lines
|
|
19
|
+
from openduck.core.clipboard import copy_to_system_clipboard, get_system_clipboard
|
|
20
|
+
from openduck.gui.app import DuckCLI
|
|
21
|
+
from openduck.main import main
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"DuckCLI",
|
|
25
|
+
"main",
|
|
26
|
+
"load_config",
|
|
27
|
+
"save_config",
|
|
28
|
+
"add_to_history",
|
|
29
|
+
"save_query",
|
|
30
|
+
"save_connection",
|
|
31
|
+
"delete_connection",
|
|
32
|
+
"classify_file_error",
|
|
33
|
+
"is_duckdb_file",
|
|
34
|
+
"sql_for_file",
|
|
35
|
+
"toggle_sql_comment_lines",
|
|
36
|
+
"copy_to_system_clipboard",
|
|
37
|
+
"get_system_clipboard",
|
|
38
|
+
"CWD",
|
|
39
|
+
"CONFIG_FILE",
|
|
40
|
+
"MAX_UI_ROWS",
|
|
41
|
+
"DUCKDB_EXTENSIONS",
|
|
42
|
+
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Core modules for OpenDuck: config, clipboard, SQL utilities, and error classification."""
|
|
2
|
+
|
|
3
|
+
from openduck.core.clipboard import copy_to_system_clipboard, get_system_clipboard
|
|
4
|
+
from openduck.core.config import (
|
|
5
|
+
CONFIG_FILE,
|
|
6
|
+
CWD,
|
|
7
|
+
DUCKDB_EXTENSIONS,
|
|
8
|
+
MAX_UI_ROWS,
|
|
9
|
+
add_to_history,
|
|
10
|
+
delete_connection,
|
|
11
|
+
load_config,
|
|
12
|
+
save_config,
|
|
13
|
+
save_connection,
|
|
14
|
+
save_query,
|
|
15
|
+
)
|
|
16
|
+
from openduck.core.error_classifier import classify_file_error
|
|
17
|
+
from openduck.core.sql_utils import is_duckdb_file, sql_for_file, toggle_sql_comment_lines
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"CWD",
|
|
21
|
+
"CONFIG_FILE",
|
|
22
|
+
"MAX_UI_ROWS",
|
|
23
|
+
"DUCKDB_EXTENSIONS",
|
|
24
|
+
"load_config",
|
|
25
|
+
"save_config",
|
|
26
|
+
"add_to_history",
|
|
27
|
+
"save_query",
|
|
28
|
+
"save_connection",
|
|
29
|
+
"delete_connection",
|
|
30
|
+
"copy_to_system_clipboard",
|
|
31
|
+
"get_system_clipboard",
|
|
32
|
+
"is_duckdb_file",
|
|
33
|
+
"sql_for_file",
|
|
34
|
+
"toggle_sql_comment_lines",
|
|
35
|
+
"classify_file_error",
|
|
36
|
+
]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Cross-platform system clipboard integration."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def copy_to_system_clipboard(text: str) -> bool:
|
|
11
|
+
"""Copy text to system clipboard (macOS pbcopy, xclip/wl-copy on Linux, clip on Windows)."""
|
|
12
|
+
if not text:
|
|
13
|
+
return False
|
|
14
|
+
try:
|
|
15
|
+
if sys.platform == "darwin":
|
|
16
|
+
p = subprocess.Popen(["pbcopy"], stdin=subprocess.PIPE, text=True)
|
|
17
|
+
p.communicate(input=text)
|
|
18
|
+
return True
|
|
19
|
+
elif sys.platform.startswith("linux"):
|
|
20
|
+
for cmd in (
|
|
21
|
+
["wl-copy"],
|
|
22
|
+
["xclip", "-selection", "clipboard"],
|
|
23
|
+
["xsel", "--clipboard", "--input"],
|
|
24
|
+
):
|
|
25
|
+
try:
|
|
26
|
+
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, text=True)
|
|
27
|
+
p.communicate(input=text)
|
|
28
|
+
return True
|
|
29
|
+
except FileNotFoundError:
|
|
30
|
+
continue
|
|
31
|
+
elif sys.platform == "win32":
|
|
32
|
+
p = subprocess.Popen(["clip"], stdin=subprocess.PIPE, text=True)
|
|
33
|
+
p.communicate(input=text)
|
|
34
|
+
return True
|
|
35
|
+
except Exception as e:
|
|
36
|
+
logger.error(f"Failed to copy to system clipboard: {e}")
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_system_clipboard() -> str:
|
|
41
|
+
"""Read text from system clipboard (macOS pbpaste, wl-paste/xclip on Linux)."""
|
|
42
|
+
try:
|
|
43
|
+
if sys.platform == "darwin":
|
|
44
|
+
return subprocess.check_output(["pbpaste"], text=True)
|
|
45
|
+
elif sys.platform.startswith("linux"):
|
|
46
|
+
for cmd in (
|
|
47
|
+
["wl-paste"],
|
|
48
|
+
["xclip", "-selection", "clipboard", "-o"],
|
|
49
|
+
["xsel", "--clipboard", "--output"],
|
|
50
|
+
):
|
|
51
|
+
try:
|
|
52
|
+
return subprocess.check_output(cmd, text=True)
|
|
53
|
+
except FileNotFoundError:
|
|
54
|
+
continue
|
|
55
|
+
except Exception as e:
|
|
56
|
+
logger.error(f"Failed to read from system clipboard: {e}")
|
|
57
|
+
return ""
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Configuration and persistence management for OpenDuck."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
CWD = Path(os.getcwd())
|
|
12
|
+
CONFIG_FILE = CWD / "openduck.json"
|
|
13
|
+
MAX_UI_ROWS = 10000
|
|
14
|
+
DUCKDB_EXTENSIONS = {
|
|
15
|
+
".parquet", ".arrow", ".duckdb", ".csv", ".csv.gz",
|
|
16
|
+
".json", ".jsonl", ".sqlite", ".sqlite3", ".db", ".xlsx", ".xls"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def load_config() -> dict:
|
|
21
|
+
"""Load config from openduck.json, create if doesn't exist or is invalid."""
|
|
22
|
+
if CONFIG_FILE.exists():
|
|
23
|
+
try:
|
|
24
|
+
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
|
25
|
+
config = json.load(f)
|
|
26
|
+
if isinstance(config, dict):
|
|
27
|
+
if "connections" not in config:
|
|
28
|
+
config["connections"] = []
|
|
29
|
+
if "history" not in config:
|
|
30
|
+
config["history"] = []
|
|
31
|
+
if "saved_queries" not in config:
|
|
32
|
+
config["saved_queries"] = []
|
|
33
|
+
return config
|
|
34
|
+
except Exception as e:
|
|
35
|
+
logger.error(f"Failed to read config file, falling back to default: {e}")
|
|
36
|
+
|
|
37
|
+
default_config = {
|
|
38
|
+
"history": [],
|
|
39
|
+
"saved_queries": [],
|
|
40
|
+
"connections": []
|
|
41
|
+
}
|
|
42
|
+
save_config(default_config)
|
|
43
|
+
return default_config
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def save_config(config: dict) -> None:
|
|
47
|
+
"""Save config to openduck.json."""
|
|
48
|
+
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
49
|
+
json.dump(config, f, indent=2, ensure_ascii=False)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def add_to_history(sql: str) -> dict:
|
|
53
|
+
"""Add a query to history in the config file, capped at the last 200 queries."""
|
|
54
|
+
config = load_config()
|
|
55
|
+
timestamp = datetime.now().isoformat()
|
|
56
|
+
config["history"].append({
|
|
57
|
+
"sql": sql.strip(),
|
|
58
|
+
"timestamp": timestamp
|
|
59
|
+
})
|
|
60
|
+
# Keep only the last 200 queries to prevent boundless file growth
|
|
61
|
+
config["history"] = config["history"][-200:]
|
|
62
|
+
save_config(config)
|
|
63
|
+
return config
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def save_query(name: str, sql: str) -> dict:
|
|
67
|
+
"""Save a query with a name to the config file."""
|
|
68
|
+
config = load_config()
|
|
69
|
+
timestamp = datetime.now().isoformat()
|
|
70
|
+
# Check if query with same name already exists and update it
|
|
71
|
+
for i, query in enumerate(config["saved_queries"]):
|
|
72
|
+
if query["name"] == name:
|
|
73
|
+
config["saved_queries"][i] = {
|
|
74
|
+
"name": name,
|
|
75
|
+
"sql": sql.strip(),
|
|
76
|
+
"timestamp": timestamp
|
|
77
|
+
}
|
|
78
|
+
save_config(config)
|
|
79
|
+
return config
|
|
80
|
+
# If not found, add as new query
|
|
81
|
+
config["saved_queries"].append({
|
|
82
|
+
"name": name,
|
|
83
|
+
"sql": sql.strip(),
|
|
84
|
+
"timestamp": timestamp
|
|
85
|
+
})
|
|
86
|
+
save_config(config)
|
|
87
|
+
return config
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def save_connection(conn_info: dict) -> dict:
|
|
91
|
+
"""Save a database connection to config."""
|
|
92
|
+
config = load_config()
|
|
93
|
+
for i, c in enumerate(config["connections"]):
|
|
94
|
+
if c["id"] == conn_info["id"]:
|
|
95
|
+
config["connections"][i] = conn_info
|
|
96
|
+
save_config(config)
|
|
97
|
+
return config
|
|
98
|
+
config["connections"].append(conn_info)
|
|
99
|
+
save_config(config)
|
|
100
|
+
return config
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def delete_connection(conn_id: str) -> dict:
|
|
104
|
+
"""Delete a database connection from config."""
|
|
105
|
+
config = load_config()
|
|
106
|
+
config["connections"] = [c for c in config["connections"] if c["id"] != conn_id]
|
|
107
|
+
save_config(config)
|
|
108
|
+
return config
|