execsql2 2.5.0__py3-none-any.whl → 2.7.1__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.
- execsql/exporters/__init__.py +3 -3
- execsql/exporters/delimited.py +2 -2
- execsql/exporters/markdown.py +126 -0
- execsql/exporters/xlsx.py +317 -0
- execsql/exporters/yaml.py +87 -0
- execsql/gui/tui.py +132 -0
- execsql/metacommands/__init__.py +203 -182
- execsql/metacommands/dispatch.py +11 -0
- execsql/metacommands/io.py +2 -0
- execsql/metacommands/io_export.py +75 -0
- execsql/state.py +261 -200
- {execsql2-2.5.0.dist-info → execsql2-2.7.1.dist-info}/METADATA +5 -2
- {execsql2-2.5.0.dist-info → execsql2-2.7.1.dist-info}/RECORD +32 -29
- {execsql2-2.5.0.data → execsql2-2.7.1.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.5.0.data → execsql2-2.7.1.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.5.0.data → execsql2-2.7.1.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.5.0.data → execsql2-2.7.1.data}/data/execsql2_extras/execsql.conf +0 -0
- {execsql2-2.5.0.data → execsql2-2.7.1.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.5.0.data → execsql2-2.7.1.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.5.0.data → execsql2-2.7.1.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.5.0.data → execsql2-2.7.1.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.5.0.data → execsql2-2.7.1.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.5.0.data → execsql2-2.7.1.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.5.0.data → execsql2-2.7.1.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.5.0.data → execsql2-2.7.1.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.5.0.data → execsql2-2.7.1.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.5.0.data → execsql2-2.7.1.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.5.0.data → execsql2-2.7.1.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.5.0.dist-info → execsql2-2.7.1.dist-info}/WHEEL +0 -0
- {execsql2-2.5.0.dist-info → execsql2-2.7.1.dist-info}/entry_points.txt +0 -0
- {execsql2-2.5.0.dist-info → execsql2-2.7.1.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.5.0.dist-info → execsql2-2.7.1.dist-info}/licenses/NOTICE +0 -0
execsql/gui/tui.py
CHANGED
|
@@ -101,6 +101,10 @@ def _button_row(button_list: list) -> list[Button]:
|
|
|
101
101
|
class _BaseDialog(ModalScreen):
|
|
102
102
|
"""Common infrastructure for all execsql dialog screens."""
|
|
103
103
|
|
|
104
|
+
BINDINGS = [
|
|
105
|
+
Binding("escape", "cancel", "Cancel", show=True),
|
|
106
|
+
]
|
|
107
|
+
|
|
104
108
|
DEFAULT_CSS = """
|
|
105
109
|
_BaseDialog {
|
|
106
110
|
align: center middle;
|
|
@@ -151,6 +155,11 @@ class _BaseDialog(ModalScreen):
|
|
|
151
155
|
def result(self) -> dict:
|
|
152
156
|
return self._result
|
|
153
157
|
|
|
158
|
+
def action_cancel(self) -> None:
|
|
159
|
+
"""Dismiss the dialog as cancelled (triggered by Escape key)."""
|
|
160
|
+
self._result = {"button": None, "cancelled": True}
|
|
161
|
+
self.dismiss(self._result)
|
|
162
|
+
|
|
154
163
|
@on(Button.Pressed, "#btn_cancel_exit")
|
|
155
164
|
def _on_cancel_exit(self, event: Button.Pressed) -> None:
|
|
156
165
|
"""Universal Cancel handler — dismisses with cancelled=True so the sync queue can exit."""
|
|
@@ -167,6 +176,11 @@ class _BaseDialog(ModalScreen):
|
|
|
167
176
|
class MsgScreen(_BaseDialog):
|
|
168
177
|
"""Simple message dialog with Continue and Cancel buttons."""
|
|
169
178
|
|
|
179
|
+
BINDINGS = [
|
|
180
|
+
*_BaseDialog.BINDINGS,
|
|
181
|
+
Binding("enter", "submit", "Continue", show=True),
|
|
182
|
+
]
|
|
183
|
+
|
|
170
184
|
def compose(self) -> ComposeResult:
|
|
171
185
|
title = self.args.get("title", "Message")
|
|
172
186
|
message = self.args.get("message", "")
|
|
@@ -176,6 +190,12 @@ class MsgScreen(_BaseDialog):
|
|
|
176
190
|
with Horizontal(id="buttons"):
|
|
177
191
|
yield Button("Cancel", id="btn_cancel_exit", variant="warning")
|
|
178
192
|
yield Button("Continue", id="btn_close", variant="primary")
|
|
193
|
+
yield Footer()
|
|
194
|
+
|
|
195
|
+
def action_submit(self) -> None:
|
|
196
|
+
"""Continue the dialog (triggered by Enter key)."""
|
|
197
|
+
self._result = {"button": 1}
|
|
198
|
+
self.dismiss(self._result)
|
|
179
199
|
|
|
180
200
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
181
201
|
if event.button.id == "btn_close":
|
|
@@ -191,6 +211,11 @@ class MsgScreen(_BaseDialog):
|
|
|
191
211
|
class PauseScreen(_BaseDialog):
|
|
192
212
|
"""Pause dialog with optional countdown and Continue/Cancel buttons."""
|
|
193
213
|
|
|
214
|
+
BINDINGS = [
|
|
215
|
+
*_BaseDialog.BINDINGS,
|
|
216
|
+
Binding("enter", "submit", "Continue", show=True),
|
|
217
|
+
]
|
|
218
|
+
|
|
194
219
|
def compose(self) -> ComposeResult:
|
|
195
220
|
title = self.args.get("title", "Pause")
|
|
196
221
|
message = self.args.get("message", "")
|
|
@@ -200,6 +225,7 @@ class PauseScreen(_BaseDialog):
|
|
|
200
225
|
with Horizontal(id="buttons"):
|
|
201
226
|
yield Button("Cancel", id="btn_cancel_exit", variant="warning")
|
|
202
227
|
yield Button("Continue", id="btn_continue", variant="primary")
|
|
228
|
+
yield Footer()
|
|
203
229
|
|
|
204
230
|
def on_mount(self) -> None:
|
|
205
231
|
countdown = self.args.get("countdown")
|
|
@@ -210,6 +236,11 @@ class PauseScreen(_BaseDialog):
|
|
|
210
236
|
self._result = {"quit": False}
|
|
211
237
|
self.dismiss(self._result)
|
|
212
238
|
|
|
239
|
+
def action_submit(self) -> None:
|
|
240
|
+
"""Continue the dialog (triggered by Enter key)."""
|
|
241
|
+
self._result = {"quit": False}
|
|
242
|
+
self.dismiss(self._result)
|
|
243
|
+
|
|
213
244
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
214
245
|
if event.button.id == "btn_continue":
|
|
215
246
|
self._result = {"quit": False}
|
|
@@ -224,6 +255,11 @@ class PauseScreen(_BaseDialog):
|
|
|
224
255
|
class DisplayScreen(_BaseDialog):
|
|
225
256
|
"""Data display dialog: title, message, optional table, optional text entry, buttons."""
|
|
226
257
|
|
|
258
|
+
BINDINGS = [
|
|
259
|
+
*_BaseDialog.BINDINGS,
|
|
260
|
+
Binding("enter", "submit", "Submit", show=True),
|
|
261
|
+
]
|
|
262
|
+
|
|
227
263
|
def compose(self) -> ComposeResult:
|
|
228
264
|
title = self.args.get("title", "")
|
|
229
265
|
message = self.args.get("message", "")
|
|
@@ -251,6 +287,23 @@ class DisplayScreen(_BaseDialog):
|
|
|
251
287
|
)
|
|
252
288
|
with Horizontal(id="buttons"):
|
|
253
289
|
yield from _button_row(button_list)
|
|
290
|
+
yield Footer()
|
|
291
|
+
|
|
292
|
+
def action_submit(self) -> None:
|
|
293
|
+
"""Submit the first primary button value (triggered by Enter key)."""
|
|
294
|
+
button_list = self.args.get("button_list", [("Continue", 1)])
|
|
295
|
+
# Find the first non-cancel button value.
|
|
296
|
+
value = None
|
|
297
|
+
for btn in button_list:
|
|
298
|
+
if btn[1]:
|
|
299
|
+
value = btn[1]
|
|
300
|
+
break
|
|
301
|
+
if value is None:
|
|
302
|
+
return
|
|
303
|
+
text_input = self.query_one("#text_input", Input) if self.args.get("textentry") else None
|
|
304
|
+
return_value = text_input.value if text_input else None
|
|
305
|
+
self._result = {"button": value, "return_value": return_value}
|
|
306
|
+
self.dismiss(self._result)
|
|
254
307
|
|
|
255
308
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
256
309
|
btn_id = event.button.id
|
|
@@ -272,6 +325,11 @@ class DisplayScreen(_BaseDialog):
|
|
|
272
325
|
class EntryFormScreen(_BaseDialog):
|
|
273
326
|
"""Multi-field entry form dialog."""
|
|
274
327
|
|
|
328
|
+
BINDINGS = [
|
|
329
|
+
*_BaseDialog.BINDINGS,
|
|
330
|
+
Binding("enter", "submit", "OK", show=True),
|
|
331
|
+
]
|
|
332
|
+
|
|
275
333
|
DEFAULT_CSS = (
|
|
276
334
|
_BaseDialog.DEFAULT_CSS
|
|
277
335
|
+ """
|
|
@@ -337,9 +395,26 @@ class EntryFormScreen(_BaseDialog):
|
|
|
337
395
|
with Horizontal(id="buttons"):
|
|
338
396
|
yield Button("Cancel", id="btn_cancel_exit", variant="warning")
|
|
339
397
|
yield Button("OK", id="btn_ok", variant="primary")
|
|
398
|
+
yield Footer()
|
|
340
399
|
|
|
341
400
|
self._specs = specs
|
|
342
401
|
|
|
402
|
+
def action_submit(self) -> None:
|
|
403
|
+
"""Submit the form (triggered by Enter key)."""
|
|
404
|
+
for spec in self._specs:
|
|
405
|
+
widget = self._inputs.get(spec.varname)
|
|
406
|
+
if widget is None:
|
|
407
|
+
continue
|
|
408
|
+
etype = (spec.entry_type or "text").lower()
|
|
409
|
+
if etype == "checkbox":
|
|
410
|
+
spec.value = "True" if widget.value else "False"
|
|
411
|
+
elif etype in ("dropdown", "select"):
|
|
412
|
+
spec.value = str(widget.value) if widget.value is not None else ""
|
|
413
|
+
else:
|
|
414
|
+
spec.value = widget.value
|
|
415
|
+
self._result = {"button": 1, "return_value": self._specs}
|
|
416
|
+
self.dismiss(self._result)
|
|
417
|
+
|
|
343
418
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
344
419
|
if event.button.id == "btn_ok":
|
|
345
420
|
for spec in self._specs:
|
|
@@ -365,6 +440,11 @@ class EntryFormScreen(_BaseDialog):
|
|
|
365
440
|
class CompareScreen(_BaseDialog):
|
|
366
441
|
"""Side-by-side table comparison dialog."""
|
|
367
442
|
|
|
443
|
+
BINDINGS = [
|
|
444
|
+
*_BaseDialog.BINDINGS,
|
|
445
|
+
Binding("enter", "submit", "Continue", show=True),
|
|
446
|
+
]
|
|
447
|
+
|
|
368
448
|
DEFAULT_CSS = (
|
|
369
449
|
_BaseDialog.DEFAULT_CSS
|
|
370
450
|
+ """
|
|
@@ -412,6 +492,7 @@ class CompareScreen(_BaseDialog):
|
|
|
412
492
|
yield _make_table_widget("table2", headers2, rows2)
|
|
413
493
|
with Horizontal(id="buttons"):
|
|
414
494
|
yield from _button_row(button_list)
|
|
495
|
+
yield Footer()
|
|
415
496
|
|
|
416
497
|
def on_mount(self) -> None:
|
|
417
498
|
keylist = [str(k) for k in self.args.get("keylist", [])]
|
|
@@ -461,6 +542,15 @@ class CompareScreen(_BaseDialog):
|
|
|
461
542
|
finally:
|
|
462
543
|
self._syncing = False
|
|
463
544
|
|
|
545
|
+
def action_submit(self) -> None:
|
|
546
|
+
"""Submit the first primary button value (triggered by Enter key)."""
|
|
547
|
+
button_list = self.args.get("button_list", [("Continue", 1)])
|
|
548
|
+
for btn in button_list:
|
|
549
|
+
if btn[1]:
|
|
550
|
+
self._result = {"button": btn[1]}
|
|
551
|
+
self.dismiss(self._result)
|
|
552
|
+
return
|
|
553
|
+
|
|
464
554
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
465
555
|
btn_id = event.button.id
|
|
466
556
|
if btn_id and btn_id.startswith("btn_") and btn_id[4:].isdigit():
|
|
@@ -478,6 +568,11 @@ class CompareScreen(_BaseDialog):
|
|
|
478
568
|
class SelectRowsScreen(_BaseDialog):
|
|
479
569
|
"""Row selection dialog: pick rows from source table and add to target."""
|
|
480
570
|
|
|
571
|
+
BINDINGS = [
|
|
572
|
+
*_BaseDialog.BINDINGS,
|
|
573
|
+
Binding("enter", "submit", "Continue", show=True),
|
|
574
|
+
]
|
|
575
|
+
|
|
481
576
|
DEFAULT_CSS = (
|
|
482
577
|
_BaseDialog.DEFAULT_CSS
|
|
483
578
|
+ """
|
|
@@ -516,6 +611,7 @@ class SelectRowsScreen(_BaseDialog):
|
|
|
516
611
|
yield _make_table_widget("dest_table", headers2, rows2)
|
|
517
612
|
with Horizontal(id="buttons"):
|
|
518
613
|
yield from _button_row(button_list)
|
|
614
|
+
yield Footer()
|
|
519
615
|
|
|
520
616
|
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
|
521
617
|
if event.data_table.id == "source_table":
|
|
@@ -525,6 +621,15 @@ class SelectRowsScreen(_BaseDialog):
|
|
|
525
621
|
values = [src.get_cell(row_key, col) for col in src.columns]
|
|
526
622
|
dest.add_row(*values)
|
|
527
623
|
|
|
624
|
+
def action_submit(self) -> None:
|
|
625
|
+
"""Submit the first primary button value (triggered by Enter key)."""
|
|
626
|
+
button_list = self.args.get("button_list", [("Continue", 1)])
|
|
627
|
+
for btn in button_list:
|
|
628
|
+
if btn[1]:
|
|
629
|
+
self._result = {"button": btn[1]}
|
|
630
|
+
self.dismiss(self._result)
|
|
631
|
+
return
|
|
632
|
+
|
|
528
633
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
529
634
|
btn_id = event.button.id
|
|
530
635
|
if btn_id and btn_id.startswith("btn_") and btn_id[4:].isdigit():
|
|
@@ -588,6 +693,10 @@ class SelectSubScreen(_BaseDialog):
|
|
|
588
693
|
class ActionScreen(_BaseDialog):
|
|
589
694
|
"""Action button grid dialog."""
|
|
590
695
|
|
|
696
|
+
BINDINGS = [
|
|
697
|
+
*_BaseDialog.BINDINGS,
|
|
698
|
+
]
|
|
699
|
+
|
|
591
700
|
def compose(self) -> ComposeResult:
|
|
592
701
|
title = self.args.get("title", "Actions")
|
|
593
702
|
message = self.args.get("message", "")
|
|
@@ -609,6 +718,7 @@ class ActionScreen(_BaseDialog):
|
|
|
609
718
|
yield Button(f"{spec.label} — {spec.prompt}", id=f"action_{i}", variant="primary")
|
|
610
719
|
if include_continue:
|
|
611
720
|
yield Button("Continue", id="action_continue", variant="default")
|
|
721
|
+
yield Footer()
|
|
612
722
|
|
|
613
723
|
self._button_specs = button_specs
|
|
614
724
|
|
|
@@ -1045,6 +1155,7 @@ class ConsoleApp(App):
|
|
|
1045
1155
|
self._script_thread: threading.Thread | None = None
|
|
1046
1156
|
self._script_exception: BaseException | None = None
|
|
1047
1157
|
self._screen_map: dict | None = None
|
|
1158
|
+
self._console_lines: list[str] = []
|
|
1048
1159
|
|
|
1049
1160
|
def compose(self) -> ComposeResult:
|
|
1050
1161
|
yield Header(show_clock=False)
|
|
@@ -1118,11 +1229,19 @@ class ConsoleApp(App):
|
|
|
1118
1229
|
self.call_from_thread(self._append_console, text)
|
|
1119
1230
|
|
|
1120
1231
|
def _append_console(self, text: str) -> None:
|
|
1232
|
+
self._console_lines.append(text)
|
|
1121
1233
|
try:
|
|
1122
1234
|
self.query_one("#console_log", RichLog).write(text)
|
|
1123
1235
|
except Exception:
|
|
1124
1236
|
pass # Widget may not be mounted yet.
|
|
1125
1237
|
|
|
1238
|
+
def save(self, outfile: str, append: bool = False) -> None:
|
|
1239
|
+
"""Save the console text contents to *outfile*."""
|
|
1240
|
+
mode = "a" if append else "w"
|
|
1241
|
+
with open(outfile, mode, encoding="utf-8") as fh:
|
|
1242
|
+
for line in self._console_lines:
|
|
1243
|
+
fh.write(line if line.endswith("\n") else line + "\n")
|
|
1244
|
+
|
|
1126
1245
|
def set_status(self, msg: str) -> None:
|
|
1127
1246
|
"""Thread-safe status bar update."""
|
|
1128
1247
|
self.call_from_thread(self._update_status, msg)
|
|
@@ -1199,6 +1318,19 @@ class TextualBackend(GuiBackend):
|
|
|
1199
1318
|
pct = (num / total * 100.0) if total else num
|
|
1200
1319
|
self._console_app.set_progress(pct)
|
|
1201
1320
|
|
|
1321
|
+
def console_save(self, outfile: str, append: bool = False) -> None:
|
|
1322
|
+
"""Save console text contents to *outfile*."""
|
|
1323
|
+
if self._console_app is not None:
|
|
1324
|
+
self._console_app.save(outfile, append)
|
|
1325
|
+
|
|
1326
|
+
def console_hide(self) -> None:
|
|
1327
|
+
"""Hide the console (minimize) — Textual has no window-level hide,
|
|
1328
|
+
so this is a no-op in a terminal environment."""
|
|
1329
|
+
|
|
1330
|
+
def console_show(self) -> None:
|
|
1331
|
+
"""Show the console — Textual has no window-level show,
|
|
1332
|
+
so this is a no-op in a terminal environment."""
|
|
1333
|
+
|
|
1202
1334
|
def console_wait_user(self, message: str = "") -> None:
|
|
1203
1335
|
# ConsoleApp exits when the script is done; nothing extra needed here.
|
|
1204
1336
|
pass
|