sqlide 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.
- sqlide-0.1.0/.github/workflows/ci.yml +37 -0
- sqlide-0.1.0/.github/workflows/release.yml +17 -0
- sqlide-0.1.0/.gitignore +7 -0
- sqlide-0.1.0/.python-version +1 -0
- sqlide-0.1.0/LICENSE +21 -0
- sqlide-0.1.0/PKG-INFO +151 -0
- sqlide-0.1.0/README.md +123 -0
- sqlide-0.1.0/packaging/homebrew/README.md +11 -0
- sqlide-0.1.0/packaging/homebrew/sqlide.rb +36 -0
- sqlide-0.1.0/pyproject.toml +69 -0
- sqlide-0.1.0/src/sqlide/__init__.py +3 -0
- sqlide-0.1.0/src/sqlide/__main__.py +5 -0
- sqlide-0.1.0/src/sqlide/app.py +46 -0
- sqlide-0.1.0/src/sqlide/cli.py +64 -0
- sqlide-0.1.0/src/sqlide/clipboard.py +65 -0
- sqlide-0.1.0/src/sqlide/config/__init__.py +1 -0
- sqlide-0.1.0/src/sqlide/config/_toml.py +41 -0
- sqlide-0.1.0/src/sqlide/config/connections.py +76 -0
- sqlide-0.1.0/src/sqlide/config/keymap.py +25 -0
- sqlide-0.1.0/src/sqlide/config/paths.py +48 -0
- sqlide-0.1.0/src/sqlide/config/secrets.py +82 -0
- sqlide-0.1.0/src/sqlide/config/settings.py +32 -0
- sqlide-0.1.0/src/sqlide/consoles.py +100 -0
- sqlide-0.1.0/src/sqlide/db/__init__.py +1 -0
- sqlide-0.1.0/src/sqlide/db/completion.py +127 -0
- sqlide-0.1.0/src/sqlide/db/factory.py +37 -0
- sqlide-0.1.0/src/sqlide/db/metadata.py +174 -0
- sqlide-0.1.0/src/sqlide/db/result.py +52 -0
- sqlide-0.1.0/src/sqlide/db/session.py +339 -0
- sqlide-0.1.0/src/sqlide/db/types.py +84 -0
- sqlide-0.1.0/src/sqlide/doctor.py +60 -0
- sqlide-0.1.0/src/sqlide/drivers/__init__.py +1 -0
- sqlide-0.1.0/src/sqlide/drivers/catalog.toml +103 -0
- sqlide-0.1.0/src/sqlide/drivers/cli.py +74 -0
- sqlide-0.1.0/src/sqlide/drivers/custom.py +69 -0
- sqlide-0.1.0/src/sqlide/drivers/loader.py +75 -0
- sqlide-0.1.0/src/sqlide/drivers/maven.py +108 -0
- sqlide-0.1.0/src/sqlide/drivers/registry.py +117 -0
- sqlide-0.1.0/src/sqlide/export/__init__.py +12 -0
- sqlide-0.1.0/src/sqlide/export/base.py +86 -0
- sqlide-0.1.0/src/sqlide/export/csv_.py +40 -0
- sqlide-0.1.0/src/sqlide/export/html.py +44 -0
- sqlide-0.1.0/src/sqlide/export/json_.py +47 -0
- sqlide-0.1.0/src/sqlide/export/markdown.py +27 -0
- sqlide-0.1.0/src/sqlide/export/service.py +34 -0
- sqlide-0.1.0/src/sqlide/export/sql_insert.py +37 -0
- sqlide-0.1.0/src/sqlide/export/xlsx.py +109 -0
- sqlide-0.1.0/src/sqlide/grid/__init__.py +1 -0
- sqlide-0.1.0/src/sqlide/grid/copyfmt.py +97 -0
- sqlide-0.1.0/src/sqlide/grid/formatting.py +43 -0
- sqlide-0.1.0/src/sqlide/grid/model.py +109 -0
- sqlide-0.1.0/src/sqlide/history/__init__.py +5 -0
- sqlide-0.1.0/src/sqlide/history/store.py +117 -0
- sqlide-0.1.0/src/sqlide/jvm/__init__.py +1 -0
- sqlide-0.1.0/src/sqlide/jvm/locate.py +98 -0
- sqlide-0.1.0/src/sqlide/jvm/runtime.py +44 -0
- sqlide-0.1.0/src/sqlide/sql/__init__.py +1 -0
- sqlide-0.1.0/src/sqlide/sql/context.py +195 -0
- sqlide-0.1.0/src/sqlide/sql/dialects.py +93 -0
- sqlide-0.1.0/src/sqlide/sql/format.py +50 -0
- sqlide-0.1.0/src/sqlide/sql/keywords.py +143 -0
- sqlide-0.1.0/src/sqlide/sql/lexer.py +148 -0
- sqlide-0.1.0/src/sqlide/sql/snippets.py +44 -0
- sqlide-0.1.0/src/sqlide/sql/splitter.py +325 -0
- sqlide-0.1.0/src/sqlide/ui/__init__.py +1 -0
- sqlide-0.1.0/src/sqlide/ui/app.tcss +67 -0
- sqlide-0.1.0/src/sqlide/ui/commands.py +59 -0
- sqlide-0.1.0/src/sqlide/ui/keymap.py +32 -0
- sqlide-0.1.0/src/sqlide/ui/screens/__init__.py +0 -0
- sqlide-0.1.0/src/sqlide/ui/screens/connection_editor.py +113 -0
- sqlide-0.1.0/src/sqlide/ui/screens/dialogs.py +110 -0
- sqlide-0.1.0/src/sqlide/ui/screens/driver_manager.py +195 -0
- sqlide-0.1.0/src/sqlide/ui/screens/export_dialog.py +158 -0
- sqlide-0.1.0/src/sqlide/ui/screens/grid_dialogs.py +92 -0
- sqlide-0.1.0/src/sqlide/ui/screens/history.py +130 -0
- sqlide-0.1.0/src/sqlide/ui/screens/main.py +383 -0
- sqlide-0.1.0/src/sqlide/ui/screens/settings.py +77 -0
- sqlide-0.1.0/src/sqlide/ui/widgets/__init__.py +0 -0
- sqlide-0.1.0/src/sqlide/ui/widgets/completion_popup.py +81 -0
- sqlide-0.1.0/src/sqlide/ui/widgets/connections_list.py +54 -0
- sqlide-0.1.0/src/sqlide/ui/widgets/console_export.py +124 -0
- sqlide-0.1.0/src/sqlide/ui/widgets/console_tab.py +335 -0
- sqlide-0.1.0/src/sqlide/ui/widgets/console_tabs.py +117 -0
- sqlide-0.1.0/src/sqlide/ui/widgets/result_grid.py +435 -0
- sqlide-0.1.0/src/sqlide/ui/widgets/result_panel.py +73 -0
- sqlide-0.1.0/src/sqlide/ui/widgets/result_view.py +50 -0
- sqlide-0.1.0/src/sqlide/ui/widgets/schema_tree.py +145 -0
- sqlide-0.1.0/src/sqlide/ui/widgets/sql_editor.py +310 -0
- sqlide-0.1.0/src/sqlide/ui/widgets/status_bar.py +32 -0
- sqlide-0.1.0/src/sqlide/workspace.py +97 -0
- sqlide-0.1.0/tests/__init__.py +0 -0
- sqlide-0.1.0/tests/conftest.py +35 -0
- sqlide-0.1.0/tests/db/test_completion.py +68 -0
- sqlide-0.1.0/tests/db/test_embedded_dbs.py +104 -0
- sqlide-0.1.0/tests/db/test_h2_driver.py +14 -0
- sqlide-0.1.0/tests/db/test_metadata.py +28 -0
- sqlide-0.1.0/tests/db/test_session.py +94 -0
- sqlide-0.1.0/tests/db/test_stream_export.py +63 -0
- sqlide-0.1.0/tests/docker/conftest.py +107 -0
- sqlide-0.1.0/tests/docker/multi.py +91 -0
- sqlide-0.1.0/tests/docker/test_databases.py +166 -0
- sqlide-0.1.0/tests/docker/test_postgres.py +57 -0
- sqlide-0.1.0/tests/docker/test_sql_battery.py +172 -0
- sqlide-0.1.0/tests/ui/__init__.py +0 -0
- sqlide-0.1.0/tests/ui/conftest.py +20 -0
- sqlide-0.1.0/tests/ui/helpers.py +37 -0
- sqlide-0.1.0/tests/ui/test_app_e2e.py +170 -0
- sqlide-0.1.0/tests/ui/test_completion.py +98 -0
- sqlide-0.1.0/tests/ui/test_export_ui.py +156 -0
- sqlide-0.1.0/tests/ui/test_grid.py +328 -0
- sqlide-0.1.0/tests/ui/test_history.py +95 -0
- sqlide-0.1.0/tests/ui/test_polish.py +94 -0
- sqlide-0.1.0/tests/ui/test_schema.py +76 -0
- sqlide-0.1.0/tests/ui/test_tabs.py +356 -0
- sqlide-0.1.0/tests/unit/config/test_config.py +125 -0
- sqlide-0.1.0/tests/unit/drivers/test_custom.py +53 -0
- sqlide-0.1.0/tests/unit/drivers/test_maven.py +78 -0
- sqlide-0.1.0/tests/unit/drivers/test_registry_loader.py +81 -0
- sqlide-0.1.0/tests/unit/export/test_exporters.py +173 -0
- sqlide-0.1.0/tests/unit/grid/test_copyfmt.py +57 -0
- sqlide-0.1.0/tests/unit/grid/test_model.py +100 -0
- sqlide-0.1.0/tests/unit/jvm/test_locate.py +45 -0
- sqlide-0.1.0/tests/unit/sql/test_lexer.py +57 -0
- sqlide-0.1.0/tests/unit/sql/test_splitter.py +269 -0
- sqlide-0.1.0/tests/unit/test_cli.py +21 -0
- sqlide-0.1.0/tests/unit/test_clipboard.py +68 -0
- sqlide-0.1.0/tests/unit/test_consoles.py +65 -0
- sqlide-0.1.0/tests/unit/test_context.py +81 -0
- sqlide-0.1.0/tests/unit/test_format.py +39 -0
- sqlide-0.1.0/tests/unit/test_history.py +53 -0
- sqlide-0.1.0/tests/unit/test_keymap.py +23 -0
- sqlide-0.1.0/tests/unit/test_private_db.py +21 -0
- sqlide-0.1.0/tests/unit/test_snippets.py +54 -0
- sqlide-0.1.0/tests/unit/test_version.py +9 -0
- sqlide-0.1.0/uv.lock +1605 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
strategy:
|
|
11
|
+
fail-fast: false
|
|
12
|
+
matrix:
|
|
13
|
+
os: [ubuntu-latest, macos-latest]
|
|
14
|
+
runs-on: ${{ matrix.os }}
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-java@v4
|
|
18
|
+
with:
|
|
19
|
+
distribution: temurin
|
|
20
|
+
java-version: "21"
|
|
21
|
+
- uses: astral-sh/setup-uv@v5
|
|
22
|
+
with:
|
|
23
|
+
enable-cache: true
|
|
24
|
+
- run: uv sync --locked
|
|
25
|
+
- run: uv run ruff check
|
|
26
|
+
- run: uv run ruff format --check
|
|
27
|
+
- run: uv run pyright
|
|
28
|
+
- name: Tests
|
|
29
|
+
shell: bash
|
|
30
|
+
run: uv run pytest -q --tb=short -p no:cacheprovider 2>&1 | tee pytest.log; exit "${PIPESTATUS[0]}"
|
|
31
|
+
- name: Show failures as annotations
|
|
32
|
+
if: failure()
|
|
33
|
+
shell: bash
|
|
34
|
+
run: |
|
|
35
|
+
grep -E "^(FAILED|ERROR)|Error|assert " pytest.log | head -30 | while IFS= read -r line; do
|
|
36
|
+
echo "::error::${line:0:400}"
|
|
37
|
+
done
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
pypi:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: pypi
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write # PyPI trusted publishing, no token stored
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: astral-sh/setup-uv@v5
|
|
16
|
+
- run: uv build
|
|
17
|
+
- run: uv publish
|
sqlide-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
sqlide-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 XFaIT
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
sqlide-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sqlide
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Terminal SQL IDE (DataGrip-like) over JDBC
|
|
5
|
+
Project-URL: Homepage, https://github.com/XFaIT/sqlide
|
|
6
|
+
Project-URL: Repository, https://github.com/XFaIT/sqlide
|
|
7
|
+
Project-URL: Issues, https://github.com/XFaIT/sqlide/issues
|
|
8
|
+
Author: XFaIT
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: database,datagrip,jdbc,sql,terminal,tui
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Database :: Front-Ends
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Requires-Dist: httpx>=0.27
|
|
21
|
+
Requires-Dist: jpype1>=1.5
|
|
22
|
+
Requires-Dist: openpyxl>=3.1
|
|
23
|
+
Requires-Dist: platformdirs>=4
|
|
24
|
+
Requires-Dist: sqlglot>=25
|
|
25
|
+
Requires-Dist: textual[syntax]>=0.80
|
|
26
|
+
Requires-Dist: tomli-w>=1.0
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# sqlide
|
|
30
|
+
|
|
31
|
+
A DataGrip-style SQL IDE that runs in your terminal. Connect to any database over JDBC,
|
|
32
|
+
write SQL in an editor that outlines the statement about to run, and browse results in
|
|
33
|
+
sortable, copyable tables that export to CSV, XLSX and more.
|
|
34
|
+
|
|
35
|
+
Works on Linux, macOS and WSL.
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- **Any JDBC database.** PostgreSQL and ClickHouse work out of the box; MySQL, MariaDB,
|
|
40
|
+
Oracle, SQL Server, H2, SQLite and DuckDB are one keypress away. Add your own driver from
|
|
41
|
+
jar files or Maven coordinates, as in DataGrip. Drivers are downloaded from Maven Central
|
|
42
|
+
(checksum verified) and each one gets its own class loader.
|
|
43
|
+
- **Framed statement.** The statement under the cursor is outlined in the gutter; `F5`
|
|
44
|
+
runs it, a selection runs the selection, `Shift+F5` runs the whole file. Dialect-aware
|
|
45
|
+
splitting: `$$` quotes, nested comments, Oracle `/`, SQL Server `GO`, procedural blocks.
|
|
46
|
+
- **Result grid.** Sort by several columns, filter, select a rectangle, view long values,
|
|
47
|
+
load more rows on demand. Copy as TSV, CSV, Markdown, JSON or `INSERT` statements.
|
|
48
|
+
- **Export.** CSV, TSV, JSON, JSON Lines, Markdown, SQL inserts, HTML, XLSX. Export the loaded
|
|
49
|
+
rows or re-run the query and stream the full result to disk, with progress and cancel.
|
|
50
|
+
- **Schema tree and autocomplete.** Lazy metadata, `SELECT *` on Enter, alias-aware column
|
|
51
|
+
completion, keywords and functions per dialect.
|
|
52
|
+
- **Consoles and files.** Tabs with their own connection; consoles autosave and come back
|
|
53
|
+
after a restart; open and save `.sql` files (`sqlide a.sql b.sql`).
|
|
54
|
+
- **History, formatting, transactions.** Searchable query history, SQL formatting,
|
|
55
|
+
Auto/Manual commit with Commit/Rollback.
|
|
56
|
+
- **Rebindable keys, themes, command palette** (`Ctrl+P`). No AI features.
|
|
57
|
+
|
|
58
|
+
## Install
|
|
59
|
+
|
|
60
|
+
Requirements: Python 3.12+ and a Java 11+ runtime (`brew install openjdk`,
|
|
61
|
+
`apt install openjdk-21-jre`, ...). Check with `sqlide doctor`.
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
uv tool install sqlide # or: pipx install sqlide
|
|
65
|
+
sqlide
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Homebrew formula: see [packaging/homebrew](packaging/homebrew/README.md).
|
|
69
|
+
|
|
70
|
+
## Quick start
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
sqlide driver install postgres # or just connect: sqlide offers to download it
|
|
74
|
+
sqlide # Ctrl+N: new connection, Enter on it: connect
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Passwords are never written to disk. sqlide asks when you connect and keeps the password
|
|
78
|
+
in memory until you quit. A connection can instead read it from an environment variable
|
|
79
|
+
(`password_ref = "${env:PGPASSWORD}"`) or from a command such as a keychain CLI
|
|
80
|
+
(`password_cmd = "security find-generic-password -s mydb -w"`).
|
|
81
|
+
|
|
82
|
+
## Keys
|
|
83
|
+
|
|
84
|
+
`sqlide keys` prints every rebindable action with its default key. Override in
|
|
85
|
+
`~/.config/sqlide/keymap.toml`:
|
|
86
|
+
|
|
87
|
+
```toml
|
|
88
|
+
[keys]
|
|
89
|
+
"editor.run" = "f4"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
| Action | Keys |
|
|
93
|
+
|---|---|
|
|
94
|
+
| Run statement / selection | `F5`, `Ctrl+J`, `Ctrl+Enter`\* |
|
|
95
|
+
| Run all | `Shift+F5` |
|
|
96
|
+
| Cancel query | `Ctrl+F2` |
|
|
97
|
+
| Autocomplete | `Ctrl+Space` (opens by itself after a dot) |
|
|
98
|
+
| Format / comment | `F7` or `Ctrl+Alt+L` / `Ctrl+/` or `Alt+/` |
|
|
99
|
+
| New connection / console / close tab | `Ctrl+N` / `Ctrl+T` / `Alt+W` |
|
|
100
|
+
| Open / save file | `Ctrl+O` / `Ctrl+S` |
|
|
101
|
+
| History | `Alt+E` |
|
|
102
|
+
| Switch tab | `Alt+←` `Alt+→` |
|
|
103
|
+
| Focus connections / schema / editor / results | `Alt+C` / `Alt+D` / `Alt+Q` / `Alt+R` (or `F6` / `Shift+F6` to cycle) |
|
|
104
|
+
| Tx: toggle / commit / rollback | `F8` / `F9` / `F10` |
|
|
105
|
+
| Grid: sort / copy / copy as / filter / export | `s` / `Ctrl+C` / `y` / `/` / `e` |
|
|
106
|
+
| Command palette | `Ctrl+P` |
|
|
107
|
+
| Quit | `Ctrl+Q` |
|
|
108
|
+
|
|
109
|
+
\* Most terminals send `Ctrl+Enter` as plain `Enter`; it only works where the terminal
|
|
110
|
+
speaks the kitty keyboard protocol. `Alt+Enter` is not used on purpose: it toggles
|
|
111
|
+
full screen in Windows Terminal.
|
|
112
|
+
|
|
113
|
+
## Files
|
|
114
|
+
|
|
115
|
+
| What | Where (Linux and WSL; macOS uses the platform equivalents) |
|
|
116
|
+
|---|---|
|
|
117
|
+
| Settings, connections, custom drivers, keymap | `~/.config/sqlide/` |
|
|
118
|
+
| Downloaded drivers, history, consoles | `~/.local/share/sqlide/` |
|
|
119
|
+
|
|
120
|
+
Override with `SQLIDE_CONFIG_DIR` and `SQLIDE_DATA_DIR`.
|
|
121
|
+
|
|
122
|
+
## Notes
|
|
123
|
+
|
|
124
|
+
- Schema reads (tree, autocomplete) use a second connection, so they work while a query runs.
|
|
125
|
+
For in-memory databases (H2 `mem:`, SQLite `:memory:`, DuckDB) a second connection would see
|
|
126
|
+
a different database, so the query connection is shared and reads wait for a running query.
|
|
127
|
+
- Autocomplete reads cached metadata. DDL you run in the editor refreshes it; changes made by
|
|
128
|
+
other clients need `F5` in the schema tree. In manual-commit mode new objects appear after
|
|
129
|
+
Commit.
|
|
130
|
+
- MySQL `DELIMITER x` lines are understood (they are not sent to the server). On SQL Server,
|
|
131
|
+
`CREATE PROCEDURE/FUNCTION/TRIGGER` without `BEGIN..END` runs up to the next `GO`, as T-SQL does.
|
|
132
|
+
- `Alt+1`..`Alt+4` only work in terminals with the kitty keyboard protocol; most terminals
|
|
133
|
+
deliver them as Mac characters, so use the letter keys or `F6`.
|
|
134
|
+
- Tested on Linux and WSL against PostgreSQL, MySQL, MariaDB, ClickHouse, SQL Server, Oracle and
|
|
135
|
+
H2. macOS is covered by CI. On Windows, run it inside WSL (native Windows is not supported).
|
|
136
|
+
|
|
137
|
+
## Development
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
uv sync
|
|
141
|
+
uv run pytest # unit, H2 integration, Textual pilot tests
|
|
142
|
+
uv run pytest -m docker # also PostgreSQL in Docker
|
|
143
|
+
uv run ruff check && uv run pyright
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The code is split into blocks that depend only downwards: `ui → db, sql, grid, export,
|
|
147
|
+
history, config → jvm, drivers`. The core never imports the UI.
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
MIT
|
sqlide-0.1.0/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# sqlide
|
|
2
|
+
|
|
3
|
+
A DataGrip-style SQL IDE that runs in your terminal. Connect to any database over JDBC,
|
|
4
|
+
write SQL in an editor that outlines the statement about to run, and browse results in
|
|
5
|
+
sortable, copyable tables that export to CSV, XLSX and more.
|
|
6
|
+
|
|
7
|
+
Works on Linux, macOS and WSL.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Any JDBC database.** PostgreSQL and ClickHouse work out of the box; MySQL, MariaDB,
|
|
12
|
+
Oracle, SQL Server, H2, SQLite and DuckDB are one keypress away. Add your own driver from
|
|
13
|
+
jar files or Maven coordinates, as in DataGrip. Drivers are downloaded from Maven Central
|
|
14
|
+
(checksum verified) and each one gets its own class loader.
|
|
15
|
+
- **Framed statement.** The statement under the cursor is outlined in the gutter; `F5`
|
|
16
|
+
runs it, a selection runs the selection, `Shift+F5` runs the whole file. Dialect-aware
|
|
17
|
+
splitting: `$$` quotes, nested comments, Oracle `/`, SQL Server `GO`, procedural blocks.
|
|
18
|
+
- **Result grid.** Sort by several columns, filter, select a rectangle, view long values,
|
|
19
|
+
load more rows on demand. Copy as TSV, CSV, Markdown, JSON or `INSERT` statements.
|
|
20
|
+
- **Export.** CSV, TSV, JSON, JSON Lines, Markdown, SQL inserts, HTML, XLSX. Export the loaded
|
|
21
|
+
rows or re-run the query and stream the full result to disk, with progress and cancel.
|
|
22
|
+
- **Schema tree and autocomplete.** Lazy metadata, `SELECT *` on Enter, alias-aware column
|
|
23
|
+
completion, keywords and functions per dialect.
|
|
24
|
+
- **Consoles and files.** Tabs with their own connection; consoles autosave and come back
|
|
25
|
+
after a restart; open and save `.sql` files (`sqlide a.sql b.sql`).
|
|
26
|
+
- **History, formatting, transactions.** Searchable query history, SQL formatting,
|
|
27
|
+
Auto/Manual commit with Commit/Rollback.
|
|
28
|
+
- **Rebindable keys, themes, command palette** (`Ctrl+P`). No AI features.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
Requirements: Python 3.12+ and a Java 11+ runtime (`brew install openjdk`,
|
|
33
|
+
`apt install openjdk-21-jre`, ...). Check with `sqlide doctor`.
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
uv tool install sqlide # or: pipx install sqlide
|
|
37
|
+
sqlide
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Homebrew formula: see [packaging/homebrew](packaging/homebrew/README.md).
|
|
41
|
+
|
|
42
|
+
## Quick start
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
sqlide driver install postgres # or just connect: sqlide offers to download it
|
|
46
|
+
sqlide # Ctrl+N: new connection, Enter on it: connect
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Passwords are never written to disk. sqlide asks when you connect and keeps the password
|
|
50
|
+
in memory until you quit. A connection can instead read it from an environment variable
|
|
51
|
+
(`password_ref = "${env:PGPASSWORD}"`) or from a command such as a keychain CLI
|
|
52
|
+
(`password_cmd = "security find-generic-password -s mydb -w"`).
|
|
53
|
+
|
|
54
|
+
## Keys
|
|
55
|
+
|
|
56
|
+
`sqlide keys` prints every rebindable action with its default key. Override in
|
|
57
|
+
`~/.config/sqlide/keymap.toml`:
|
|
58
|
+
|
|
59
|
+
```toml
|
|
60
|
+
[keys]
|
|
61
|
+
"editor.run" = "f4"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
| Action | Keys |
|
|
65
|
+
|---|---|
|
|
66
|
+
| Run statement / selection | `F5`, `Ctrl+J`, `Ctrl+Enter`\* |
|
|
67
|
+
| Run all | `Shift+F5` |
|
|
68
|
+
| Cancel query | `Ctrl+F2` |
|
|
69
|
+
| Autocomplete | `Ctrl+Space` (opens by itself after a dot) |
|
|
70
|
+
| Format / comment | `F7` or `Ctrl+Alt+L` / `Ctrl+/` or `Alt+/` |
|
|
71
|
+
| New connection / console / close tab | `Ctrl+N` / `Ctrl+T` / `Alt+W` |
|
|
72
|
+
| Open / save file | `Ctrl+O` / `Ctrl+S` |
|
|
73
|
+
| History | `Alt+E` |
|
|
74
|
+
| Switch tab | `Alt+←` `Alt+→` |
|
|
75
|
+
| Focus connections / schema / editor / results | `Alt+C` / `Alt+D` / `Alt+Q` / `Alt+R` (or `F6` / `Shift+F6` to cycle) |
|
|
76
|
+
| Tx: toggle / commit / rollback | `F8` / `F9` / `F10` |
|
|
77
|
+
| Grid: sort / copy / copy as / filter / export | `s` / `Ctrl+C` / `y` / `/` / `e` |
|
|
78
|
+
| Command palette | `Ctrl+P` |
|
|
79
|
+
| Quit | `Ctrl+Q` |
|
|
80
|
+
|
|
81
|
+
\* Most terminals send `Ctrl+Enter` as plain `Enter`; it only works where the terminal
|
|
82
|
+
speaks the kitty keyboard protocol. `Alt+Enter` is not used on purpose: it toggles
|
|
83
|
+
full screen in Windows Terminal.
|
|
84
|
+
|
|
85
|
+
## Files
|
|
86
|
+
|
|
87
|
+
| What | Where (Linux and WSL; macOS uses the platform equivalents) |
|
|
88
|
+
|---|---|
|
|
89
|
+
| Settings, connections, custom drivers, keymap | `~/.config/sqlide/` |
|
|
90
|
+
| Downloaded drivers, history, consoles | `~/.local/share/sqlide/` |
|
|
91
|
+
|
|
92
|
+
Override with `SQLIDE_CONFIG_DIR` and `SQLIDE_DATA_DIR`.
|
|
93
|
+
|
|
94
|
+
## Notes
|
|
95
|
+
|
|
96
|
+
- Schema reads (tree, autocomplete) use a second connection, so they work while a query runs.
|
|
97
|
+
For in-memory databases (H2 `mem:`, SQLite `:memory:`, DuckDB) a second connection would see
|
|
98
|
+
a different database, so the query connection is shared and reads wait for a running query.
|
|
99
|
+
- Autocomplete reads cached metadata. DDL you run in the editor refreshes it; changes made by
|
|
100
|
+
other clients need `F5` in the schema tree. In manual-commit mode new objects appear after
|
|
101
|
+
Commit.
|
|
102
|
+
- MySQL `DELIMITER x` lines are understood (they are not sent to the server). On SQL Server,
|
|
103
|
+
`CREATE PROCEDURE/FUNCTION/TRIGGER` without `BEGIN..END` runs up to the next `GO`, as T-SQL does.
|
|
104
|
+
- `Alt+1`..`Alt+4` only work in terminals with the kitty keyboard protocol; most terminals
|
|
105
|
+
deliver them as Mac characters, so use the letter keys or `F6`.
|
|
106
|
+
- Tested on Linux and WSL against PostgreSQL, MySQL, MariaDB, ClickHouse, SQL Server, Oracle and
|
|
107
|
+
H2. macOS is covered by CI. On Windows, run it inside WSL (native Windows is not supported).
|
|
108
|
+
|
|
109
|
+
## Development
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
uv sync
|
|
113
|
+
uv run pytest # unit, H2 integration, Textual pilot tests
|
|
114
|
+
uv run pytest -m docker # also PostgreSQL in Docker
|
|
115
|
+
uv run ruff check && uv run pyright
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The code is split into blocks that depend only downwards: `ui → db, sql, grid, export,
|
|
119
|
+
history, config → jvm, drivers`. The core never imports the UI.
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Homebrew
|
|
2
|
+
|
|
3
|
+
`sqlide.rb` is a template for a tap (`brew tap XFaIT/sqlide`).
|
|
4
|
+
|
|
5
|
+
1. Release to PyPI (push a `v*` tag; `.github/workflows/release.yml` publishes).
|
|
6
|
+
2. Create the repository `homebrew-sqlide` and put the formula in `Formula/sqlide.rb`.
|
|
7
|
+
3. Set `url` and `sha256` from the PyPI sdist, then run
|
|
8
|
+
`brew update-python-resources sqlide` to add the dependency resources.
|
|
9
|
+
4. `brew install --build-from-source XFaIT/sqlide/sqlide && brew test sqlide`.
|
|
10
|
+
|
|
11
|
+
Users then run `brew install XFaIT/sqlide/sqlide`.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Homebrew formula template. Copy to your tap (homebrew-sqlide/Formula/sqlide.rb) after the
|
|
2
|
+
# first PyPI release, then fill url/sha256 and the resource blocks:
|
|
3
|
+
#
|
|
4
|
+
# brew update-python-resources sqlide
|
|
5
|
+
#
|
|
6
|
+
class Sqlide < Formula
|
|
7
|
+
include Language::Python::Virtualenv
|
|
8
|
+
|
|
9
|
+
desc "Terminal SQL IDE (DataGrip-like) over JDBC"
|
|
10
|
+
homepage "https://github.com/XFaIT/sqlide"
|
|
11
|
+
url "https://files.pythonhosted.org/packages/source/s/sqlide/sqlide-0.1.0.tar.gz"
|
|
12
|
+
sha256 "REPLACE_WITH_SDIST_SHA256"
|
|
13
|
+
license "MIT"
|
|
14
|
+
|
|
15
|
+
depends_on "openjdk"
|
|
16
|
+
depends_on "python@3.12"
|
|
17
|
+
|
|
18
|
+
# resource blocks (textual, jpype1, openpyxl, httpx, sqlglot, ...) are generated by
|
|
19
|
+
# `brew update-python-resources`
|
|
20
|
+
|
|
21
|
+
def install
|
|
22
|
+
virtualenv_install_with_resources
|
|
23
|
+
# keg-only openjdk is not on PATH: point sqlide at it
|
|
24
|
+
java_home = if OS.mac?
|
|
25
|
+
Formula["openjdk"].opt_libexec/"openjdk.jdk/Contents/Home"
|
|
26
|
+
else
|
|
27
|
+
Formula["openjdk"].opt_prefix
|
|
28
|
+
end
|
|
29
|
+
rm bin/"sqlide"
|
|
30
|
+
(bin/"sqlide").write_env_script libexec/"bin/sqlide", JAVA_HOME: java_home
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
test do
|
|
34
|
+
assert_match "sqlide", shell_output("#{bin}/sqlide --version")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "sqlide"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Terminal SQL IDE (DataGrip-like) over JDBC"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
authors = [{ name = "XFaIT" }]
|
|
8
|
+
keywords = ["sql", "jdbc", "tui", "terminal", "database", "datagrip"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Environment :: Console",
|
|
11
|
+
"Intended Audience :: Developers",
|
|
12
|
+
"License :: OSI Approved :: MIT License",
|
|
13
|
+
"Operating System :: POSIX :: Linux",
|
|
14
|
+
"Operating System :: MacOS",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Topic :: Database :: Front-Ends",
|
|
17
|
+
]
|
|
18
|
+
requires-python = ">=3.12"
|
|
19
|
+
dependencies = [
|
|
20
|
+
"textual[syntax]>=0.80",
|
|
21
|
+
"jpype1>=1.5",
|
|
22
|
+
"openpyxl>=3.1",
|
|
23
|
+
"platformdirs>=4",
|
|
24
|
+
"tomli-w>=1.0",
|
|
25
|
+
"httpx>=0.27",
|
|
26
|
+
"sqlglot>=25",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/XFaIT/sqlide"
|
|
31
|
+
Repository = "https://github.com/XFaIT/sqlide"
|
|
32
|
+
Issues = "https://github.com/XFaIT/sqlide/issues"
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
sqlide = "sqlide.cli:main"
|
|
36
|
+
|
|
37
|
+
[dependency-groups]
|
|
38
|
+
dev = [
|
|
39
|
+
"pytest>=8",
|
|
40
|
+
"pytest-asyncio>=0.23",
|
|
41
|
+
"pytest-textual-snapshot>=1",
|
|
42
|
+
"ruff>=0.6",
|
|
43
|
+
"pyright>=1.1.380",
|
|
44
|
+
"textual-dev>=1.8.0",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[build-system]
|
|
48
|
+
requires = ["hatchling"]
|
|
49
|
+
build-backend = "hatchling.build"
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.wheel]
|
|
52
|
+
packages = ["src/sqlide"]
|
|
53
|
+
|
|
54
|
+
[tool.ruff]
|
|
55
|
+
line-length = 100
|
|
56
|
+
src = ["src", "tests"]
|
|
57
|
+
|
|
58
|
+
[tool.ruff.lint]
|
|
59
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
60
|
+
|
|
61
|
+
[tool.pytest.ini_options]
|
|
62
|
+
testpaths = ["tests"]
|
|
63
|
+
addopts = "-m 'not docker'"
|
|
64
|
+
asyncio_mode = "auto"
|
|
65
|
+
markers = ["docker: needs Docker (postgres, clickhouse)"]
|
|
66
|
+
|
|
67
|
+
[tool.pyright]
|
|
68
|
+
include = ["src"]
|
|
69
|
+
pythonVersion = "3.12"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Textual application: only wires the workspace to the main screen."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from textual.app import App
|
|
8
|
+
from textual.binding import Binding
|
|
9
|
+
|
|
10
|
+
from sqlide.config.keymap import load_keymap
|
|
11
|
+
from sqlide.config.settings import save_settings
|
|
12
|
+
from sqlide.ui.commands import commands
|
|
13
|
+
from sqlide.ui.screens.main import MainScreen
|
|
14
|
+
from sqlide.workspace import Workspace
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SqlideApp(App):
|
|
18
|
+
CSS_PATH = str(Path(__file__).parent / "ui" / "app.tcss")
|
|
19
|
+
BINDINGS = [Binding("ctrl+q", "quit", "Quit", priority=True, id="app.quit")]
|
|
20
|
+
|
|
21
|
+
def __init__(self, workspace: Workspace | None = None, files: list[Path] | None = None) -> None:
|
|
22
|
+
super().__init__()
|
|
23
|
+
self.workspace = workspace or Workspace()
|
|
24
|
+
self._files = files or []
|
|
25
|
+
|
|
26
|
+
def on_mount(self) -> None:
|
|
27
|
+
self.set_keymap(load_keymap()) # user overrides from keymap.toml
|
|
28
|
+
if self.workspace.settings.theme in self.available_themes:
|
|
29
|
+
self.theme = self.workspace.settings.theme
|
|
30
|
+
self.theme_changed_signal.subscribe(self, self._remember_theme)
|
|
31
|
+
self.main = MainScreen(self.workspace, self._files)
|
|
32
|
+
self.push_screen(self.main)
|
|
33
|
+
|
|
34
|
+
def _remember_theme(self, theme) -> None:
|
|
35
|
+
settings = self.workspace.settings
|
|
36
|
+
if theme.name != settings.theme:
|
|
37
|
+
settings.theme = theme.name
|
|
38
|
+
save_settings(settings)
|
|
39
|
+
|
|
40
|
+
def get_system_commands(self, screen):
|
|
41
|
+
yield from super().get_system_commands(screen)
|
|
42
|
+
yield from commands(self)
|
|
43
|
+
|
|
44
|
+
async def action_quit(self) -> None:
|
|
45
|
+
await self.main.tabs.shutdown() # save consoles, cancel queries, close connections
|
|
46
|
+
self.exit()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Command line entry point. Thin: dispatch only, no logic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from sqlide import __version__
|
|
10
|
+
from sqlide.drivers import cli as driver_cli
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
14
|
+
p = argparse.ArgumentParser(prog="sqlide", description="Terminal SQL IDE over JDBC")
|
|
15
|
+
p.add_argument("--version", action="version", version=f"sqlide {__version__}")
|
|
16
|
+
sub = p.add_subparsers(dest="command")
|
|
17
|
+
d = sub.add_parser("doctor", help="check environment (JVM, drivers, config dirs)")
|
|
18
|
+
d.set_defaults(handler=lambda _: _doctor())
|
|
19
|
+
k = sub.add_parser("keys", help="list rebindable keys (override in keymap.toml)")
|
|
20
|
+
k.set_defaults(handler=lambda _: _keys())
|
|
21
|
+
driver_cli.register(sub)
|
|
22
|
+
return p
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _doctor() -> int:
|
|
26
|
+
from sqlide import doctor
|
|
27
|
+
|
|
28
|
+
return doctor.run()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _keys() -> int:
|
|
32
|
+
from sqlide.config import paths
|
|
33
|
+
from sqlide.ui.keymap import catalogue
|
|
34
|
+
|
|
35
|
+
print(f'Override in {paths.keymap_file()} ([keys] id = "key,key")\n')
|
|
36
|
+
infos = catalogue()
|
|
37
|
+
width = max(len(i.id) for i in infos)
|
|
38
|
+
for i in infos:
|
|
39
|
+
print(f"{i.id:<{width}} {i.keys:<40} {i.description}")
|
|
40
|
+
return 0
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
COMMANDS = {"doctor", "driver", "keys"}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def main(argv: list[str] | None = None) -> int:
|
|
47
|
+
argv = sys.argv[1:] if argv is None else argv
|
|
48
|
+
if argv and argv[0] not in COMMANDS and not argv[0].startswith("-"):
|
|
49
|
+
return _run_tui([Path(a) for a in argv]) # `sqlide query.sql other.sql`
|
|
50
|
+
args = build_parser().parse_args(argv)
|
|
51
|
+
if getattr(args, "handler", None):
|
|
52
|
+
return args.handler(args)
|
|
53
|
+
return _run_tui([])
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _run_tui(files: list[Path]) -> int:
|
|
57
|
+
from sqlide.app import SqlideApp
|
|
58
|
+
|
|
59
|
+
SqlideApp(files=files).run()
|
|
60
|
+
return 0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
sys.exit(main())
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""System clipboard across macOS, Linux (X11/Wayland), WSL; OSC52 as the last resort."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
Backend = tuple[str, list[str], Callable[[str], bytes]]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _utf8(s: str) -> bytes:
|
|
15
|
+
return s.encode("utf-8")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _utf16(s: str) -> bytes: # clip.exe reads UTF-16LE when a BOM is present
|
|
19
|
+
return b"\xff\xfe" + s.encode("utf-16-le")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def is_wsl() -> bool:
|
|
23
|
+
try:
|
|
24
|
+
return "microsoft" in Path("/proc/version").read_text().lower()
|
|
25
|
+
except OSError:
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def backends(platform: str | None = None, wsl: bool | None = None) -> list[Backend]:
|
|
30
|
+
"""Installed native clipboard tools in preference order."""
|
|
31
|
+
import sys
|
|
32
|
+
|
|
33
|
+
platform = platform or sys.platform
|
|
34
|
+
wsl = is_wsl() if wsl is None else wsl
|
|
35
|
+
candidates: list[Backend] = []
|
|
36
|
+
if platform == "darwin":
|
|
37
|
+
candidates.append(("pbcopy", ["pbcopy"], _utf8))
|
|
38
|
+
elif platform == "win32":
|
|
39
|
+
candidates.append(("clip", ["clip"], _utf16))
|
|
40
|
+
else:
|
|
41
|
+
if os.environ.get("WAYLAND_DISPLAY"):
|
|
42
|
+
candidates.append(("wl-copy", ["wl-copy"], _utf8))
|
|
43
|
+
if os.environ.get("DISPLAY"):
|
|
44
|
+
candidates.append(("xclip", ["xclip", "-selection", "clipboard", "-i"], _utf8))
|
|
45
|
+
candidates.append(("xsel", ["xsel", "--clipboard", "--input"], _utf8))
|
|
46
|
+
if wsl:
|
|
47
|
+
candidates.append(("clip.exe", ["clip.exe"], _utf16))
|
|
48
|
+
return [c for c in candidates if shutil.which(c[1][0])]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def copy_native(text: str) -> str | None:
|
|
52
|
+
"""Copy via the first working tool. Returns its name, or None if none worked."""
|
|
53
|
+
for name, cmd, encode in backends():
|
|
54
|
+
try:
|
|
55
|
+
subprocess.run(cmd, input=encode(text), check=True, timeout=5, capture_output=True)
|
|
56
|
+
return name
|
|
57
|
+
except (OSError, subprocess.SubprocessError):
|
|
58
|
+
continue
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def available() -> str:
|
|
63
|
+
"""Human-readable description for `sqlide doctor`."""
|
|
64
|
+
found = backends()
|
|
65
|
+
return found[0][0] if found else "none (falls back to OSC52 terminal escape)"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Configuration block: paths, settings, saved connections, password resolution."""
|