sqlbench 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,572 @@
1
+ """Spool file utility tab."""
2
+
3
+ import tkinter as tk
4
+ from tkinter import ttk, messagebox, filedialog
5
+ import threading
6
+ import subprocess
7
+ import tempfile
8
+ import platform
9
+
10
+
11
+ class SpoolTab:
12
+ def __init__(self, parent, app, connection, conn_name, os_version):
13
+ self.app = app
14
+ self.connection = connection
15
+ self.conn_name = conn_name
16
+ self.os_version = os_version
17
+ self.frame = ttk.Frame(parent)
18
+ self.frame.pack(fill=tk.BOTH, expand=True)
19
+ self._running = False
20
+ self._current_spool_info = None # Store spool file info for PDF export
21
+ self._create_widgets()
22
+
23
+ def _create_widgets(self):
24
+ # Top controls
25
+ ctrl_frame = ttk.Frame(self.frame)
26
+ ctrl_frame.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5)
27
+
28
+ ttk.Label(ctrl_frame, text="User:").pack(side=tk.LEFT, padx=(0, 5))
29
+ self.user_entry = ttk.Entry(ctrl_frame, width=15)
30
+ self.user_entry.pack(side=tk.LEFT, padx=(0, 10))
31
+ self.user_entry.insert(0, "*CURRENT")
32
+
33
+ self.refresh_btn = ttk.Button(ctrl_frame, text="Refresh", command=self._refresh_spool_files)
34
+ self.refresh_btn.pack(side=tk.LEFT, padx=2)
35
+ ttk.Button(ctrl_frame, text="View", command=self._view_spool_file).pack(side=tk.LEFT, padx=2)
36
+ ttk.Button(ctrl_frame, text="Delete", command=self._delete_spool_files).pack(side=tk.LEFT, padx=2)
37
+
38
+ # Connection info label
39
+ ttk.Label(ctrl_frame, text=f" [{self.conn_name}]").pack(side=tk.RIGHT, padx=5)
40
+
41
+ # Paned window for list and viewer
42
+ self.paned = ttk.PanedWindow(self.frame, orient=tk.HORIZONTAL)
43
+ self.paned.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
44
+
45
+ # Spool file list
46
+ list_frame = ttk.LabelFrame(self.paned, text="Spool Files")
47
+ self.paned.add(list_frame, weight=1)
48
+
49
+ columns = ("file", "user", "job", "filenumber", "status", "pages")
50
+ self.spool_tree = ttk.Treeview(list_frame, columns=columns, show="headings", selectmode="extended")
51
+
52
+ self.spool_tree.heading("file", text="File")
53
+ self.spool_tree.heading("user", text="User")
54
+ self.spool_tree.heading("job", text="Job")
55
+ self.spool_tree.heading("filenumber", text="File #")
56
+ self.spool_tree.heading("status", text="Status")
57
+ self.spool_tree.heading("pages", text="Pages")
58
+
59
+ self.spool_tree.column("file", width=75, minwidth=50)
60
+ self.spool_tree.column("user", width=50, minwidth=35)
61
+ self.spool_tree.column("job", width=180, minwidth=100)
62
+ self.spool_tree.column("filenumber", width=40, minwidth=30, anchor="e")
63
+ self.spool_tree.column("status", width=45, minwidth=35)
64
+ self.spool_tree.column("pages", width=40, minwidth=30, anchor="e")
65
+
66
+ spool_scroll = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.spool_tree.yview)
67
+ self.spool_tree.configure(yscrollcommand=spool_scroll.set)
68
+
69
+ spool_scroll.pack(side=tk.RIGHT, fill=tk.Y)
70
+ self.spool_tree.pack(fill=tk.BOTH, expand=True)
71
+
72
+ self.spool_tree.bind("<Double-1>", lambda e: self._view_spool_file())
73
+
74
+ # Viewer area
75
+ viewer_frame = ttk.LabelFrame(self.paned, text="Viewer")
76
+ self.paned.add(viewer_frame, weight=2)
77
+
78
+ # Viewer button bar
79
+ viewer_btn_frame = ttk.Frame(viewer_frame)
80
+ viewer_btn_frame.pack(side=tk.TOP, fill=tk.X, padx=2, pady=2)
81
+
82
+ self.save_pdf_btn = ttk.Button(viewer_btn_frame, text="Save PDF", command=self._save_pdf, state=tk.DISABLED)
83
+ self.save_pdf_btn.pack(side=tk.LEFT, padx=2)
84
+ self.print_btn = ttk.Button(viewer_btn_frame, text="Print", command=self._print_spool, state=tk.DISABLED)
85
+ self.print_btn.pack(side=tk.LEFT, padx=2)
86
+
87
+ self.viewer_text = tk.Text(viewer_frame, wrap=tk.NONE, font=("Courier", 10))
88
+ viewer_scroll_y = ttk.Scrollbar(viewer_frame, orient=tk.VERTICAL, command=self.viewer_text.yview)
89
+ viewer_scroll_x = ttk.Scrollbar(viewer_frame, orient=tk.HORIZONTAL, command=self.viewer_text.xview)
90
+ self.viewer_text.configure(yscrollcommand=viewer_scroll_y.set, xscrollcommand=viewer_scroll_x.set)
91
+
92
+ viewer_scroll_y.pack(side=tk.RIGHT, fill=tk.Y)
93
+ viewer_scroll_x.pack(side=tk.BOTTOM, fill=tk.X)
94
+ self.viewer_text.pack(fill=tk.BOTH, expand=True)
95
+
96
+ def _set_running(self, running):
97
+ """Update UI state for running/not running."""
98
+ self._running = running
99
+ if running:
100
+ self.refresh_btn.config(state=tk.DISABLED)
101
+ self.app.root.config(cursor="watch")
102
+ else:
103
+ self.refresh_btn.config(state=tk.NORMAL)
104
+ self.app.root.config(cursor="")
105
+
106
+ def _refresh_spool_files(self):
107
+ if self._running:
108
+ return
109
+
110
+ user = self.user_entry.get().strip().upper() or "*CURRENT"
111
+ self.user_entry.delete(0, tk.END)
112
+ self.user_entry.insert(0, user)
113
+
114
+ # Clear current list
115
+ self.spool_tree.delete(*self.spool_tree.get_children())
116
+
117
+ self._set_running(True)
118
+ self.app.statusbar.config(text=f"Loading spool files from {self.conn_name}...")
119
+
120
+ thread = threading.Thread(target=self._fetch_spool_files, args=(user,), daemon=True)
121
+ thread.start()
122
+
123
+ def _fetch_spool_files(self, user):
124
+ """Fetch spool files in background thread."""
125
+ try:
126
+ cursor = self.connection.cursor()
127
+
128
+ sql = """
129
+ SELECT
130
+ SPOOLED_FILE_NAME,
131
+ USER_NAME,
132
+ JOB_NAME,
133
+ FILE_NUMBER,
134
+ STATUS,
135
+ TOTAL_PAGES
136
+ FROM QSYS2.OUTPUT_QUEUE_ENTRIES
137
+ WHERE USER_NAME = CASE WHEN ? = '*CURRENT' THEN USER ELSE ? END
138
+ ORDER BY CREATE_TIMESTAMP DESC
139
+ FETCH FIRST 100 ROWS ONLY
140
+ """
141
+ cursor.execute(sql, (user, user))
142
+ rows = cursor.fetchall()
143
+ cursor.close()
144
+
145
+ self.app.root.after(0, self._display_spool_files, rows, user)
146
+ except Exception as e:
147
+ self.app.root.after(0, self._spool_error, str(e))
148
+ finally:
149
+ self.app.root.after(0, self._set_running, False)
150
+
151
+ def _display_spool_files(self, rows, user):
152
+ """Display spool files in treeview (called from main thread)."""
153
+ for row in rows:
154
+ clean_row = tuple(str(v) if v is not None else "" for v in row)
155
+ self.spool_tree.insert("", tk.END, values=clean_row)
156
+ self.app.statusbar.config(text=f"Loaded {len(rows)} spool files for {user} on {self.conn_name}")
157
+
158
+ def _spool_error(self, error):
159
+ """Handle spool file errors."""
160
+ messagebox.showerror("Error", error)
161
+
162
+ def _view_spool_file(self):
163
+ selection = self.spool_tree.selection()
164
+ if not selection:
165
+ messagebox.showinfo("Select", "Please select a spool file to view.")
166
+ return
167
+
168
+ item = self.spool_tree.item(selection[0])
169
+ values = item["values"]
170
+ file_name = values[0]
171
+ qualified_job = values[2] # Already in "number/user/name" format
172
+ file_number = values[3]
173
+
174
+ # Parse job name from qualified job (format: number/user/name)
175
+ job_parts = qualified_job.split("/")
176
+ job_name = job_parts[2] if len(job_parts) == 3 else qualified_job
177
+
178
+ try:
179
+ cursor = self.connection.cursor()
180
+
181
+ # Get spool file attributes for proper page formatting
182
+ attr_sql = """
183
+ SELECT PAGE_LENGTH, PAGE_WIDTH, LPI, CPI
184
+ FROM QSYS2.OUTPUT_QUEUE_ENTRIES
185
+ WHERE JOB_NAME = ?
186
+ AND SPOOLED_FILE_NAME = ?
187
+ AND FILE_NUMBER = ?
188
+ """
189
+ cursor.execute(attr_sql, (qualified_job, file_name, int(file_number)))
190
+ attr_row = cursor.fetchone()
191
+
192
+ page_length = 66 # Default
193
+ page_width = 132 # Default
194
+ if attr_row:
195
+ page_length = attr_row[0] or 66
196
+ page_width = attr_row[1] or 132
197
+
198
+ # Save info for PDF export
199
+ self._current_spool_info = {
200
+ "file_name": file_name,
201
+ "job_name": job_name,
202
+ "file_number": file_number,
203
+ "page_length": page_length,
204
+ "page_width": page_width
205
+ }
206
+
207
+ # Read spool file content using SYSTOOLS.SPOOLED_FILE_DATA
208
+ sql = """
209
+ SELECT SPOOLED_DATA
210
+ FROM TABLE(SYSTOOLS.SPOOLED_FILE_DATA(
211
+ JOB_NAME => ?,
212
+ SPOOLED_FILE_NAME => ?,
213
+ SPOOLED_FILE_NUMBER => ?
214
+ ))
215
+ """
216
+ cursor.execute(sql, (qualified_job, file_name, int(file_number)))
217
+
218
+ self.viewer_text.delete("1.0", tk.END)
219
+
220
+ # Store raw lines for PDF generation
221
+ raw_lines = []
222
+ for row in cursor.fetchall():
223
+ line = row[0] if row[0] else ""
224
+ # Clean control characters but keep printable ASCII and common chars
225
+ clean_line = ''.join(c if (c.isprintable() or c in '\t') else ' ' for c in line)
226
+ raw_lines.append(clean_line)
227
+ self.viewer_text.insert(tk.END, clean_line + "\n")
228
+
229
+ # Store lines in spool info for PDF
230
+ self._current_spool_info["lines"] = raw_lines
231
+
232
+ cursor.close()
233
+ self._update_viewer_buttons()
234
+ self.app.statusbar.config(text=f"Loaded spool file {file_name} ({page_width}x{page_length}, {len(raw_lines)} lines) from {self.conn_name}")
235
+ except Exception as e:
236
+ messagebox.showerror("Error", f"Could not read spool file: {e}")
237
+
238
+ def _delete_spool_files(self):
239
+ """Delete selected spool files."""
240
+ selection = self.spool_tree.selection()
241
+ if not selection:
242
+ messagebox.showinfo("Select", "Please select spool file(s) to delete.")
243
+ return
244
+
245
+ # Build list of files to delete
246
+ files_to_delete = []
247
+ for item_id in selection:
248
+ item = self.spool_tree.item(item_id)
249
+ values = item["values"]
250
+ files_to_delete.append({
251
+ "file_name": values[0],
252
+ "job": values[2],
253
+ "file_number": values[3]
254
+ })
255
+
256
+ # Confirmation
257
+ count = len(files_to_delete)
258
+ if count == 1:
259
+ msg = f"Delete spool file '{files_to_delete[0]['file_name']}'?"
260
+ else:
261
+ msg = f"Delete {count} selected spool files?"
262
+
263
+ if not messagebox.askyesno("Confirm Delete", msg):
264
+ return
265
+
266
+ # Delete in background thread
267
+ self._set_running(True)
268
+ self.app.statusbar.config(text=f"Deleting {count} spool file(s)...")
269
+ thread = threading.Thread(target=self._do_delete_spool_files, args=(files_to_delete,), daemon=True)
270
+ thread.start()
271
+
272
+ def _do_delete_spool_files(self, files_to_delete):
273
+ """Delete spool files in background thread."""
274
+ deleted = 0
275
+ errors = []
276
+
277
+ try:
278
+ cursor = self.connection.cursor()
279
+
280
+ for f in files_to_delete:
281
+ try:
282
+ # Parse job name (format: number/user/name)
283
+ job_parts = f["job"].split("/")
284
+ if len(job_parts) == 3:
285
+ job_number, job_user, job_name = job_parts
286
+ else:
287
+ job_name = f["job"]
288
+ job_user = "*N"
289
+ job_number = "*N"
290
+
291
+ # Build DLTSPLF command
292
+ cmd = f"DLTSPLF FILE({f['file_name']}) JOB({job_number}/{job_user}/{job_name}) SPLNBR({f['file_number']})"
293
+
294
+ cursor.execute("CALL QSYS2.QCMDEXC(?)", (cmd,))
295
+ deleted += 1
296
+ except Exception as e:
297
+ errors.append(f"{f['file_name']}: {e}")
298
+
299
+ cursor.close()
300
+ except Exception as e:
301
+ errors.append(str(e))
302
+
303
+ # Update UI on main thread
304
+ self.app.root.after(0, self._delete_complete, deleted, errors)
305
+
306
+ def _delete_complete(self, deleted, errors):
307
+ """Handle delete completion on main thread."""
308
+ self._set_running(False)
309
+
310
+ if errors:
311
+ error_msg = "\n".join(errors[:5]) # Show first 5 errors
312
+ if len(errors) > 5:
313
+ error_msg += f"\n... and {len(errors) - 5} more errors"
314
+ messagebox.showerror("Delete Errors", f"Deleted {deleted} file(s).\n\nErrors:\n{error_msg}")
315
+ else:
316
+ self.app.statusbar.config(text=f"Deleted {deleted} spool file(s) from {self.conn_name}")
317
+
318
+ # Refresh list
319
+ self._refresh_spool_files()
320
+
321
+ def _generate_pdf(self, content, file_path):
322
+ """Generate a PDF from content using spool file attributes."""
323
+ from reportlab.lib.pagesizes import letter, landscape
324
+ from reportlab.lib.units import inch
325
+ from reportlab.pdfgen import canvas
326
+
327
+ # Get spool file attributes and stored lines
328
+ spool_page_length = 66 # Default lines per page
329
+ spool_page_width = 132 # Default chars per line
330
+ lines = None
331
+
332
+ if self._current_spool_info:
333
+ spool_page_length = self._current_spool_info.get("page_length", 66)
334
+ spool_page_width = self._current_spool_info.get("page_width", 132)
335
+ lines = self._current_spool_info.get("lines")
336
+
337
+ # Fall back to parsing content if no stored lines
338
+ if not lines:
339
+ lines = content.split("\n")
340
+
341
+ # Use landscape if spool file is wider than 80 characters
342
+ if spool_page_width > 80:
343
+ pagesize = landscape(letter)
344
+ font_size = 6.5 # Smaller to fit 132 chars
345
+ else:
346
+ pagesize = letter
347
+ font_size = 10
348
+
349
+ c = canvas.Canvas(file_path, pagesize=pagesize)
350
+ width, height = pagesize
351
+
352
+ # Calculate margins
353
+ left_margin = 0.3 * inch
354
+ top_margin = 0.3 * inch
355
+ bottom_margin = 0.3 * inch
356
+ usable_height = height - top_margin - bottom_margin
357
+
358
+ # Calculate line height to fit spool_page_length lines on the page
359
+ line_height = usable_height / spool_page_length
360
+
361
+ # Starting y position (from top of usable area)
362
+ start_y = height - top_margin - font_size
363
+
364
+ c.setFont("Courier", font_size)
365
+ y = start_y
366
+ line_on_page = 0
367
+
368
+ for line in lines:
369
+ # Check if we've reached the spool file's page length
370
+ if line_on_page >= spool_page_length:
371
+ c.showPage()
372
+ c.setFont("Courier", font_size)
373
+ y = start_y
374
+ line_on_page = 0
375
+
376
+ # Truncate long lines to page width
377
+ if len(line) > spool_page_width:
378
+ line = line[:spool_page_width]
379
+
380
+ c.drawString(left_margin, y, line)
381
+ y -= line_height
382
+ line_on_page += 1
383
+
384
+ c.save()
385
+ return True
386
+
387
+ def _save_pdf(self):
388
+ """Save the current viewer content as PDF."""
389
+ content = self.viewer_text.get("1.0", tk.END).strip()
390
+ if not content:
391
+ messagebox.showwarning("Empty", "No spool file content to save.")
392
+ return
393
+
394
+ # Generate filename from job name and file number
395
+ if self._current_spool_info:
396
+ job_name = self._current_spool_info.get("job_name", "spool")
397
+ file_number = self._current_spool_info.get("file_number", "0")
398
+ default_name = f"{job_name}_{file_number}.pdf"
399
+ else:
400
+ default_name = "spoolfile.pdf"
401
+
402
+ file_path = filedialog.asksaveasfilename(
403
+ defaultextension=".pdf",
404
+ filetypes=[("PDF files", "*.pdf"), ("All files", "*.*")],
405
+ initialfile=default_name
406
+ )
407
+
408
+ if not file_path:
409
+ return
410
+
411
+ try:
412
+ self._generate_pdf(content, file_path)
413
+ self.app.statusbar.config(text=f"Saved PDF: {file_path}")
414
+ if messagebox.askyesno("Saved", f"PDF saved to:\n{file_path}\n\nOpen the PDF now?"):
415
+ self._open_file(file_path)
416
+ except ImportError:
417
+ messagebox.showerror("Error", "reportlab not installed. Run: pip install reportlab")
418
+ except Exception as e:
419
+ messagebox.showerror("Error", f"Could not save PDF: {e}")
420
+
421
+ def _open_file(self, file_path):
422
+ """Open file with system default application."""
423
+ try:
424
+ system = platform.system()
425
+ if system == "Darwin":
426
+ subprocess.run(["open", file_path], check=True)
427
+ elif system == "Windows":
428
+ import os
429
+ os.startfile(file_path)
430
+ else: # Linux and others
431
+ subprocess.run(["xdg-open", file_path], check=True)
432
+ except Exception as e:
433
+ messagebox.showerror("Error", f"Could not open file: {e}")
434
+
435
+ def _update_viewer_buttons(self):
436
+ """Enable/disable viewer buttons based on content."""
437
+ content = self.viewer_text.get("1.0", tk.END).strip()
438
+ state = tk.NORMAL if content else tk.DISABLED
439
+ self.save_pdf_btn.config(state=state)
440
+ self.print_btn.config(state=state)
441
+
442
+ def _print_spool(self):
443
+ """Print the current viewer content."""
444
+ content = self.viewer_text.get("1.0", tk.END).strip()
445
+ if not content:
446
+ messagebox.showwarning("Empty", "No spool file content to print.")
447
+ return
448
+
449
+ # Show print dialog
450
+ self._show_print_dialog(content)
451
+
452
+ def _get_printers(self):
453
+ """Get list of available printers."""
454
+ printers = []
455
+ default = None
456
+ system = platform.system()
457
+ try:
458
+ if system in ("Linux", "Darwin"):
459
+ # Use lpstat to get printer list
460
+ result = subprocess.run(["lpstat", "-a"], capture_output=True, text=True)
461
+ if result.returncode == 0:
462
+ for line in result.stdout.strip().split("\n"):
463
+ if line:
464
+ # Format: "printer_name accepting requests..."
465
+ printer = line.split()[0]
466
+ printers.append(printer)
467
+ # Get default printer
468
+ result = subprocess.run(["lpstat", "-d"], capture_output=True, text=True)
469
+ if result.returncode == 0 and ":" in result.stdout:
470
+ default = result.stdout.split(":")[1].strip()
471
+ elif system == "Windows":
472
+ # Use PowerShell to get printers (more reliable than wmic)
473
+ result = subprocess.run(
474
+ ["powershell", "-Command", "Get-Printer | Select-Object -ExpandProperty Name"],
475
+ capture_output=True, text=True, creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, 'CREATE_NO_WINDOW') else 0
476
+ )
477
+ if result.returncode == 0:
478
+ for line in result.stdout.strip().split("\n"):
479
+ if line.strip():
480
+ printers.append(line.strip())
481
+ # Get default printer
482
+ result = subprocess.run(
483
+ ["powershell", "-Command", "(Get-WmiObject -Query \"SELECT * FROM Win32_Printer WHERE Default=$true\").Name"],
484
+ capture_output=True, text=True, creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, 'CREATE_NO_WINDOW') else 0
485
+ )
486
+ if result.returncode == 0 and result.stdout.strip():
487
+ default = result.stdout.strip()
488
+ except Exception:
489
+ pass
490
+ return printers, default
491
+
492
+ def _show_print_dialog(self, content):
493
+ """Show a print dialog to select printer."""
494
+ printers, default = self._get_printers()
495
+
496
+ dialog = tk.Toplevel(self.frame)
497
+ dialog.title("Print")
498
+ dialog.geometry("350x200")
499
+ dialog.transient(self.frame.winfo_toplevel())
500
+ dialog.grab_set()
501
+
502
+ # Printer selection
503
+ ttk.Label(dialog, text="Select Printer:").pack(anchor=tk.W, padx=10, pady=(10, 5))
504
+
505
+ printer_var = tk.StringVar()
506
+ if printers:
507
+ printer_combo = ttk.Combobox(dialog, textvariable=printer_var, values=printers, state="readonly", width=40)
508
+ printer_combo.pack(padx=10, pady=5)
509
+ if default and default in printers:
510
+ printer_combo.set(default)
511
+ elif printers:
512
+ printer_combo.set(printers[0])
513
+ else:
514
+ ttk.Label(dialog, text="No printers found. Using system default.").pack(padx=10, pady=5)
515
+
516
+ # Copies
517
+ copies_frame = ttk.Frame(dialog)
518
+ copies_frame.pack(fill=tk.X, padx=10, pady=10)
519
+ ttk.Label(copies_frame, text="Copies:").pack(side=tk.LEFT)
520
+ copies_var = tk.StringVar(value="1")
521
+ copies_spin = ttk.Spinbox(copies_frame, from_=1, to=99, width=5, textvariable=copies_var)
522
+ copies_spin.pack(side=tk.LEFT, padx=5)
523
+
524
+ # Buttons
525
+ btn_frame = ttk.Frame(dialog)
526
+ btn_frame.pack(fill=tk.X, padx=10, pady=20)
527
+
528
+ def do_print():
529
+ dialog.destroy()
530
+ self._send_to_printer(content, printer_var.get() if printers else None, int(copies_var.get()))
531
+
532
+ ttk.Button(btn_frame, text="Print", command=do_print).pack(side=tk.RIGHT, padx=5)
533
+ ttk.Button(btn_frame, text="Cancel", command=dialog.destroy).pack(side=tk.RIGHT, padx=5)
534
+
535
+ def _send_to_printer(self, content, printer, copies):
536
+ """Send content to the specified printer by generating a PDF first."""
537
+ try:
538
+ # Generate PDF to temp file
539
+ temp_path = tempfile.mktemp(suffix='.pdf')
540
+ self._generate_pdf(content, temp_path)
541
+
542
+ system = platform.system()
543
+ if system in ("Linux", "Darwin"):
544
+ cmd = ["lp", "-n", str(copies)]
545
+ if printer:
546
+ cmd.extend(["-d", printer])
547
+ cmd.append(temp_path)
548
+ subprocess.run(cmd, check=True)
549
+ self.app.statusbar.config(text=f"Sent to printer: {printer or 'default'}")
550
+ elif system == "Windows":
551
+ import os
552
+ # Print PDF using default handler
553
+ for _ in range(copies):
554
+ os.startfile(temp_path, "print")
555
+ self.app.statusbar.config(text=f"Sent to printer: {printer or 'default'}")
556
+ except ImportError:
557
+ messagebox.showerror("Error", "reportlab not installed. Run: pip install reportlab")
558
+ except FileNotFoundError:
559
+ messagebox.showerror("Error", "Print command not found.\nEnsure CUPS (Linux/Mac) is available.")
560
+ except subprocess.CalledProcessError as e:
561
+ messagebox.showerror("Error", f"Print failed: {e}")
562
+ except Exception as e:
563
+ messagebox.showerror("Error", f"Could not print: {e}")
564
+
565
+ def get_user(self):
566
+ """Get current user filter."""
567
+ return self.user_entry.get().strip()
568
+
569
+ def set_user(self, user):
570
+ """Set user filter."""
571
+ self.user_entry.delete(0, tk.END)
572
+ self.user_entry.insert(0, user)