#https://ChristosM.net import os import json import datetime import tkinter as tk from tkinter import ttk, messagebox, simpledialog try: import win32print WIN32_AVAILABLE = True except ImportError: WIN32_AVAILABLE = False APP_DIR = os.path.abspath(os.path.dirname(__file__)) OUT_DIR = os.path.join(APP_DIR, "prints") os.makedirs(OUT_DIR, exist_ok=True) SETTINGS_PATH = os.path.join(APP_DIR, "settings.json") DEFAULT_SETTINGS = { "printer_name": None, "width_mm": 80, "design": "Simple" } BOTTOM_FEED_LINES = 6 # Extra feed after each task # ------------------------- # Settings Load/Save # ------------------------- def load_settings(): settings = DEFAULT_SETTINGS.copy() try: if os.path.exists(SETTINGS_PATH): with open(SETTINGS_PATH, "r", encoding="utf-8") as f: data = json.load(f) settings.update(data) except Exception: pass return settings def save_settings(settings): try: with open(SETTINGS_PATH, "w", encoding="utf-8") as f: json.dump(settings, f, indent=4) except Exception as e: messagebox.showwarning("Save settings", f"Could not save settings:\n{e}") # ------------------------- # Printer helpers # ------------------------- def list_printers(): if not WIN32_AVAILABLE: return [] printers = [] try: flags = win32print.PRINTER_ENUM_LOCAL | win32print.PRINTER_ENUM_CONNECTIONS for p in win32print.EnumPrinters(flags): if len(p) >= 3: printers.append(p[2]) except Exception: try: printers = [win32print.GetDefaultPrinter()] except Exception: printers = [] return printers def send_bytes_to_printer(printer_name: str, data: bytes): if not WIN32_AVAILABLE: raise RuntimeError("pywin32 is not available") hPrinter = None try: if not printer_name: printer_name = win32print.GetDefaultPrinter() hPrinter = win32print.OpenPrinter(printer_name) win32print.StartDocPrinter(hPrinter, 1, ("PythonRawPrintJob", None, "RAW")) win32print.StartPagePrinter(hPrinter) win32print.WritePrinter(hPrinter, data) win32print.EndPagePrinter(hPrinter) win32print.EndDocPrinter(hPrinter) finally: if hPrinter: win32print.ClosePrinter(hPrinter) # ------------------------- # ESC/POS Task Builder # ------------------------- def build_design_lines(title: str, contents: str, design: str): contents = contents.replace("\r\n", "\n").replace("\r", "\n") lines = [] if design == "Boxed": width = 30 lines.append("=" * width) lines.append(title.center(width)) lines.append("=" * width) elif design == "Header": lines.append(f"--- {title.upper()} ---") else: # Simple lines.append(f"TASK: {title}") lines.append("-" * max(10, len(title))) if contents.strip() == "": lines.append("(no contents)") else: lines.extend(contents.splitlines()) return lines def build_raw_for_task(title: str, contents: str, design: str): ESC = b'\x1b' GS = b'\x1d' bold_on = ESC + b'\x45' + b'\x01' bold_off = ESC + b'\x45' + b'\x00' cut_full = GS + b'\x56' + b'\x00' lf = b'\n' def enc(s): return s.encode("cp437", errors="replace") lines = build_design_lines(title, contents, design) data = b'' # Bold first line data += bold_on + enc(lines[0]) + lf + bold_off # Rest normal for ln in lines[1:]: data += enc(ln) + lf # Bottom feed data += lf * BOTTOM_FEED_LINES + cut_full return data # ------------------------- # GUI Application # ------------------------- class ThermalTasksApp(tk.Tk): def __init__(self): super().__init__() self.title("Thermal Tasks Printer") self.geometry("900x600") self.settings = load_settings() self.tasks = [] self.create_menu() self.create_widgets() self.refresh_task_list() self.status_write("Ready.\n") def create_menu(self): menubar = tk.Menu(self) file_menu = tk.Menu(menubar, tearoff=0) file_menu.add_command(label="Settings", command=self.open_settings) file_menu.add_separator() file_menu.add_command(label="Exit", command=self.quit) menubar.add_cascade(label="File", menu=file_menu) self.config(menu=menubar) def create_widgets(self): left = ttk.Frame(self) left.pack(side="left", fill="y", padx=8, pady=8) ttk.Label(left, text="Tasks").pack(anchor="w") self.listbox = tk.Listbox(left, width=44, height=28) self.listbox.pack(padx=4, pady=4) self.listbox.bind("", lambda e: self.edit_task()) btn_frame = ttk.Frame(left) btn_frame.pack(fill="x", pady=6) ttk.Button(btn_frame, text="Add Task", command=self.add_task).pack(side="left", padx=3) ttk.Button(btn_frame, text="Edit Selected", command=self.edit_task).pack(side="left", padx=3) ttk.Button(btn_frame, text="Delete Selected", command=self.delete_task).pack(side="left", padx=3) action_frame = ttk.Frame(left) action_frame.pack(fill="x", pady=6) ttk.Button(action_frame, text="Print Selected", command=self.print_selected).pack(side="left", padx=3) ttk.Button(action_frame, text="Print All", command=self.print_all).pack(side="left", padx=3) right = ttk.Frame(self) right.pack(side="left", fill="both", expand=True, padx=8, pady=8) ttk.Label(right, text="Status / Log").pack(anchor="w") self.text_status = tk.Text(right, wrap="word", state="disabled") self.text_status.pack(fill="both", expand=True, padx=4, pady=4) bottom = ttk.Frame(self) bottom.pack(side="bottom", fill="x", padx=8, pady=6) self.printer_label_var = tk.StringVar(value=f"Printer: {self.settings.get('printer_name')}") ttk.Label(bottom, textvariable=self.printer_label_var).pack(side="left") self.design_label_var = tk.StringVar(value=f"Design: {self.settings.get('design')}") ttk.Label(bottom, textvariable=self.design_label_var).pack(side="left", padx=(12,0)) def status_write(self, s: str): self.text_status.configure(state="normal") self.text_status.insert("end", s) self.text_status.see("end") self.text_status.configure(state="disabled") # ------------------------- # Task operations # ------------------------- def add_task(self): dlg = TaskDialog(self, "Add Task") self.wait_window(dlg.top) if dlg.result: title, contents = dlg.result self.tasks.append((title, contents)) self.refresh_task_list() self.status_write(f"Added task: {title}\n") def edit_task(self): sel = self.listbox.curselection() if not sel: messagebox.showinfo("Edit", "Select a task to edit.") return idx = sel[0] title, contents = self.tasks[idx] dlg = TaskDialog(self, "Edit Task", init_title=title, init_contents=contents) self.wait_window(dlg.top) if dlg.result: self.tasks[idx] = dlg.result self.refresh_task_list() self.status_write(f"Edited task #{idx+1}\n") def delete_task(self): sel = self.listbox.curselection() if not sel: messagebox.showinfo("Delete", "Select a task to delete.") return idx = sel[0] t, _ = self.tasks.pop(idx) self.refresh_task_list() self.status_write(f"Deleted task: {t}\n") def refresh_task_list(self): self.listbox.delete(0, "end") for i, (t, _) in enumerate(self.tasks): display = t if len(t) <= 36 else t[:33] + "..." self.listbox.insert("end", f"{i+1}. {display}") # ------------------------- # Printing # ------------------------- def print_selected(self): sel = self.listbox.curselection() if not sel: messagebox.showinfo("Print", "Select a task to print.") return idx = sel[0] self._print_tasks([self.tasks[idx]]) def print_all(self): if not self.tasks: messagebox.showinfo("Print", "No tasks to print.") return self._print_tasks(self.tasks) def _print_tasks(self, task_list): printer_name = self.settings.get("printer_name") design = self.settings.get("design", "Simple") if not printer_name: messagebox.showerror("No Printer", "Please select a printer in Settings.") return self.status_write(f"Printing to {printer_name}...\n") for title, contents in task_list: timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S_%f") try: raw_bytes = build_raw_for_task(title, contents, design=design) if WIN32_AVAILABLE: try: send_bytes_to_printer(printer_name, raw_bytes) self.status_write(f"Printed: {title}\n") except Exception as e: self.status_write(f"Print failed ({e}), saved to file.\n") path = os.path.join(OUT_DIR, f"task_raw_{timestamp}.bin") with open(path, "wb") as f: f.write(raw_bytes) self.status_write(f"Saved: {path}\n") else: path = os.path.join(OUT_DIR, f"task_raw_{timestamp}.bin") with open(path, "wb") as f: f.write(raw_bytes) self.status_write(f"Saved raw print file: {path}\n") except Exception as e: self.status_write(f"Error printing '{title}': {e}\n") messagebox.showinfo("Print", "Sent to printer.") # ------------------------- # Settings # ------------------------- def open_settings(self): dlg = SettingsDialog(self, self.settings) self.wait_window(dlg.top) if getattr(dlg, "saved", False): self.settings = load_settings() self.printer_label_var.set(f"Printer: {self.settings.get('printer_name')}") self.design_label_var.set(f"Design: {self.settings.get('design')}") self.status_write("Settings updated.\n") # ------------------------- # Dialogs # ------------------------- class TaskDialog: def __init__(self, parent, title, init_title="", init_contents=""): self.top = tk.Toplevel(parent) self.top.title(title) self.result = None frm = ttk.Frame(self.top, padding=8) frm.pack(fill="both", expand=True) ttk.Label(frm, text="Task (bold)").pack(anchor="w") self.entry_title = ttk.Entry(frm, width=80) self.entry_title.pack(fill="x", pady=(0,6)) self.entry_title.insert(0, init_title) ttk.Label(frm, text="Contents").pack(anchor="w") self.text_contents = tk.Text(frm, width=80, height=12) self.text_contents.pack(fill="both", expand=True, pady=(0,6)) self.text_contents.insert("1.0", init_contents) btns = ttk.Frame(frm) btns.pack(fill="x") ttk.Button(btns, text="OK", command=self.on_ok).pack(side="right", padx=4) ttk.Button(btns, text="Cancel", command=self.top.destroy).pack(side="right") def on_ok(self): title = self.entry_title.get().strip() contents = self.text_contents.get("1.0", "end").rstrip() if not title: messagebox.showwarning("Validation", "Task title cannot be empty.") return self.result = (title, contents) self.top.destroy() class SettingsDialog: def __init__(self, parent, settings): self.top = tk.Toplevel(parent) self.top.title("Settings") self.saved = False self.settings = settings.copy() frm = ttk.Frame(self.top, padding=8) frm.pack(fill="both", expand=True) ttk.Label(frm, text="Printer (Windows):").grid(row=0, column=0, sticky="w", pady=4) self.printers = list_printers() if not self.printers: self.printers = ["(no printers detected)"] self.printer_var = tk.StringVar(value=self.settings.get("printer_name") or "") self.printer_combo = ttk.Combobox(frm, values=self.printers, textvariable=self.printer_var, width=60) self.printer_combo.grid(row=0, column=1, sticky="w", padx=6, pady=4) ttk.Label(frm, text="Paper width (mm):").grid(row=1, column=0, sticky="w", pady=4) self.width_var = tk.IntVar(value=self.settings.get("width_mm", 80)) ttk.Spinbox(frm, from_=40, to=120, textvariable=self.width_var, width=8).grid(row=1, column=1, sticky="w", padx=6, pady=4) ttk.Label(frm, text="Design:").grid(row=2, column=0, sticky="w", pady=4) self.design_var = tk.StringVar(value=self.settings.get("design", "Simple")) ttk.Combobox(frm, values=["Simple", "Boxed", "Header"], textvariable=self.design_var, width=20).grid(row=2, column=1, sticky="w", padx=6, pady=4) btn_frame = ttk.Frame(frm) btn_frame.grid(row=10, column=0, columnspan=2, pady=(12,0), sticky="e") ttk.Button(btn_frame, text="Save", command=self.save).pack(side="right", padx=6) ttk.Button(btn_frame, text="Cancel", command=self.top.destroy).pack(side="right") ttk.Button(frm, text="Test Print", command=self.test_print).grid(row=11, column=0, columnspan=2, pady=(8,0)) def save(self): new_settings = { "printer_name": self.printer_var.get() or None, "width_mm": int(self.width_var.get()), "design": self.design_var.get() } save_settings(new_settings) self.saved = True self.top.destroy() def test_print(self): sample_title = "SAMPLE TASK" sample_contents = "This is a test print." raw = build_raw_for_task(sample_title, sample_contents, design=self.design_var.get()) printer_name = self.printer_var.get() or None ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") if WIN32_AVAILABLE and printer_name: try: send_bytes_to_printer(printer_name, raw) messagebox.showinfo("Test Print", "Test print sent to printer.") except Exception as e: path = os.path.join(OUT_DIR, f"test_raw_{ts}.bin") with open(path, "wb") as f: f.write(raw) messagebox.showwarning("Test Print Failed", f"Could not send to printer: {e}\nSaved raw bytes to {path}") else: path = os.path.join(OUT_DIR, f"test_raw_{ts}.bin") with open(path, "wb") as f: f.write(raw) messagebox.showinfo("Test Print", f"No printer available. Test file saved:\n{path}") # ------------------------- # Run Application # ------------------------- if __name__ == "__main__": if not os.path.exists(SETTINGS_PATH): save_settings(DEFAULT_SETTINGS) app = ThermalTasksApp() app.mainloop()