harlequin 2.5.2__tar.gz → 2.7.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.
- {harlequin-2.5.2 → harlequin-2.7.0}/.gitignore +4 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/CHANGELOG.md +35 -1
- {harlequin-2.5.2 → harlequin-2.7.0}/PKG-INFO +8 -7
- {harlequin-2.5.2 → harlequin-2.7.0}/pyproject.toml +17 -9
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/app.py +52 -40
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/autocomplete/completers.py +20 -13
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/cli.py +29 -5
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/colors.py +3 -1
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/components/code_editor.py +2 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/components/data_catalog/tree.py +1 -16
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/components/error_modal.py +5 -3
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/config_wizard.py +2 -3
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/exception.py +4 -2
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/export.py +2 -2
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin_duckdb/adapter.py +1 -1
- {harlequin-2.5.2 → harlequin-2.7.0}/LICENSE +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/README.md +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/__init__.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/__main__.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/actions.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/adapter.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/app.tcss +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/app_base.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/autocomplete/__init__.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/autocomplete/completion.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/autocomplete/constants.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/bindings.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/cache.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/catalog.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/catalog_cache.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/components/__init__.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/components/confirm_modal.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/components/data_catalog/__init__.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/components/data_catalog/database_tree.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/components/data_catalog/file_tree.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/components/data_catalog/s3_tree.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/components/debug_info.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/components/export_screen.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/components/help_screen.md +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/components/help_screen.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/components/history_screen.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/components/results_viewer.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/components/run_query_bar.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/config.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/copy_formats.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/copy_widgets.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/driver.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/editor_cache.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/global.tcss +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/history.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/keymap.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/keys_app.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/keys_app.tcss +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/locale_manager.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/messages.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/options.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/plugins.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/py.typed +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/transaction_mode.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin/windows_timezone.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin_duckdb/__init__.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin_duckdb/catalog.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin_duckdb/cli_options.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin_duckdb/completions.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin_duckdb/interactions.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin_duckdb/py.typed +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin_sqlite/__init__.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin_sqlite/adapter.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin_sqlite/catalog.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin_sqlite/cli_options.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin_sqlite/completions.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin_sqlite/interactions.py +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin_sqlite/py.typed +0 -0
- {harlequin-2.5.2 → harlequin-2.7.0}/src/harlequin_vscode/__init__.py +0 -0
|
@@ -179,3 +179,7 @@ cython_debug/
|
|
|
179
179
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
180
180
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
181
181
|
#.idea/
|
|
182
|
+
|
|
183
|
+
# Claude Code
|
|
184
|
+
CLAUDE.md
|
|
185
|
+
.claude/
|
|
@@ -4,6 +4,38 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.7.0] - 2026-08-05
|
|
8
|
+
|
|
9
|
+
### Dependencies
|
|
10
|
+
|
|
11
|
+
- Upgrades Textual to 8.2.8 (from 6.4.0), with matching updates to textual-textarea and textual-fastdatatable.
|
|
12
|
+
- Raises the minimum DuckDB version to 1.1.1 (from 0.8.0) on Python < 3.14.
|
|
13
|
+
- Declares `pyperclip` as a direct dependency; it was previously installed only as a dependency of textual-textarea.
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
- Fixes `harlequin --help`, which printed the help text and then crashed.
|
|
18
|
+
- Harlequin no longer prints a `UserWarning` for each duplicated CLI flag (`The parameter -u is used more than once`) on every invocation.
|
|
19
|
+
- Copying the text of an error modal, a catalog label, or a data selection now works over ssh and in terminals where pyperclip has no backend, and reports a useful message when it fails.
|
|
20
|
+
- Fixes a crash when rendering tooltips for cells containing JSON or other markup-like values in the Results Viewer ([#933](https://github.com/tconbeer/harlequin/issues/933) - thank you [@crossi-dev](https://github.com/crossi-dev)!).
|
|
21
|
+
- Fixes a crash when rendering `bytes` values in the Results Viewer ([#974](https://github.com/tconbeer/harlequin/issues/974) - thank you [@Pawansingh3889](https://github.com/Pawansingh3889)!).
|
|
22
|
+
- Fixes a bug in the Query Editor where double-clicking a word shortly after double-clicking a different word would select the line or the entire query, instead of the second word ([#708](https://github.com/tconbeer/harlequin/issues/708)).
|
|
23
|
+
|
|
24
|
+
### Refactoring
|
|
25
|
+
|
|
26
|
+
- The Data Catalog now uses Textual's `Click.chain` to detect double-clicks, instead of tracking the previously-clicked line with a timer ([#708](https://github.com/tconbeer/harlequin/issues/708)).
|
|
27
|
+
- The Query Editor now uses Textual's `Click.chain` for double-, triple-, and quadruple-click selection, instead of tracking consecutive clicks with a timer ([#708](https://github.com/tconbeer/harlequin/issues/708)).
|
|
28
|
+
|
|
29
|
+
## [2.6.0] - 2026-08-03
|
|
30
|
+
|
|
31
|
+
### Performance
|
|
32
|
+
|
|
33
|
+
- Autocomplete no longer re-sorts the full completion list every time the Data Catalog lazy-loads a node's children; new completions are staged and merged at most once per second. This keeps the UI responsive while the catalog loads in the background, especially against high-latency (remote) databases.
|
|
34
|
+
|
|
35
|
+
### Bug Fixes
|
|
36
|
+
|
|
37
|
+
- Refreshes the file tree in the Data Catalog after Harlequin writes a file, either by saving from the editor or exporting data, so newly written files appear without a manual refresh ([#871](https://github.com/tconbeer/harlequin/issues/871)).
|
|
38
|
+
|
|
7
39
|
## [2.5.2] - 2026-03-25
|
|
8
40
|
|
|
9
41
|
### Bug Fixes
|
|
@@ -873,7 +905,9 @@ All notable changes to this project will be documented in this file.
|
|
|
873
905
|
|
|
874
906
|
- Use the DuckDB CLI.
|
|
875
907
|
|
|
876
|
-
[unreleased]: https://github.com/tconbeer/harlequin/compare/2.
|
|
908
|
+
[unreleased]: https://github.com/tconbeer/harlequin/compare/2.7.0...HEAD
|
|
909
|
+
[2.7.0]: https://github.com/tconbeer/harlequin/compare/2.6.0...2.7.0
|
|
910
|
+
[2.6.0]: https://github.com/tconbeer/harlequin/compare/2.5.2...2.6.0
|
|
877
911
|
[2.5.2]: https://github.com/tconbeer/harlequin/compare/2.5.1...2.5.2
|
|
878
912
|
[2.5.1]: https://github.com/tconbeer/harlequin/compare/2.5.0...2.5.1
|
|
879
913
|
[2.5.0]: https://github.com/tconbeer/harlequin/compare/2.4.1...2.5.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: harlequin
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.0
|
|
4
4
|
Summary: The SQL IDE for Your Terminal.
|
|
5
5
|
Project-URL: Homepage, https://harlequin.sh
|
|
6
6
|
Project-URL: Documentation, https://harlequin.sh/docs/getting-started/index
|
|
@@ -21,17 +21,18 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.14
|
|
23
23
|
Requires-Python: >=3.10
|
|
24
|
-
Requires-Dist: click==8.
|
|
25
|
-
Requires-Dist: duckdb>=
|
|
24
|
+
Requires-Dist: click==8.3.3
|
|
25
|
+
Requires-Dist: duckdb>=1.1.1; python_version < '3.14'
|
|
26
26
|
Requires-Dist: duckdb>=1.4.2; python_version >= '3.14'
|
|
27
27
|
Requires-Dist: pandas>=2.3; python_version >= '3.14'
|
|
28
28
|
Requires-Dist: platformdirs<5.0,>=3.10
|
|
29
|
+
Requires-Dist: pyperclip<2,>=1.9
|
|
29
30
|
Requires-Dist: questionary==2.0.1
|
|
30
|
-
Requires-Dist: rich-click==1.8
|
|
31
|
+
Requires-Dist: rich-click==1.9.8
|
|
31
32
|
Requires-Dist: shandy-sqlfmt>=0.28.2
|
|
32
|
-
Requires-Dist: textual-fastdatatable==0.
|
|
33
|
-
Requires-Dist: textual-textarea==0.
|
|
34
|
-
Requires-Dist: textual==
|
|
33
|
+
Requires-Dist: textual-fastdatatable==0.16.0
|
|
34
|
+
Requires-Dist: textual-textarea==0.18.1
|
|
35
|
+
Requires-Dist: textual==8.2.8
|
|
35
36
|
Requires-Dist: tomlkit<0.14.0,>=0.12.5
|
|
36
37
|
Provides-Extra: adbc
|
|
37
38
|
Requires-Dist: harlequin-adbc; extra == 'adbc'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
|
|
3
3
|
name = "harlequin"
|
|
4
|
-
version = "2.
|
|
4
|
+
version = "2.7.0"
|
|
5
5
|
requires-python = ">=3.10"
|
|
6
6
|
description = "The SQL IDE for Your Terminal."
|
|
7
7
|
authors = [
|
|
@@ -23,20 +23,23 @@ classifiers = [
|
|
|
23
23
|
]
|
|
24
24
|
dependencies = [
|
|
25
25
|
# textual and component libraries
|
|
26
|
-
"textual==
|
|
27
|
-
"textual-fastdatatable==0.
|
|
28
|
-
"textual-textarea==0.
|
|
26
|
+
"textual==8.2.8",
|
|
27
|
+
"textual-fastdatatable==0.16.0",
|
|
28
|
+
"textual-textarea==0.18.1",
|
|
29
29
|
|
|
30
30
|
# click
|
|
31
|
-
"click==8.
|
|
32
|
-
|
|
31
|
+
"click==8.3.3",
|
|
32
|
+
# 1.9 is the first release compatible with click 8.2's Parameter.make_metavar()
|
|
33
|
+
"rich-click==1.9.8",
|
|
33
34
|
|
|
34
35
|
# other deps
|
|
35
|
-
"duckdb>=
|
|
36
|
+
"duckdb>=1.1.1; python_version < '3.14'",
|
|
36
37
|
"shandy-sqlfmt>=0.28.2",
|
|
37
38
|
"platformdirs>=3.10,<5.0",
|
|
38
39
|
"tomlkit>=0.12.5,<0.14.0",
|
|
39
40
|
"questionary==2.0.1",
|
|
41
|
+
# imported directly by components/error_modal.py; also a textual-textarea dep
|
|
42
|
+
"pyperclip>=1.9,<2",
|
|
40
43
|
|
|
41
44
|
# ensure we install from wheels
|
|
42
45
|
"duckdb>=1.4.2; python_version >= '3.14'",
|
|
@@ -68,14 +71,15 @@ dev = [
|
|
|
68
71
|
]
|
|
69
72
|
static = [
|
|
70
73
|
"ruff>=0.14.1",
|
|
71
|
-
"mypy==
|
|
74
|
+
"mypy==2.0.0,<3",
|
|
72
75
|
"pandas-stubs>=2,<3",
|
|
73
76
|
"boto3-stubs>=1.34.23,<2",
|
|
74
77
|
]
|
|
75
78
|
test = [
|
|
76
79
|
"pytest>=9.0.2,<9.1.0",
|
|
77
80
|
"pytest-asyncio==1.3.0",
|
|
78
|
-
"pytest-
|
|
81
|
+
"pytest-xdist>=3.6",
|
|
82
|
+
"pytest-textual-snapshot @ git+https://github.com/tconbeer/pytest-textual-snapshot.git@a2c6763b45283f76757a123342900accefcf04e8",
|
|
79
83
|
"boto3==1.34.22",
|
|
80
84
|
# extension tests require consistent version of duckdb.
|
|
81
85
|
"duckdb==1.5.1",
|
|
@@ -186,6 +190,10 @@ strict_equality = true
|
|
|
186
190
|
|
|
187
191
|
[tool.pytest.ini_options]
|
|
188
192
|
|
|
193
|
+
# Tests run in parallel, one worker per CPU. Pass -n0 to run them in a single
|
|
194
|
+
# process, e.g. when using a debugger or -s.
|
|
195
|
+
addopts = ["-n", "auto"]
|
|
196
|
+
|
|
189
197
|
markers = [
|
|
190
198
|
"online: requeries internet connection (deselect with '-m \"not online\"')",
|
|
191
199
|
"use_cache: override the autouse fixture the disables the buffer cache",
|
|
@@ -27,6 +27,7 @@ from textual.lazy import Lazy
|
|
|
27
27
|
from textual.message import Message
|
|
28
28
|
from textual.reactive import reactive
|
|
29
29
|
from textual.screen import Screen, ScreenResultCallbackType, ScreenResultType
|
|
30
|
+
from textual.timer import Timer
|
|
30
31
|
from textual.types import CSSPathType
|
|
31
32
|
from textual.widget import AwaitMount, Widget
|
|
32
33
|
from textual.widgets import Button, Footer, Input
|
|
@@ -229,6 +230,7 @@ class Harlequin(AppBase):
|
|
|
229
230
|
self.query_timer: Union[float, None] = None
|
|
230
231
|
self.connection: HarlequinConnection | None = None
|
|
231
232
|
self.harlequin_driver = HarlequinDriver(app=self)
|
|
233
|
+
self._completer_merge_timer: Timer | None = None
|
|
232
234
|
|
|
233
235
|
if keymap_names is None:
|
|
234
236
|
keymap_names = ("vscode",)
|
|
@@ -282,12 +284,10 @@ class Harlequin(AppBase):
|
|
|
282
284
|
callback: ScreenResultCallbackType[ScreenResultType] | None = None,
|
|
283
285
|
wait_for_dismiss: bool = False,
|
|
284
286
|
) -> AwaitMount | asyncio.Future[ScreenResultType]:
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
):
|
|
290
|
-
self.editor.text_input._pause_blink(visible=True)
|
|
287
|
+
# the editor keeps focus while a modal is up, so its cursor has to be
|
|
288
|
+
# frozen explicitly.
|
|
289
|
+
if self.editor is not None and self.editor._has_focus_within:
|
|
290
|
+
self.editor.pause_blink(visible=True)
|
|
291
291
|
|
|
292
292
|
## TODO: PREVENT DUPLICATE SCREENS HERE.
|
|
293
293
|
return super().push_screen( # type: ignore[no-any-return,call-overload]
|
|
@@ -301,10 +301,9 @@ class Harlequin(AppBase):
|
|
|
301
301
|
if (
|
|
302
302
|
len(self.screen_stack) == 1
|
|
303
303
|
and self.editor is not None
|
|
304
|
-
and self.editor.text_input is not None
|
|
305
304
|
and self.editor._has_focus_within
|
|
306
305
|
):
|
|
307
|
-
self.editor.
|
|
306
|
+
self.editor.restart_blink()
|
|
308
307
|
return new_screen
|
|
309
308
|
|
|
310
309
|
def append_to_history(
|
|
@@ -410,25 +409,31 @@ class Harlequin(AppBase):
|
|
|
410
409
|
self.editor.insert_text_at_selection(text=message.insert_name)
|
|
411
410
|
self.editor.focus()
|
|
412
411
|
|
|
412
|
+
def _recycle_message(self, message: Message) -> None:
|
|
413
|
+
"""Re-post a message we can't handle yet, while we wait for the editor."""
|
|
414
|
+
callback = partial(self.post_message, message)
|
|
415
|
+
self.set_timer(delay=0.1, callback=callback)
|
|
416
|
+
|
|
417
|
+
def _copy_to_clipboard(self, text: str, success_message: str) -> None:
|
|
418
|
+
"""Copy text to the editor's clipboard, and to the system's if enabled.
|
|
419
|
+
|
|
420
|
+
The editor must be loaded; callers handle that by recycling their message.
|
|
421
|
+
textual-textarea also emits OSC 52, so this works over ssh, and reports
|
|
422
|
+
failures as TextAreaClipboardError, which CodeEditor turns into a notification.
|
|
423
|
+
"""
|
|
424
|
+
assert self.editor is not None
|
|
425
|
+
self.editor.copy_to_clipboard(text)
|
|
426
|
+
self.notify(success_message)
|
|
427
|
+
|
|
413
428
|
@on(HarlequinTree.NodeCopied)
|
|
414
429
|
def copy_node_name(self, message: HarlequinTree.NodeCopied) -> None:
|
|
415
430
|
message.stop()
|
|
416
431
|
if self.editor is None or self.editor.text_input is None:
|
|
417
|
-
|
|
418
|
-
callback = partial(self.post_message, message)
|
|
419
|
-
self.set_timer(delay=0.1, callback=callback)
|
|
432
|
+
self._recycle_message(message)
|
|
420
433
|
return
|
|
421
|
-
self.
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
and self.editor.text_input.system_copy is not None
|
|
425
|
-
):
|
|
426
|
-
try:
|
|
427
|
-
self.editor.text_input.system_copy(message.copy_name)
|
|
428
|
-
except Exception:
|
|
429
|
-
self.notify("Error copying data to system clipboard.", severity="error")
|
|
430
|
-
else:
|
|
431
|
-
self.notify("Selected label copied to clipboard.")
|
|
434
|
+
self._copy_to_clipboard(
|
|
435
|
+
message.copy_name, "Selected label copied to clipboard."
|
|
436
|
+
)
|
|
432
437
|
|
|
433
438
|
@on(HarlequinDriver.InsertTextAtSelection)
|
|
434
439
|
def driver_insert_text_into_editor(
|
|
@@ -541,23 +546,11 @@ class Harlequin(AppBase):
|
|
|
541
546
|
def copy_data_to_clipboard(self, message: DataTable.SelectionCopied) -> None:
|
|
542
547
|
message.stop()
|
|
543
548
|
if self.editor is None or self.editor.text_input is None:
|
|
544
|
-
|
|
545
|
-
callback = partial(self.post_message, message)
|
|
546
|
-
self.set_timer(delay=0.1, callback=callback)
|
|
549
|
+
self._recycle_message(message)
|
|
547
550
|
return
|
|
548
551
|
# Excel, sheets, and Snowsight all use a TSV format for copying tabular data
|
|
549
552
|
text = os.linesep.join("\t".join(map(str, row)) for row in message.values)
|
|
550
|
-
self.
|
|
551
|
-
if (
|
|
552
|
-
self.editor.use_system_clipboard
|
|
553
|
-
and self.editor.text_input.system_copy is not None
|
|
554
|
-
):
|
|
555
|
-
try:
|
|
556
|
-
self.editor.text_input.system_copy(text)
|
|
557
|
-
except Exception:
|
|
558
|
-
self.notify("Error copying data to system clipboard.", severity="error")
|
|
559
|
-
else:
|
|
560
|
-
self.notify("Selected data copied to clipboard.")
|
|
553
|
+
self._copy_to_clipboard(text, "Selected data copied to clipboard.")
|
|
561
554
|
|
|
562
555
|
@on(Worker.StateChanged)
|
|
563
556
|
async def handle_worker_error(self, message: Worker.StateChanged) -> None:
|
|
@@ -889,11 +882,15 @@ class Harlequin(AppBase):
|
|
|
889
882
|
if table is None:
|
|
890
883
|
show_export_error(error=ValueError("You must execute a query first."))
|
|
891
884
|
return
|
|
892
|
-
|
|
885
|
+
|
|
886
|
+
def on_export_success() -> None:
|
|
887
|
+
self.notify("Data exported successfully.")
|
|
888
|
+
self.data_catalog.update_file_tree()
|
|
889
|
+
|
|
893
890
|
callback = partial(
|
|
894
891
|
export_callback,
|
|
895
892
|
table=table,
|
|
896
|
-
success_callback=
|
|
893
|
+
success_callback=on_export_success,
|
|
897
894
|
error_callback=show_export_error,
|
|
898
895
|
)
|
|
899
896
|
self.app.push_screen(
|
|
@@ -1168,12 +1165,27 @@ class Harlequin(AppBase):
|
|
|
1168
1165
|
self.editor_collection.word_completer is not None
|
|
1169
1166
|
and self.editor_collection.member_completer is not None
|
|
1170
1167
|
):
|
|
1168
|
+
# appending to the catalog is cheap, but merging the full
|
|
1169
|
+
# completion list is O(n log n), so when the Data Catalog's lazy
|
|
1170
|
+
# loader delivers a stream of items (one message per node), we
|
|
1171
|
+
# defer the merge and run it at most once per second.
|
|
1171
1172
|
self.editor_collection.word_completer.extend_catalog(
|
|
1172
|
-
parent=parent, items=items
|
|
1173
|
+
parent=parent, items=items, defer_merge=True
|
|
1173
1174
|
)
|
|
1174
1175
|
self.editor_collection.member_completer.extend_catalog(
|
|
1175
|
-
parent=parent, items=items
|
|
1176
|
+
parent=parent, items=items, defer_merge=True
|
|
1176
1177
|
)
|
|
1178
|
+
if self._completer_merge_timer is None:
|
|
1179
|
+
self._completer_merge_timer = self.set_timer(
|
|
1180
|
+
1.0, self._merge_completers
|
|
1181
|
+
)
|
|
1182
|
+
|
|
1183
|
+
def _merge_completers(self) -> None:
|
|
1184
|
+
self._completer_merge_timer = None
|
|
1185
|
+
if self.editor_collection.word_completer is not None:
|
|
1186
|
+
self.editor_collection.word_completer.merge()
|
|
1187
|
+
if self.editor_collection.member_completer is not None:
|
|
1188
|
+
self.editor_collection.member_completer.merge()
|
|
1177
1189
|
|
|
1178
1190
|
def update_completers(self, catalog: Catalog) -> None:
|
|
1179
1191
|
if self.connection is None:
|
|
@@ -25,12 +25,8 @@ class WordCompleter:
|
|
|
25
25
|
self._function_completions = function_completions
|
|
26
26
|
self._catalog_completions = catalog_completions
|
|
27
27
|
self._extra_completions = extra_completions or []
|
|
28
|
-
self.completions: list[HarlequinCompletion] =
|
|
29
|
-
|
|
30
|
-
self._function_completions,
|
|
31
|
-
self._catalog_completions,
|
|
32
|
-
self._extra_completions,
|
|
33
|
-
)
|
|
28
|
+
self.completions: list[HarlequinCompletion] = []
|
|
29
|
+
self.merge()
|
|
34
30
|
|
|
35
31
|
def __call__(self, prefix: str) -> list[tuple[tuple[str, str], str]]:
|
|
36
32
|
"""
|
|
@@ -64,18 +60,29 @@ class WordCompleter:
|
|
|
64
60
|
|
|
65
61
|
def update_catalog(self, catalog: Catalog) -> None:
|
|
66
62
|
self._catalog_completions = build_catalog_completions(catalog=catalog)
|
|
67
|
-
self.
|
|
68
|
-
self._keyword_completions,
|
|
69
|
-
self._function_completions,
|
|
70
|
-
self._catalog_completions,
|
|
71
|
-
self._extra_completions,
|
|
72
|
-
)
|
|
63
|
+
self.merge()
|
|
73
64
|
|
|
74
|
-
def extend_catalog(
|
|
65
|
+
def extend_catalog(
|
|
66
|
+
self,
|
|
67
|
+
parent: CatalogItem,
|
|
68
|
+
items: list[CatalogItem],
|
|
69
|
+
defer_merge: bool = False,
|
|
70
|
+
) -> None:
|
|
75
71
|
# TODO: dedupe/merge on the parent's unique key, so we can load items from
|
|
76
72
|
# a cache and update them later when they are lazy-loaded.
|
|
77
73
|
new_completions = _build_children_completions(items=items, context=parent.label)
|
|
78
74
|
self._catalog_completions.extend(new_completions)
|
|
75
|
+
if not defer_merge:
|
|
76
|
+
self.merge()
|
|
77
|
+
|
|
78
|
+
def merge(self) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Rebuild the merged, sorted completions list. This is O(n log n) in the
|
|
81
|
+
total number of completions, so callers that extend the catalog many
|
|
82
|
+
times in quick succession (e.g., the Data Catalog's lazy loader) should
|
|
83
|
+
pass defer_merge=True to extend_catalog and call this once, on a
|
|
84
|
+
debounce.
|
|
85
|
+
"""
|
|
79
86
|
self.completions = self._merge_completions(
|
|
80
87
|
self._keyword_completions,
|
|
81
88
|
self._function_completions,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import sys
|
|
4
|
+
import warnings
|
|
4
5
|
from importlib.metadata import entry_points, version
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import Any, Sequence
|
|
@@ -36,7 +37,7 @@ DEFAULT_KEYMAP_NAMES = ["vscode"]
|
|
|
36
37
|
DOCS_URL = "https://harlequin.sh/docs/getting-started"
|
|
37
38
|
|
|
38
39
|
# general
|
|
39
|
-
click.rich_click.
|
|
40
|
+
click.rich_click.TEXT_MARKUP = "rich"
|
|
40
41
|
click.rich_click.COLOR_SYSTEM = "truecolor"
|
|
41
42
|
|
|
42
43
|
click.rich_click.STYLE_OPTIONS_TABLE_LEADING = 1
|
|
@@ -50,9 +51,21 @@ click.rich_click.STYLE_ARGUMENT = PINK
|
|
|
50
51
|
click.rich_click.STYLE_COMMAND = PINK
|
|
51
52
|
click.rich_click.STYLE_SWITCH = GREEN
|
|
52
53
|
|
|
53
|
-
# metavars
|
|
54
|
-
click.rich_click.
|
|
55
|
-
|
|
54
|
+
# metavars: drop the metavar column, and append the metavar to the help text instead
|
|
55
|
+
click.rich_click.OPTIONS_TABLE_COLUMN_TYPES = [
|
|
56
|
+
"required",
|
|
57
|
+
"opt_long",
|
|
58
|
+
"opt_short",
|
|
59
|
+
"help",
|
|
60
|
+
]
|
|
61
|
+
click.rich_click.OPTIONS_TABLE_HELP_SECTIONS = [
|
|
62
|
+
"help",
|
|
63
|
+
"deprecated",
|
|
64
|
+
"envvar",
|
|
65
|
+
"default",
|
|
66
|
+
"required",
|
|
67
|
+
"metavar",
|
|
68
|
+
]
|
|
56
69
|
click.rich_click.STYLE_METAVAR_APPEND = PURPLE
|
|
57
70
|
click.rich_click.STYLE_METAVAR_SEPARATOR = PURPLE
|
|
58
71
|
|
|
@@ -409,4 +422,15 @@ def harlequin() -> None:
|
|
|
409
422
|
The main entrypoint for the Harlequin IDE. Builds and executes the click Command.
|
|
410
423
|
"""
|
|
411
424
|
cli = build_cli()
|
|
412
|
-
|
|
425
|
+
with warnings.catch_warnings():
|
|
426
|
+
# Two installed adapters can claim the same flag for different options --
|
|
427
|
+
# -u is duckdb's --allow-unsigned-extensions and postgres' --user, and -d
|
|
428
|
+
# is --database for one adapter and --dbname for another. Click warns about
|
|
429
|
+
# each collision on every invocation, but the flags belong to separate
|
|
430
|
+
# plugins, so there is nothing the user can do about it.
|
|
431
|
+
warnings.filterwarnings(
|
|
432
|
+
"ignore",
|
|
433
|
+
message="The parameter .* is used more than once",
|
|
434
|
+
category=UserWarning,
|
|
435
|
+
)
|
|
436
|
+
cli()
|
|
@@ -31,7 +31,9 @@ HARLEQUIN_TEXTUAL_THEME = TextualTheme(
|
|
|
31
31
|
)
|
|
32
32
|
|
|
33
33
|
VALID_THEMES = BUILTIN_THEMES
|
|
34
|
-
|
|
34
|
+
# Harlequin doesn't support ANSI-passthrough themes.
|
|
35
|
+
VALID_THEMES.pop("ansi-dark", None)
|
|
36
|
+
VALID_THEMES.pop("ansi-light", None)
|
|
35
37
|
VALID_THEMES.update({"harlequin": HARLEQUIN_TEXTUAL_THEME})
|
|
36
38
|
|
|
37
39
|
HARLEQUIN_QUESTIONARY_STYLE = (
|
|
@@ -81,6 +81,8 @@ class CodeEditor(TextEditor, inherit_bindings=False):
|
|
|
81
81
|
|
|
82
82
|
def on_text_area_saved(self, message: TextAreaSaved) -> None:
|
|
83
83
|
self.app.notify(f"Editor contents saved to {message.path}")
|
|
84
|
+
if hasattr(self.app, "data_catalog"):
|
|
85
|
+
self.app.data_catalog.update_file_tree()
|
|
84
86
|
|
|
85
87
|
def on_text_area_clipboard_error(self) -> None:
|
|
86
88
|
if not self.has_shown_clipboard_error:
|
|
@@ -19,7 +19,6 @@ class HarlequinTree(Tree[TTreeNode], inherit_bindings=False):
|
|
|
19
19
|
COMPONENT_CLASSES: ClassVar[set[str]] = {
|
|
20
20
|
"harlequin-tree--type-label",
|
|
21
21
|
}
|
|
22
|
-
double_click: int | None = None
|
|
23
22
|
|
|
24
23
|
class CatalogError(Message):
|
|
25
24
|
def __init__(self, catalog_type: str, error: BaseException) -> None:
|
|
@@ -76,31 +75,17 @@ class HarlequinTree(Tree[TTreeNode], inherit_bindings=False):
|
|
|
76
75
|
click_line: Union[int, None] = meta.get("line", None)
|
|
77
76
|
if event.button == 1: # left button click
|
|
78
77
|
self.post_message(self.HideContextMenu())
|
|
79
|
-
if
|
|
80
|
-
self.double_click is not None
|
|
81
|
-
and click_line is not None
|
|
82
|
-
and self.double_click == click_line
|
|
83
|
-
):
|
|
78
|
+
if event.chain == 2 and click_line is not None: # double click
|
|
84
79
|
event.prevent_default()
|
|
85
80
|
node = self.get_node_at_line(click_line)
|
|
86
81
|
if node is not None:
|
|
87
82
|
self.post_message(self.NodeSubmitted(node=node))
|
|
88
83
|
node.expand()
|
|
89
|
-
else:
|
|
90
|
-
self.double_click = click_line
|
|
91
|
-
self.set_timer(
|
|
92
|
-
delay=0.5,
|
|
93
|
-
callback=self._clear_double_click,
|
|
94
|
-
name="double_click_timer",
|
|
95
|
-
)
|
|
96
84
|
elif event.button == 3 and click_line is not None: # right click
|
|
97
85
|
node = self.get_node_at_line(click_line)
|
|
98
86
|
if node is not None and isinstance(node.data, CatalogItem):
|
|
99
87
|
self.post_message(self.ShowContextMenu(node=node))
|
|
100
88
|
|
|
101
|
-
def _clear_double_click(self) -> None:
|
|
102
|
-
self.double_click = None
|
|
103
|
-
|
|
104
89
|
def action_submit(self) -> None:
|
|
105
90
|
if self.cursor_node is not None:
|
|
106
91
|
self.post_message(self.NodeSubmitted(node=self.cursor_node))
|
|
@@ -12,12 +12,14 @@ from textual.widgets import Static
|
|
|
12
12
|
class ClickableStatic(Static):
|
|
13
13
|
def on_click(self, message: events.Click) -> None:
|
|
14
14
|
message.stop()
|
|
15
|
+
text = str(self.content)
|
|
16
|
+
# OSC 52 works over ssh and where pyperclip has no backend
|
|
17
|
+
self.app.copy_to_clipboard(text)
|
|
15
18
|
try:
|
|
16
|
-
pyperclip.copy(
|
|
19
|
+
pyperclip.copy(text)
|
|
17
20
|
except pyperclip.PyperclipException:
|
|
18
21
|
pass
|
|
19
|
-
|
|
20
|
-
self.app.notify("Error copied to clipboard.")
|
|
22
|
+
self.app.notify("Error copied to clipboard.")
|
|
21
23
|
|
|
22
24
|
|
|
23
25
|
class ErrorModal(ModalScreen):
|
|
@@ -9,10 +9,9 @@ import tomlkit
|
|
|
9
9
|
from rich import print as rich_print
|
|
10
10
|
from rich.markup import escape
|
|
11
11
|
from rich.panel import Panel
|
|
12
|
-
from textual.theme import BUILTIN_THEMES
|
|
13
12
|
|
|
14
13
|
from harlequin.adapter import HarlequinAdapter
|
|
15
|
-
from harlequin.colors import HARLEQUIN_QUESTIONARY_STYLE, YELLOW
|
|
14
|
+
from harlequin.colors import HARLEQUIN_QUESTIONARY_STYLE, VALID_THEMES, YELLOW
|
|
16
15
|
from harlequin.config import (
|
|
17
16
|
Config,
|
|
18
17
|
ConfigFile,
|
|
@@ -67,7 +66,7 @@ def _wizard(config_path: Path | None) -> None:
|
|
|
67
66
|
|
|
68
67
|
theme = questionary.select(
|
|
69
68
|
message="What theme should this profile use?",
|
|
70
|
-
choices=sorted(
|
|
69
|
+
choices=sorted(VALID_THEMES.keys()),
|
|
71
70
|
default=selected_profile.get("theme", "harlequin"),
|
|
72
71
|
style=HARLEQUIN_QUESTIONARY_STYLE,
|
|
73
72
|
).unsafe_ask()
|
|
@@ -49,9 +49,11 @@ class HarlequinLocaleError(HarlequinError):
|
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
def pretty_print_error(error: HarlequinError) -> None:
|
|
52
|
-
from rich import
|
|
52
|
+
from rich.console import Console
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
# errors are diagnostics on a failing exit path, so they belong on stderr;
|
|
55
|
+
# rich.print would put them on stdout and contaminate piped output.
|
|
56
|
+
Console(stderr=True).print(pretty_error_message(error))
|
|
55
57
|
|
|
56
58
|
|
|
57
59
|
def pretty_error_message(error: HarlequinError) -> Panel:
|
|
@@ -64,7 +64,7 @@ def _export_csv(
|
|
|
64
64
|
if "header" not in kwargs:
|
|
65
65
|
kwargs["header"] = False
|
|
66
66
|
try:
|
|
67
|
-
relation = duckdb.
|
|
67
|
+
relation = duckdb.from_arrow(data)
|
|
68
68
|
relation.write_csv(file_name=dest_path, **kwargs)
|
|
69
69
|
except (duckdb.Error, OSError) as e:
|
|
70
70
|
raise HarlequinCopyError(
|
|
@@ -81,7 +81,7 @@ def _export_parquet(
|
|
|
81
81
|
import duckdb
|
|
82
82
|
|
|
83
83
|
try:
|
|
84
|
-
relation = duckdb.
|
|
84
|
+
relation = duckdb.from_arrow(data)
|
|
85
85
|
relation.write_parquet(
|
|
86
86
|
file_name=dest_path, compression=kwargs.get("compression")
|
|
87
87
|
)
|
|
@@ -53,7 +53,7 @@ class DuckDbCursor(HarlequinCursor):
|
|
|
53
53
|
|
|
54
54
|
def fetchall(self) -> AutoBackendType | None:
|
|
55
55
|
try:
|
|
56
|
-
result = self.relation.
|
|
56
|
+
result = self.relation.to_arrow_table()
|
|
57
57
|
except duckdb.InterruptException:
|
|
58
58
|
return None
|
|
59
59
|
except duckdb.Error as e:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|