openduck-cli 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,63 @@
1
+ """Query tab container combining editor, results table, action buttons, and status bar."""
2
+
3
+ from textual.app import ComposeResult
4
+ from textual.containers import Horizontal, Vertical
5
+ from textual.widgets import Button, DataTable, Static
6
+
7
+ from openduck.gui.widgets.results_table import ResultsTable
8
+ from openduck.gui.widgets.sql_editor import SqlTextArea
9
+
10
+
11
+ class QueryTab(Vertical):
12
+ """A tab containing a SQL text editor, results table, action toolbar, and metadata bar."""
13
+
14
+ def __init__(self, sql: str = "", connection_id: str = None):
15
+ super().__init__()
16
+ self.initial_sql = sql
17
+ self.full_data, self.column_names, self.col_states = [], [], {}
18
+ self.running_task = None
19
+ self.connection_id = connection_id
20
+
21
+ def compose(self) -> ComposeResult:
22
+ text_area = SqlTextArea(
23
+ self.initial_sql,
24
+ language="sql",
25
+ id="query-input",
26
+ theme="vscode_dark",
27
+ soft_wrap=False, # Disabled to prevent crash with long lines
28
+ read_only=False,
29
+ tab_behavior="indent",
30
+ show_line_numbers=True,
31
+ )
32
+ # Force SQL highlighting
33
+ try:
34
+ text_area.language = "sql"
35
+ except Exception:
36
+ pass
37
+ yield text_area
38
+ with Horizontal(): # Container for table and export sidebar
39
+ yield ResultsTable(id="results-table")
40
+ with Vertical(id="export-sidebar"):
41
+ yield Button("▶ Run", id="btn-run-query", classes="export-btn", variant="primary", tooltip="Run query / selection (Ctrl+Enter)")
42
+ yield Button("💬 --", id="btn-comment", classes="export-btn", tooltip="Toggle comment (F3 / Cmd+/)")
43
+ yield Button("↓CSV", id="export-csv", classes="export-btn")
44
+ yield Button("↓XLSX", id="export-excel", classes="export-btn")
45
+ yield Button("Abort", id="cancel-query", classes="export-btn", tooltip="Cancel Query", variant="error")
46
+ with Horizontal(id="metadata-container"):
47
+ yield Static("Ready", id="metadata-bar")
48
+ yield Static("Memory: N/A", id="memory-bar")
49
+
50
+ def on_mount(self):
51
+ t = self.query_one(DataTable)
52
+ t.cursor_type, t.zebra_stripes = "row", True
53
+ # Use slight delay for focus to allow UI to settle
54
+ self.set_timer(0.1, self.safe_focus)
55
+
56
+ def safe_focus(self):
57
+ """Focus the editor safely."""
58
+ try:
59
+ editor = self.query_one("#query-input")
60
+ editor.move_cursor((0, 0))
61
+ editor.focus()
62
+ except Exception:
63
+ pass
@@ -0,0 +1,36 @@
1
+ """Draggable sidebar resize handle widget."""
2
+
3
+ from textual.events import MouseDown, MouseMove, MouseUp
4
+ from textual.widgets import Static
5
+
6
+ from openduck.gui.styles import RESIZE_CSS
7
+
8
+
9
+ class ResizeHandle(Static):
10
+ """A draggable handle to resize the sidebar."""
11
+
12
+ DEFAULT_CSS = RESIZE_CSS
13
+
14
+ def __init__(self):
15
+ super().__init__("┃")
16
+ self._dragging = False
17
+
18
+ def on_mouse_down(self, event: MouseDown) -> None:
19
+ self._dragging = True
20
+ self.capture_mouse()
21
+ event.stop()
22
+
23
+ def on_mouse_move(self, event: MouseMove) -> None:
24
+ if self._dragging:
25
+ # screen_x gives us the absolute position of the mouse
26
+ screen_width = self.screen.size.width
27
+ new_width = max(10, min(event.screen_x, screen_width - 20))
28
+ sidebar = self.screen.query_one("#sidebar")
29
+ sidebar.styles.width = new_width
30
+ event.stop()
31
+
32
+ def on_mouse_up(self, event: MouseUp) -> None:
33
+ if self._dragging:
34
+ self._dragging = False
35
+ self.release_mouse()
36
+ event.stop()
@@ -0,0 +1,54 @@
1
+ """Data table widget for query results with copy and keyboard bindings."""
2
+
3
+ import logging
4
+ from textual.binding import Binding
5
+ from textual.widgets import DataTable
6
+
7
+ from openduck.core.clipboard import copy_to_system_clipboard
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class ResultsTable(DataTable):
13
+ """DataTable for displaying SQL query results with system clipboard row/table copying."""
14
+
15
+ BINDINGS = [
16
+ *DataTable.BINDINGS,
17
+ Binding("ctrl+c,super+c", "copy_row", "Copy Row", show=False),
18
+ Binding("ctrl+a,super+a", "copy_table", "Copy All", show=False),
19
+ ]
20
+
21
+ def action_copy_row(self) -> None:
22
+ """Copy highlighted row or cell to clipboard."""
23
+ try:
24
+ if self.cursor_type == "row" and self.cursor_row is not None:
25
+ row_data = self.get_row_at(self.cursor_row)
26
+ text = "\t".join(str(v) if v is not None else "" for v in row_data)
27
+ copy_to_system_clipboard(text)
28
+ self.app.copy_to_clipboard(text)
29
+ self.notify(f"Rij gekopieerd ({len(row_data)} kolommen)", timeout=1.5)
30
+ elif self.cursor_type == "cell" and self.cursor_coordinate is not None:
31
+ cell_data = self.get_cell_at(self.cursor_coordinate)
32
+ text = str(cell_data) if cell_data is not None else ""
33
+ copy_to_system_clipboard(text)
34
+ self.app.copy_to_clipboard(text)
35
+ self.notify("Cel gekopieerd naar klembord", timeout=1.5)
36
+ except Exception as e:
37
+ logger.error(f"Error copying table row: {e}")
38
+
39
+ def action_copy_table(self) -> None:
40
+ """Copy entire table with column headers as TSV."""
41
+ try:
42
+ lines = []
43
+ headers = [col.label.plain for col in self.columns.values()]
44
+ if headers:
45
+ lines.append("\t".join(headers))
46
+ for row_idx in range(self.row_count):
47
+ row_data = self.get_row_at(row_idx)
48
+ lines.append("\t".join(str(v) if v is not None else "" for v in row_data))
49
+ text = "\n".join(lines)
50
+ copy_to_system_clipboard(text)
51
+ self.app.copy_to_clipboard(text)
52
+ self.notify(f"Tabel gekopieerd ({self.row_count} rijen)", timeout=1.5)
53
+ except Exception as e:
54
+ logger.error(f"Error copying entire table: {e}")
@@ -0,0 +1,207 @@
1
+ """SQL editor widget with syntax highlighting, undo/redo, and keyboard bindings."""
2
+
3
+ import re
4
+ from textual.binding import Binding
5
+ from textual.events import Click
6
+ from textual.widgets import TextArea
7
+ from textual.widgets.text_area import Selection
8
+
9
+ from openduck.core.clipboard import copy_to_system_clipboard, get_system_clipboard
10
+ from openduck.core.sql_utils import toggle_sql_comment_lines
11
+
12
+ # Construct bindings for SqlTextArea
13
+ _sql_text_area_bindings = []
14
+ for b in TextArea.BINDINGS:
15
+ if b.action == "cursor_line_start":
16
+ _sql_text_area_bindings.append(Binding("home", "cursor_line_start", "Cursor line start", show=False))
17
+ elif b.action == "select_all":
18
+ _sql_text_area_bindings.append(Binding("f7,ctrl+a,super+a,cmd+a,command+a", "select_all", "Select all", show=False))
19
+ elif b.action == "copy":
20
+ _sql_text_area_bindings.append(Binding("ctrl+c,super+c,cmd+c,command+c", "copy", "Copy", show=False))
21
+ elif b.action == "paste":
22
+ _sql_text_area_bindings.append(Binding("ctrl+v,super+v,cmd+v,command+v", "paste", "Paste", show=False))
23
+ elif b.action == "cut":
24
+ _sql_text_area_bindings.append(Binding("ctrl+x,super+x,cmd+x,command+x", "cut", "Cut", show=False))
25
+ elif b.action == "undo":
26
+ _sql_text_area_bindings.append(Binding("ctrl+z,super+z,cmd+z,command+z", "undo", "Undo", show=False))
27
+ elif b.action == "redo":
28
+ _sql_text_area_bindings.append(Binding(
29
+ "ctrl+y,super+y,cmd+y,command+y,shift+ctrl+z,ctrl+shift+z,shift+super+z,super+shift+z,shift+cmd+z,cmd+shift+z,shift+command+z,command+shift+z",
30
+ "redo", "Redo", show=False
31
+ ))
32
+ else:
33
+ _sql_text_area_bindings.append(b)
34
+
35
+ _sql_text_area_bindings.append(
36
+ Binding(
37
+ "f3,ctrl+slash,super+slash,cmd+slash,command+slash,ctrl+underscore,super+underscore,cmd+underscore,command+underscore,ctrl+7,super+7,ctrl+minus,super+minus",
38
+ "toggle_comment", "Comment", show=False
39
+ )
40
+ )
41
+
42
+
43
+ class SqlTextArea(TextArea):
44
+ """Specialized TextArea widget for SQL input with enhanced selection & clipboard support."""
45
+
46
+ allow_select = False
47
+ BINDINGS = _sql_text_area_bindings
48
+
49
+ def select_word_at(self, location: tuple[int, int]) -> None:
50
+ """Select the word at the given (row, col) location."""
51
+ row, col = location
52
+ if row >= self.document.line_count:
53
+ return
54
+ line = self.document.get_line(row)
55
+ if not line:
56
+ return
57
+ col = max(0, min(col, len(line)))
58
+
59
+ word_char_regex = re.compile(r'\w')
60
+ if col < len(line) and not word_char_regex.match(line[col]) and col > 0 and word_char_regex.match(line[col - 1]):
61
+ col -= 1
62
+
63
+ if col < len(line) and word_char_regex.match(line[col]):
64
+ start_col = col
65
+ while start_col > 0 and word_char_regex.match(line[start_col - 1]):
66
+ start_col -= 1
67
+ end_col = col
68
+ while end_col < len(line) and word_char_regex.match(line[end_col]):
69
+ end_col += 1
70
+ self.selection = Selection((row, start_col), (row, end_col))
71
+ else:
72
+ start_col = col
73
+ while start_col > 0 and not line[start_col - 1].isspace() and not word_char_regex.match(line[start_col - 1]):
74
+ start_col -= 1
75
+ end_col = col
76
+ while end_col < len(line) and not line[end_col].isspace() and not word_char_regex.match(line[end_col]):
77
+ end_col += 1
78
+ if start_col < end_col:
79
+ self.selection = Selection((row, start_col), (row, end_col))
80
+
81
+ async def _on_click(self, event: Click) -> None:
82
+ if event.chain == 2:
83
+ target = self.get_target_document_location(event)
84
+ self.select_word_at(target)
85
+ if self.selected_text:
86
+ copy_to_system_clipboard(self.selected_text)
87
+ self.app._clipboard = self.selected_text
88
+ event.stop()
89
+ return
90
+ elif event.chain == 3:
91
+ cursor_row, _ = self.get_target_document_location(event)
92
+ self.select_line(cursor_row)
93
+ if self.selected_text:
94
+ copy_to_system_clipboard(self.selected_text)
95
+ self.app._clipboard = self.selected_text
96
+ event.stop()
97
+ return
98
+ await super()._on_click(event)
99
+
100
+ def _end_mouse_selection(self) -> None:
101
+ """When mouse selection finishes, copy selected text automatically to system clipboard."""
102
+ super()._end_mouse_selection()
103
+ if self.selected_text:
104
+ copy_to_system_clipboard(self.selected_text)
105
+ self.app._clipboard = self.selected_text
106
+
107
+ def action_copy(self) -> None:
108
+ """Copy selected text (or current line if no selection, like VS Code) to system clipboard."""
109
+ selected_text = self.selected_text
110
+ if selected_text:
111
+ copy_to_system_clipboard(selected_text)
112
+ self.app.copy_to_clipboard(selected_text)
113
+ self.notify("Gekopieerd naar klembord", timeout=1.5)
114
+ else:
115
+ row = self.cursor_location[0]
116
+ line = self.document.get_line(row)
117
+ if line:
118
+ copy_to_system_clipboard(line)
119
+ self.app.copy_to_clipboard(line)
120
+ self.notify("Regel gekopieerd naar klembord", timeout=1.5)
121
+
122
+ def action_cut(self) -> None:
123
+ """Cut selected text to system clipboard."""
124
+ selected_text = self.selected_text
125
+ if selected_text:
126
+ copy_to_system_clipboard(selected_text)
127
+ self.app.copy_to_clipboard(selected_text)
128
+ self.replace("", *self.selection)
129
+ self.notify("Geknipt naar klembord", timeout=1.5)
130
+ else:
131
+ row = self.cursor_location[0]
132
+ line = self.document.get_line(row)
133
+ if line:
134
+ copy_to_system_clipboard(line)
135
+ self.app.copy_to_clipboard(line)
136
+ self.action_delete_line()
137
+ self.notify("Regel geknipt naar klembord", timeout=1.5)
138
+
139
+ def action_paste(self) -> None:
140
+ """Paste from system clipboard (macOS pbpaste / local clipboard)."""
141
+ if self.read_only:
142
+ return
143
+ text = get_system_clipboard()
144
+ if not text:
145
+ text = self.app.clipboard
146
+ if text:
147
+ if result := self._replace_via_keyboard(text, *self.selection):
148
+ self.move_cursor(result.end_location)
149
+
150
+ def action_select_all(self) -> None:
151
+ """Select all text in document."""
152
+ self.select_all()
153
+
154
+ def action_toggle_comment(self) -> None:
155
+ """Toggle SQL comment (-- ) like in VS Code."""
156
+ selection = self.selection
157
+ start_row, start_col = selection.start
158
+ end_row, end_col = selection.end
159
+
160
+ if start_row > end_row or (start_row == end_row and start_col > end_col):
161
+ start_row, end_row = end_row, start_row
162
+ start_col, end_col = end_col, start_col
163
+
164
+ if end_row > start_row and end_col == 0:
165
+ actual_end_row = end_row - 1
166
+ else:
167
+ actual_end_row = end_row
168
+
169
+ actual_end_row = min(actual_end_row, max(0, self.document.line_count - 1))
170
+ lines = [self.document.get_line(r) for r in range(start_row, actual_end_row + 1)]
171
+ new_lines = toggle_sql_comment_lines(lines)
172
+
173
+ replacement = "\n".join(new_lines)
174
+ self.replace(replacement, (start_row, 0), (actual_end_row, len(lines[-1])), maintain_selection_offset=False)
175
+
176
+ if selection.is_empty:
177
+ new_col = min(len(new_lines[0]), max(0, start_col + (len(new_lines[0]) - len(lines[0]))))
178
+ self.move_cursor((start_row, new_col))
179
+ else:
180
+ self.selection = Selection((start_row, 0), (actual_end_row, len(new_lines[-1])))
181
+
182
+ is_commented = any(l.lstrip().startswith("--") for l in new_lines if l.strip())
183
+ status = "toegevoegd (--)" if is_commented else "verwijderd"
184
+ self.notify(f"Commentaar {status}", timeout=1.2)
185
+
186
+ def on_key(self, event) -> None:
187
+ key = (event.key or "").lower()
188
+ if key in ("super+z", "cmd+z", "command+z", "ctrl+z"):
189
+ self.action_undo()
190
+ event.stop()
191
+ event.prevent_default()
192
+ elif key in (
193
+ "super+shift+z", "shift+super+z", "cmd+shift+z", "shift+cmd+z",
194
+ "command+shift+z", "shift+command+z", "ctrl+shift+z", "shift+ctrl+z",
195
+ "super+y", "cmd+y", "command+y", "ctrl+y"
196
+ ):
197
+ self.action_redo()
198
+ event.stop()
199
+ event.prevent_default()
200
+
201
+ def action_undo(self) -> None:
202
+ """Undo last action in editor."""
203
+ self.undo()
204
+
205
+ def action_redo(self) -> None:
206
+ """Redo last undone action in editor."""
207
+ self.redo()
openduck/main.py ADDED
@@ -0,0 +1,25 @@
1
+ """OpenDuck - Main application entry point."""
2
+
3
+ import logging
4
+ from openduck.gui.app import DuckCLI
5
+
6
+
7
+ def setup_logging():
8
+ """Configure application logging."""
9
+ logging.basicConfig(
10
+ filename='openduck_debug.log',
11
+ level=logging.DEBUG,
12
+ format='%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s',
13
+ filemode='a'
14
+ )
15
+
16
+
17
+ def main():
18
+ """Run OpenDuck."""
19
+ setup_logging()
20
+ app = DuckCLI()
21
+ app.run()
22
+
23
+
24
+ if __name__ == "__main__":
25
+ main()
@@ -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,30 @@
1
+ openduck/__init__.py,sha256=blZkVJJEm5MV8UvCh7dU0Uhlteew0j6RpQPsCZHKX9M,1022
2
+ openduck/__main__.py,sha256=vbtNJq4whJ_j0wceIQeAz_VpPmNUYEEK6K-X9HWfjyo,133
3
+ openduck/main.py,sha256=1on3Q6dNMHglPq7Dy4FmlBm8CAbCNIsx7PIOoSm5oXc,513
4
+ openduck/core/__init__.py,sha256=uXQyQ1S-e1NtHD2MkyrKZJoGVzAQnCGvKmScFFU3CyQ,924
5
+ openduck/core/clipboard.py,sha256=BMfgqngof3lqBAwzbqX7xSeRKi-p4xMaYusp48nvwjo,1996
6
+ openduck/core/config.py,sha256=Rwv0hYkDgeNymDRWsT11A6Ck3NzrJOfuX-B2OI5NTzI,3401
7
+ openduck/core/error_classifier.py,sha256=rPCUYt8DeBg8kW_U7Cf5_ulWFYm0E9Wf010MPzSKbxA,3123
8
+ openduck/core/sql_utils.py,sha256=SVUI3HUXAMuqBX5j_5uEXf_Mfdx5h4AdP_GjVXZ04lQ,2377
9
+ openduck/gui/__init__.py,sha256=bwTTe2INx35q9QOCxS4Twz9y-SsbtD_cDj_BJzOVvc8,146
10
+ openduck/gui/app.py,sha256=HsH_jy0BjUSe6XnqymU8Yiayf3fyFF1xBoFs3LVS22c,33735
11
+ openduck/gui/styles.py,sha256=Psog9l8jgpI9zhNcmYgiu2Eufo2M73eycEQ_jCzGvGs,2603
12
+ openduck/gui/dialogs/__init__.py,sha256=5coq1SnwAzs5wSWVN9IJu7HFgIT_3L9dQM2r_JpTsaI,307
13
+ openduck/gui/dialogs/add_connection_dialog.py,sha256=RuMlgIPJOJlJ3BY82pCl9-BLwZJT39Fj98YNbmJQqbY,4092
14
+ openduck/gui/dialogs/export_dialog.py,sha256=pb_Cy-priGiGIvB9isYXtnMEZ-GnnTIoqmYS2lZsdvA,4436
15
+ openduck/gui/dialogs/save_query_dialog.py,sha256=s0Qf8B5uaKoDx7jIRH__zj9R2_dzMwPyj0CkcMad7oI,1600
16
+ openduck/gui/screens/__init__.py,sha256=vp5_5h5igLaNjyvzbGfh1qR9aj4QCaBBL7WyY4FtpWU,244
17
+ openduck/gui/screens/about.py,sha256=myUEf0x12ORnm92SNdsKEw__67ZCnF9fYUGuunIc54o,3153
18
+ openduck/gui/screens/matrix.py,sha256=kxckiA0ErxAUFGVo2WK4dK-Nv1DkhRz8jKV2V-j2pJY,3181
19
+ openduck/gui/widgets/__init__.py,sha256=6v47jeMAjSuksc2QqOn6glV7LejuFl_uzMNaxqXMz3g,498
20
+ openduck/gui/widgets/database_tree.py,sha256=iyrZVsslvXlk2NX7O9HSGBnLO255ku7tnKWkMcCyHKU,4289
21
+ openduck/gui/widgets/file_tree.py,sha256=iCTeWoVVDk2WaBntZ544qz_hg4zr6FOibzOxtE06W4U,464
22
+ openduck/gui/widgets/query_tab.py,sha256=xnyr3hUIa231a1aPN6BtXOpsi4FJgSeCIwK8RJ6aZfM,2613
23
+ openduck/gui/widgets/resize_handle.py,sha256=g4nBIvS0sKf-lmnUEzFx6jwzCklsgxRX2fDHdIjMwtY,1089
24
+ openduck/gui/widgets/results_table.py,sha256=vUx2qTbV0y2nLRlSQbPaMqg4xWeJcClMTFUfLfXLzgI,2358
25
+ openduck/gui/widgets/sql_editor.py,sha256=16ADxQZfWJQo1byv51hruBCwv8P3njUrUvmdjJ5zNEg,8774
26
+ openduck_cli-0.1.0.dist-info/METADATA,sha256=1wHkEYmUW1zWjXOLgOkN3xriie9JTGPRudAG-Oy41pM,8670
27
+ openduck_cli-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
28
+ openduck_cli-0.1.0.dist-info/entry_points.txt,sha256=MHqMGWlgdS2pUc2ra2zovSadppo0RhYStZWkBMDWZ1I,82
29
+ openduck_cli-0.1.0.dist-info/top_level.txt,sha256=RI2CatGYvLV8ZiXyOEKqXcTfR5WEB7WOKc2SFfdZO3E,9
30
+ openduck_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ openduck = openduck.main:main
3
+ openduck-cli = openduck.main:main
@@ -0,0 +1 @@
1
+ openduck