execsql2 2.13.2__py3-none-any.whl → 2.14.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.
Files changed (29) hide show
  1. execsql/gui/base.py +52 -1
  2. execsql/gui/console.py +86 -9
  3. execsql/gui/desktop.py +261 -39
  4. execsql/gui/tui.py +325 -51
  5. execsql/metacommands/connect.py +5 -1
  6. execsql/metacommands/dispatch.py +49 -6
  7. execsql/metacommands/io_export.py +2 -2
  8. execsql/metacommands/prompt.py +6 -11
  9. {execsql2-2.13.2.dist-info → execsql2-2.14.1.dist-info}/METADATA +1 -1
  10. {execsql2-2.13.2.dist-info → execsql2-2.14.1.dist-info}/RECORD +29 -29
  11. {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/README.md +0 -0
  12. {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  13. {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  14. {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/execsql.conf +0 -0
  15. {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/make_config_db.sql +0 -0
  16. {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/md_compare.sql +0 -0
  17. {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/md_glossary.sql +0 -0
  18. {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/md_upsert.sql +0 -0
  19. {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/pg_compare.sql +0 -0
  20. {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  21. {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  22. {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/script_template.sql +0 -0
  23. {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/ss_compare.sql +0 -0
  24. {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  25. {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  26. {execsql2-2.13.2.dist-info → execsql2-2.14.1.dist-info}/WHEEL +0 -0
  27. {execsql2-2.13.2.dist-info → execsql2-2.14.1.dist-info}/entry_points.txt +0 -0
  28. {execsql2-2.13.2.dist-info → execsql2-2.14.1.dist-info}/licenses/LICENSE.txt +0 -0
  29. {execsql2-2.13.2.dist-info → execsql2-2.14.1.dist-info}/licenses/NOTICE +0 -0
execsql/gui/desktop.py CHANGED
@@ -50,6 +50,27 @@ def _center_window(win: tk.Tk | tk.Toplevel, width: int = 600, height: int = 400
50
50
  win.geometry(f"{width}x{height}+{x}+{y}")
51
51
 
52
52
 
53
+ from execsql.gui.base import compare_stats as _compare_stats
54
+
55
+
56
+ def _add_help_button(frame: tk.Frame, url: str | None) -> None:
57
+ """Add a Help button to the top-right of *frame* that opens *url* in the system browser.
58
+
59
+ Uses ``place()`` so the button overlays the top-right corner without
60
+ consuming vertical space in the pack/grid layout.
61
+ """
62
+ if url:
63
+ import webbrowser
64
+
65
+ btn = ttk.Button(frame, text="Help", command=lambda: webbrowser.open(url))
66
+ btn.place(relx=1.0, x=-4, y=4, anchor="ne")
67
+
68
+
69
+ def _row_count_text(n: int) -> str:
70
+ """Return a human-readable row count string, e.g. '3 rows' or '1 row'."""
71
+ return f"{n:,} row{'s' if n != 1 else ''}"
72
+
73
+
53
74
  def _populate_treeview(tree: ttk.Treeview, headers: list, rows: list) -> None:
54
75
  """Fill a ttk.Treeview with column headers and data rows."""
55
76
  tree["columns"] = [str(h) for h in headers]
@@ -91,7 +112,10 @@ class MsgDialog:
91
112
  frame = ttk.Frame(win, padding=12)
92
113
  frame.pack(fill=tk.BOTH, expand=True)
93
114
 
94
- ttk.Label(frame, text=args.get("message", ""), wraplength=480).pack(pady=(0, 12))
115
+ ttk.Label(frame, text=args.get("message", ""), wraplength=480, anchor="w", justify="left").pack(
116
+ anchor="w",
117
+ pady=(0, 12),
118
+ )
95
119
 
96
120
  headers = args.get("column_headers")
97
121
  rows = args.get("rowset")
@@ -102,9 +126,10 @@ class MsgDialog:
102
126
  tree.configure(yscrollcommand=vsb.set)
103
127
  tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
104
128
  vsb.pack(side=tk.RIGHT, fill=tk.Y)
129
+ ttk.Label(frame, text=_row_count_text(len(rows))).pack(anchor="w")
105
130
 
106
131
  btn_frame = ttk.Frame(frame)
107
- btn_frame.pack(pady=8)
132
+ btn_frame.pack(pady=8, anchor="e")
108
133
  ttk.Button(btn_frame, text="Close", command=lambda: self._close(win, 1)).pack()
109
134
  win.bind("<Return>", lambda e: self._close(win, 1))
110
135
  win.bind("<Escape>", lambda e: self._close(win, 1))
@@ -133,7 +158,10 @@ class PauseDialog:
133
158
  frame = ttk.Frame(win, padding=12)
134
159
  frame.pack(fill=tk.BOTH, expand=True)
135
160
 
136
- ttk.Label(frame, text=args.get("message", ""), wraplength=480).pack(pady=(0, 12))
161
+ ttk.Label(frame, text=args.get("message", ""), wraplength=480, anchor="w", justify="left").pack(
162
+ anchor="w",
163
+ pady=(0, 12),
164
+ )
137
165
 
138
166
  countdown = args.get("countdown")
139
167
  self._remaining = countdown
@@ -145,7 +173,7 @@ class PauseDialog:
145
173
  progress.pack(fill=tk.X, pady=4)
146
174
 
147
175
  btn_frame = ttk.Frame(frame)
148
- btn_frame.pack(pady=8)
176
+ btn_frame.pack(pady=8, anchor="e")
149
177
  ttk.Button(btn_frame, text="Continue", command=lambda: self._close(win, False)).pack(side=tk.LEFT, padx=4)
150
178
  ttk.Button(btn_frame, text="Cancel", command=lambda: self._close(win, True)).pack(side=tk.LEFT, padx=4)
151
179
  win.bind("<Return>", lambda e: self._close(win, False))
@@ -192,10 +220,14 @@ class DisplayDialog:
192
220
 
193
221
  frame = ttk.Frame(win, padding=12)
194
222
  frame.pack(fill=tk.BOTH, expand=True)
223
+ _add_help_button(frame, args.get("help_url"))
195
224
 
196
225
  message = args.get("message", "")
197
226
  if message:
198
- ttk.Label(frame, text=message, wraplength=580).pack(pady=(0, 8))
227
+ ttk.Label(frame, text=message, wraplength=580, anchor="w", justify="left").pack(
228
+ anchor="w",
229
+ pady=(0, 8),
230
+ )
199
231
 
200
232
  headers = args.get("column_headers")
201
233
  rows = args.get("rowset")
@@ -212,6 +244,7 @@ class DisplayDialog:
212
244
  hsb.grid(row=1, column=0, sticky="ew")
213
245
  table_frame.rowconfigure(0, weight=1)
214
246
  table_frame.columnconfigure(0, weight=1)
247
+ ttk.Label(frame, text=_row_count_text(len(rows))).pack(anchor="w")
215
248
 
216
249
  self._text_var = None
217
250
  textentry = args.get("textentry", False)
@@ -229,7 +262,7 @@ class DisplayDialog:
229
262
  no_cancel = args.get("no_cancel", False)
230
263
 
231
264
  btn_frame = ttk.Frame(frame)
232
- btn_frame.pack(pady=8)
265
+ btn_frame.pack(pady=8, anchor="e")
233
266
  _add_buttons(btn_frame, button_list, lambda v: self._close(win, v))
234
267
  if not no_cancel:
235
268
  win.bind("<Escape>", lambda e: self._close(win, None))
@@ -262,17 +295,22 @@ class EntryFormDialog:
262
295
 
263
296
  main_frame = ttk.Frame(win, padding=12)
264
297
  main_frame.pack(fill=tk.BOTH, expand=True)
298
+ _add_help_button(main_frame, args.get("help_url"))
265
299
 
266
300
  message = args.get("message", "")
267
301
  if message:
268
- ttk.Label(main_frame, text=message, wraplength=580).pack(pady=(0, 8))
302
+ ttk.Label(main_frame, text=message, wraplength=580, anchor="w", justify="left").pack(
303
+ anchor="w",
304
+ pady=(0, 8),
305
+ )
269
306
 
270
307
  headers = args.get("column_headers")
271
308
  rows = args.get("rowset")
272
309
  if headers and rows:
273
310
  tree = ttk.Treeview(main_frame, height=min(6, len(rows)))
274
311
  _populate_treeview(tree, headers, rows)
275
- tree.pack(fill=tk.BOTH, expand=True, pady=(0, 8))
312
+ tree.pack(fill=tk.BOTH, expand=True, pady=(0, 4))
313
+ ttk.Label(main_frame, text=_row_count_text(len(rows))).pack(anchor="w", pady=(0, 8))
276
314
 
277
315
  specs = args.get("entry_specs", [])
278
316
  form_frame = ttk.Frame(main_frame)
@@ -280,10 +318,16 @@ class EntryFormDialog:
280
318
 
281
319
  for i, spec in enumerate(specs):
282
320
  etype = (spec.entry_type or "text").lower()
283
- ttk.Label(form_frame, text=spec.label or spec.varname, anchor="e").grid(
321
+ # For radiobuttons, the label is semicolon-delimited: first part is the label
322
+ if etype == "radiobuttons":
323
+ parts = (spec.label or "").split(";")
324
+ field_label = parts[0].strip() if parts else spec.varname
325
+ else:
326
+ field_label = spec.label or spec.varname
327
+ ttk.Label(form_frame, text=field_label, anchor="e").grid(
284
328
  row=i,
285
329
  column=0,
286
- sticky="e",
330
+ sticky="ne",
287
331
  padx=4,
288
332
  pady=2,
289
333
  )
@@ -297,35 +341,108 @@ class EntryFormDialog:
297
341
  combo = ttk.Combobox(form_frame, textvariable=var, values=spec.lookup_list, state="readonly")
298
342
  combo.grid(row=i, column=1, sticky="ew", pady=2)
299
343
  self._widgets[spec.varname] = ("dropdown", var)
344
+ elif etype == "listbox" and spec.lookup_list:
345
+ height = spec.default_height or 4
346
+ lb = tk.Listbox(form_frame, selectmode=tk.MULTIPLE, height=height, exportselection=False)
347
+ for item in spec.lookup_list:
348
+ lb.insert(tk.END, item)
349
+ lb.grid(row=i, column=1, sticky="ew", pady=2)
350
+ self._widgets[spec.varname] = ("listbox", lb)
351
+ elif etype == "radiobuttons":
352
+ buttons = parts[1:] if len(parts) > 1 else ["Option"]
353
+ var = tk.IntVar(value=0)
354
+ rb_frame = ttk.Frame(form_frame)
355
+ rb_frame.grid(row=i, column=1, sticky="w", pady=2)
356
+ for j, btn_label in enumerate(buttons):
357
+ ttk.Radiobutton(rb_frame, text=btn_label.strip(), variable=var, value=j).pack(anchor="w")
358
+ self._widgets[spec.varname] = ("radiobuttons", var)
359
+ elif etype == "textarea":
360
+ height = spec.default_height or 5
361
+ ta = tk.Text(form_frame, width=40, height=height, wrap=tk.WORD)
362
+ if spec.initial_value:
363
+ ta.insert("1.0", spec.initial_value)
364
+ ta.grid(row=i, column=1, sticky="ew", pady=2)
365
+ self._widgets[spec.varname] = ("textarea", ta)
366
+ elif etype in ("inputfile", "outputfile"):
367
+ file_frame = ttk.Frame(form_frame)
368
+ file_frame.grid(row=i, column=1, sticky="ew", pady=2)
369
+ var = tk.StringVar(value=spec.initial_value or "")
370
+ ttk.Entry(file_frame, textvariable=var, width=30).pack(side=tk.LEFT, fill=tk.X, expand=True)
371
+
372
+ def _browse(sv=var, mode=etype):
373
+ from tkinter import filedialog
374
+
375
+ if mode == "inputfile":
376
+ fn = filedialog.askopenfilename()
377
+ else:
378
+ fn = filedialog.asksaveasfilename()
379
+ if fn:
380
+ sv.set(fn)
381
+
382
+ ttk.Button(file_frame, text="Browse…", command=_browse).pack(side=tk.LEFT, padx=(4, 0))
383
+ self._widgets[spec.varname] = ("file", var)
300
384
  else:
301
385
  var = tk.StringVar(value=spec.initial_value or "")
302
386
  entry = ttk.Entry(form_frame, textvariable=var, width=40)
387
+ if spec.validation_key_regex:
388
+ import re as _re
389
+
390
+ _pat = _re.compile(spec.validation_key_regex)
391
+ vcmd = (win.register(lambda val, p=_pat: bool(p.match(val))), "%P")
392
+ entry.configure(validate="key", validatecommand=vcmd)
303
393
  entry.grid(row=i, column=1, sticky="ew", pady=2)
304
394
  self._widgets[spec.varname] = ("text", var)
305
395
  form_frame.columnconfigure(1, weight=1)
306
396
 
307
397
  btn_frame = ttk.Frame(main_frame)
308
- btn_frame.pack(pady=8)
398
+ btn_frame.pack(pady=8, anchor="e")
309
399
  ttk.Button(btn_frame, text="OK", command=lambda: self._close(win, specs, True)).pack(side=tk.LEFT, padx=4)
310
400
  ttk.Button(btn_frame, text="Cancel", command=lambda: self._close(win, specs, False)).pack(side=tk.LEFT, padx=4)
311
401
  win.bind("<Return>", lambda e: self._close(win, specs, True))
312
402
  win.bind("<Escape>", lambda e: self._close(win, specs, False))
313
403
 
314
404
  self._specs = specs
315
- _center_window(win, 500, 120 + len(specs) * 32)
405
+ extra_height = sum(
406
+ (spec.default_height or 5) * 18 if (spec.entry_type or "").lower() in ("textarea", "listbox") else 0
407
+ for spec in specs
408
+ )
409
+ _center_window(win, 500, 120 + len(specs) * 32 + extra_height)
316
410
  root.wait_window(win)
317
411
 
318
412
  def _close(self, win: tk.Toplevel, specs: list, ok: bool) -> None:
319
413
  if ok:
414
+ import re as _re
415
+
320
416
  for spec in specs:
321
417
  entry = self._widgets.get(spec.varname)
322
418
  if entry is None:
323
419
  continue
324
420
  kind, var = entry
325
421
  if kind == "checkbox":
326
- spec.value = "True" if var.get() else "False"
422
+ spec.value = "1" if var.get() else "0"
423
+ elif kind == "listbox":
424
+ selected = [var.get(i) for i in var.curselection()]
425
+ spec.value = ",".join(f"'{v.replace(chr(39), chr(39) + chr(39))}'" for v in selected)
426
+ elif kind == "radiobuttons":
427
+ spec.value = str(var.get() + 1)
428
+ elif kind == "textarea":
429
+ spec.value = var.get("1.0", tk.END).rstrip("\n")
327
430
  else:
328
431
  spec.value = var.get()
432
+ # Validate required fields and validation_regex
433
+ errors = []
434
+ for spec in specs:
435
+ val = spec.value or ""
436
+ etype = (spec.entry_type or "text").lower()
437
+ if spec.required and not val and etype != "checkbox":
438
+ errors.append(f"{spec.label or spec.varname}: required")
439
+ if spec.validation_regex and val and not _re.fullmatch(spec.validation_regex, val):
440
+ errors.append(f"{spec.label or spec.varname}: does not match pattern")
441
+ if errors:
442
+ from tkinter import messagebox
443
+
444
+ messagebox.showerror("Validation Error", "\n".join(errors), parent=win)
445
+ return
329
446
  self.result = {"button": 1, "return_value": specs}
330
447
  else:
331
448
  self.result = {"button": None, "return_value": specs}
@@ -348,29 +465,45 @@ class CompareDialog:
348
465
 
349
466
  frame = ttk.Frame(win, padding=12)
350
467
  frame.pack(fill=tk.BOTH, expand=True)
468
+ _add_help_button(frame, args.get("help_url"))
351
469
 
352
470
  message = args.get("message", "")
353
471
  if message:
354
- ttk.Label(frame, text=message, wraplength=780).pack(pady=(0, 8))
472
+ ttk.Label(frame, text=message, wraplength=780, anchor="w", justify="left").pack(
473
+ anchor="w",
474
+ pady=(0, 8),
475
+ )
355
476
 
356
477
  headers1 = args.get("headers1", [])
357
478
  rows1 = args.get("rows1", [])
358
479
  headers2 = args.get("headers2", [])
359
480
  rows2 = args.get("rows2", [])
360
481
  keylist = [str(k) for k in args.get("keylist", [])]
482
+ sidebyside = args.get("sidebyside", True)
483
+
484
+ # Reserve a frame for the diff button (packed before tables, populated after)
485
+ diff_frame = ttk.Frame(frame) if keylist else None
486
+ if diff_frame:
487
+ diff_frame.pack(anchor="w", pady=(0, 4))
361
488
 
362
489
  tables_frame = ttk.Frame(frame)
363
490
  tables_frame.pack(fill=tk.BOTH, expand=True)
364
491
 
492
+ pack_side = tk.LEFT if sidebyside else tk.TOP
493
+ max_tree_height = 12 if sidebyside else 8
494
+
365
495
  def _add_tree(parent, label, headers, rows):
366
496
  lf = ttk.LabelFrame(parent, text=label)
367
- lf.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=4)
368
- tree = ttk.Treeview(lf, height=min(12, len(rows)))
497
+ lf.pack(side=pack_side, fill=tk.BOTH, expand=True, padx=4, pady=2)
498
+ tree_frame = ttk.Frame(lf)
499
+ tree_frame.pack(fill=tk.BOTH, expand=True)
500
+ tree = ttk.Treeview(tree_frame, height=min(max_tree_height, len(rows)))
369
501
  _populate_treeview(tree, headers, rows)
370
- vsb = ttk.Scrollbar(lf, orient=tk.VERTICAL, command=tree.yview)
502
+ vsb = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=tree.yview)
371
503
  tree.configure(yscrollcommand=vsb.set)
372
504
  tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
373
505
  vsb.pack(side=tk.RIGHT, fill=tk.Y)
506
+ ttk.Label(lf, text=_row_count_text(len(rows))).pack(anchor="w")
374
507
  return tree
375
508
 
376
509
  tree1 = _add_tree(tables_frame, "Table 1", headers1, rows1)
@@ -414,13 +547,63 @@ class CompareDialog:
414
547
  tree1.bind("<ButtonRelease-1>", _on_click1)
415
548
  tree2.bind("<ButtonRelease-1>", _on_click2)
416
549
 
550
+ # --- Highlight Diffs button (populate the pre-created diff_frame) ---
551
+ if keylist and diff_frame is not None:
552
+ tree1.tag_configure("diff_match", background="#a3d9a5", foreground="#1a3a1a")
553
+ tree1.tag_configure("diff_changed", background="#f5d98e", foreground="#3a2e00")
554
+ tree1.tag_configure("diff_only", background="#f5a3a3", foreground="#3a0a0a")
555
+ tree2.tag_configure("diff_match", background="#a3d9a5", foreground="#1a3a1a")
556
+ tree2.tag_configure("diff_changed", background="#f5d98e", foreground="#3a2e00")
557
+ tree2.tag_configure("diff_only", background="#f5a3a3", foreground="#3a0a0a")
558
+ _diff_on = [False]
559
+
560
+ def _toggle_diffs():
561
+ _diff_on[0] = not _diff_on[0]
562
+ if not _diff_on[0]:
563
+ for iid in tree1.get_children():
564
+ tree1.item(iid, tags=())
565
+ for iid in tree2.get_children():
566
+ tree2.item(iid, tags=())
567
+ return
568
+ keys1_set = set(iid_to_kv1.values())
569
+ keys2_set = set(iid_to_kv2.values())
570
+ for iid, kv in iid_to_kv1.items():
571
+ if kv not in keys2_set:
572
+ tree1.item(iid, tags=("diff_only",))
573
+ else:
574
+ r1 = [str(v) if v is not None else "" for v in rows1[list(iids1).index(iid)]]
575
+ match_iid = kv_to_iid2.get(kv)
576
+ if match_iid:
577
+ r2 = [str(v) if v is not None else "" for v in rows2[list(iids2).index(match_iid)]]
578
+ tree1.item(iid, tags=("diff_match",) if r1 == r2 else ("diff_changed",))
579
+ for iid, kv in iid_to_kv2.items():
580
+ if kv not in keys1_set:
581
+ tree2.item(iid, tags=("diff_only",))
582
+ else:
583
+ r2 = [str(v) if v is not None else "" for v in rows2[list(iids2).index(iid)]]
584
+ match_iid = kv_to_iid1.get(kv)
585
+ if match_iid:
586
+ r1 = [str(v) if v is not None else "" for v in rows1[list(iids1).index(match_iid)]]
587
+ tree2.item(iid, tags=("diff_match",) if r1 == r2 else ("diff_changed",))
588
+
589
+ ttk.Button(diff_frame, text="Highlight Diffs", command=_toggle_diffs).pack(side=tk.LEFT)
590
+ ttk.Label(diff_frame, text=" ").pack(side=tk.LEFT)
591
+ tk.Label(diff_frame, text=" Match ", bg="#a3d9a5", fg="#1a3a1a", padx=4).pack(side=tk.LEFT, padx=2)
592
+ tk.Label(diff_frame, text=" Changed ", bg="#f5d98e", fg="#3a2e00", padx=4).pack(side=tk.LEFT, padx=2)
593
+ tk.Label(diff_frame, text=" Only in one ", bg="#f5a3a3", fg="#3a0a0a", padx=4).pack(side=tk.LEFT, padx=2)
594
+
595
+ summary = _compare_stats(headers1, rows1, headers2, rows2, keylist)
596
+ if summary:
597
+ ttk.Label(frame, text=summary).pack(anchor="w", pady=(4, 0))
598
+
417
599
  button_list = args.get("button_list", [("Continue", 1, "<Return>")])
418
600
  btn_frame = ttk.Frame(frame)
419
- btn_frame.pack(pady=8)
601
+ btn_frame.pack(pady=8, anchor="e")
420
602
  _add_buttons(btn_frame, button_list, lambda v: self._close(win, v))
421
603
  win.bind("<Escape>", lambda e: self._close(win, None))
422
604
 
423
- _center_window(win, 900, 500)
605
+ win_height = 700 if not sidebyside else 500
606
+ _center_window(win, 900, win_height)
424
607
  root.wait_window(win)
425
608
 
426
609
  def _close(self, win: tk.Toplevel, value: int | None) -> None:
@@ -444,11 +627,18 @@ class SelectRowsDialog:
444
627
 
445
628
  frame = ttk.Frame(win, padding=12)
446
629
  frame.pack(fill=tk.BOTH, expand=True)
630
+ _add_help_button(frame, args.get("help_url"))
447
631
 
448
632
  message = args.get("message", "")
449
633
  if message:
450
- ttk.Label(frame, text=message, wraplength=780).pack(pady=(0, 4))
451
- ttk.Label(frame, text="Double-click a row to copy it to the destination table.").pack(pady=(0, 8))
634
+ ttk.Label(frame, text=message, wraplength=780, anchor="w", justify="left").pack(
635
+ anchor="w",
636
+ pady=(0, 4),
637
+ )
638
+ ttk.Label(frame, text="Double-click a row to copy it to the destination table.").pack(
639
+ anchor="w",
640
+ pady=(0, 8),
641
+ )
452
642
 
453
643
  headers1 = args.get("headers1", [])
454
644
  rows1 = args.get("rows1", [])
@@ -461,12 +651,15 @@ class SelectRowsDialog:
461
651
  def _add_tree_frame(parent, label, headers, rows):
462
652
  lf = ttk.LabelFrame(parent, text=label)
463
653
  lf.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=4)
464
- tree = ttk.Treeview(lf, height=min(12, max(len(rows1), len(rows2))))
654
+ tree_frame = ttk.Frame(lf)
655
+ tree_frame.pack(fill=tk.BOTH, expand=True)
656
+ tree = ttk.Treeview(tree_frame, height=min(12, max(len(rows1), len(rows2))))
465
657
  _populate_treeview(tree, headers, rows)
466
- vsb = ttk.Scrollbar(lf, orient=tk.VERTICAL, command=tree.yview)
658
+ vsb = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=tree.yview)
467
659
  tree.configure(yscrollcommand=vsb.set)
468
660
  tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
469
661
  vsb.pack(side=tk.RIGHT, fill=tk.Y)
662
+ ttk.Label(lf, text=_row_count_text(len(rows))).pack(anchor="w")
470
663
  return tree
471
664
 
472
665
  src_tree = _add_tree_frame(tables_frame, "Source", headers1, rows1)
@@ -484,7 +677,7 @@ class SelectRowsDialog:
484
677
 
485
678
  button_list = args.get("button_list", [("Continue", 1, "<Return>")])
486
679
  btn_frame = ttk.Frame(frame)
487
- btn_frame.pack(pady=8)
680
+ btn_frame.pack(pady=8, anchor="e")
488
681
  _add_buttons(btn_frame, button_list, lambda v: self._close(win, v))
489
682
  win.bind("<Escape>", lambda e: self._close(win, None))
490
683
 
@@ -512,20 +705,27 @@ class SelectSubDialog:
512
705
 
513
706
  frame = ttk.Frame(win, padding=12)
514
707
  frame.pack(fill=tk.BOTH, expand=True)
708
+ _add_help_button(frame, args.get("help_url"))
515
709
 
516
710
  message = args.get("message", "")
517
711
  if message:
518
- ttk.Label(frame, text=message, wraplength=580).pack(pady=(0, 8))
712
+ ttk.Label(frame, text=message, wraplength=580, anchor="w", justify="left").pack(
713
+ anchor="w",
714
+ pady=(0, 8),
715
+ )
519
716
 
520
717
  headers = args.get("headers", [])
521
718
  rows = args.get("rows", [])
522
719
 
523
- tree = ttk.Treeview(frame, height=min(10, len(rows)))
720
+ tree_frame = ttk.Frame(frame)
721
+ tree_frame.pack(fill=tk.BOTH, expand=True)
722
+ tree = ttk.Treeview(tree_frame, height=min(10, len(rows)))
524
723
  _populate_treeview(tree, headers, rows)
525
- vsb = ttk.Scrollbar(frame, orient=tk.VERTICAL, command=tree.yview)
724
+ vsb = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=tree.yview)
526
725
  tree.configure(yscrollcommand=vsb.set)
527
726
  tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
528
727
  vsb.pack(side=tk.RIGHT, fill=tk.Y)
728
+ ttk.Label(frame, text=_row_count_text(len(rows))).pack(anchor="w")
529
729
 
530
730
  def _select():
531
731
  sel = tree.selection()
@@ -536,7 +736,7 @@ class SelectSubDialog:
536
736
  win.destroy()
537
737
 
538
738
  btn_frame = ttk.Frame(frame)
539
- btn_frame.pack(pady=8)
739
+ btn_frame.pack(pady=8, anchor="e")
540
740
  ttk.Button(btn_frame, text="OK", command=_select).pack(side=tk.LEFT, padx=4)
541
741
  ttk.Button(btn_frame, text="Cancel", command=lambda: self._close(win)).pack(side=tk.LEFT, padx=4)
542
742
  tree.bind("<Double-1>", lambda e: _select())
@@ -566,17 +766,22 @@ class ActionDialog:
566
766
 
567
767
  frame = ttk.Frame(win, padding=12)
568
768
  frame.pack(fill=tk.BOTH, expand=True)
769
+ _add_help_button(frame, args.get("help_url"))
569
770
 
570
771
  message = args.get("message", "")
571
772
  if message:
572
- ttk.Label(frame, text=message, wraplength=580).pack(pady=(0, 8))
773
+ ttk.Label(frame, text=message, wraplength=580, anchor="w", justify="left").pack(
774
+ anchor="w",
775
+ pady=(0, 8),
776
+ )
573
777
 
574
778
  headers = args.get("column_headers")
575
779
  rows = args.get("rowset")
576
780
  if headers and rows:
577
781
  tree = ttk.Treeview(frame, height=min(6, len(rows)))
578
782
  _populate_treeview(tree, headers, rows)
579
- tree.pack(fill=tk.BOTH, expand=True, pady=(0, 8))
783
+ tree.pack(fill=tk.BOTH, expand=True, pady=(0, 4))
784
+ ttk.Label(frame, text=_row_count_text(len(rows))).pack(anchor="w", pady=(0, 8))
580
785
 
581
786
  button_specs = args.get("button_specs", [])
582
787
  for i, spec in enumerate(button_specs):
@@ -619,22 +824,31 @@ class MapDialog:
619
824
 
620
825
  message = args.get("message", "")
621
826
  if message:
622
- ttk.Label(frame, text=message, wraplength=580).pack(pady=(0, 4))
623
- ttk.Label(frame, text="(Interactive map requires tkintermapview; showing tabular data)").pack(pady=(0, 8))
827
+ ttk.Label(frame, text=message, wraplength=580, anchor="w", justify="left").pack(
828
+ anchor="w",
829
+ pady=(0, 4),
830
+ )
831
+ ttk.Label(frame, text="(Interactive map requires tkintermapview; showing tabular data)").pack(
832
+ anchor="w",
833
+ pady=(0, 8),
834
+ )
624
835
 
625
836
  headers = args.get("headers", [])
626
837
  rows = args.get("rows", [])
627
838
  if headers and rows:
628
- tree = ttk.Treeview(frame, height=min(12, len(rows)))
839
+ tree_frame = ttk.Frame(frame)
840
+ tree_frame.pack(fill=tk.BOTH, expand=True)
841
+ tree = ttk.Treeview(tree_frame, height=min(12, len(rows)))
629
842
  _populate_treeview(tree, headers, rows)
630
- vsb = ttk.Scrollbar(frame, orient=tk.VERTICAL, command=tree.yview)
843
+ vsb = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=tree.yview)
631
844
  tree.configure(yscrollcommand=vsb.set)
632
845
  tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
633
846
  vsb.pack(side=tk.RIGHT, fill=tk.Y)
847
+ ttk.Label(frame, text=_row_count_text(len(rows))).pack(anchor="w")
634
848
 
635
849
  button_list = args.get("button_list", [("Continue", 1, "<Return>")])
636
850
  btn_frame = ttk.Frame(frame)
637
- btn_frame.pack(pady=8)
851
+ btn_frame.pack(pady=8, anchor="e")
638
852
  _add_buttons(btn_frame, button_list, lambda v: self._close(win, v))
639
853
 
640
854
  _center_window(win, 600, 450)
@@ -661,10 +875,14 @@ class CredentialsDialog:
661
875
 
662
876
  frame = ttk.Frame(win, padding=12)
663
877
  frame.pack(fill=tk.BOTH, expand=True)
878
+ _add_help_button(frame, args.get("help_url"))
664
879
 
665
880
  message = args.get("message", "")
666
881
  if message:
667
- ttk.Label(frame, text=message, wraplength=380).pack(pady=(0, 8))
882
+ ttk.Label(frame, text=message, wraplength=380, anchor="w", justify="left").pack(
883
+ anchor="w",
884
+ pady=(0, 8),
885
+ )
668
886
 
669
887
  user_var = tk.StringVar()
670
888
  pw_var = tk.StringVar()
@@ -677,7 +895,7 @@ class CredentialsDialog:
677
895
  frame.columnconfigure(1, weight=1)
678
896
 
679
897
  btn_frame = ttk.Frame(frame)
680
- btn_frame.grid(row=2, column=0, columnspan=2, pady=8)
898
+ btn_frame.grid(row=2, column=0, columnspan=2, pady=8, sticky="e")
681
899
  ttk.Button(btn_frame, text="OK", command=lambda: self._close(win, user_var.get(), pw_var.get())).pack(
682
900
  side=tk.LEFT,
683
901
  padx=4,
@@ -722,10 +940,14 @@ class ConnectDialog:
722
940
 
723
941
  frame = ttk.Frame(win, padding=12)
724
942
  frame.pack(fill=tk.BOTH, expand=True)
943
+ _add_help_button(frame, args.get("help_url"))
725
944
 
726
945
  message = args.get("message", "")
727
946
  if message:
728
- ttk.Label(frame, text=message, wraplength=480).pack(pady=(0, 8))
947
+ ttk.Label(frame, text=message, wraplength=480, anchor="w", justify="left").pack(
948
+ anchor="w",
949
+ pady=(0, 8),
950
+ )
729
951
 
730
952
  type_var = tk.StringVar(value="p")
731
953
  server_var = tk.StringVar()
@@ -756,7 +978,7 @@ class ConnectDialog:
756
978
  ttk.Entry(form, textvariable=user_var, width=35).grid(row=3, column=1, sticky="ew", pady=4)
757
979
 
758
980
  btn_frame = ttk.Frame(frame)
759
- btn_frame.pack(pady=8)
981
+ btn_frame.pack(pady=8, anchor="e")
760
982
 
761
983
  def _on_connect():
762
984
  idx = type_combo.current()