epson_print_conf/ui.py

1221 lines
43 KiB
Python
Raw Normal View History

2024-09-18 22:49:14 -04:00
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Epson Printer Configuration via SNMP (TCP/IP) - GUI
"""
2024-08-08 20:41:19 -04:00
import sys
2024-07-27 07:53:03 -04:00
import re
import threading
import ipaddress
2024-07-28 13:15:10 -04:00
import inspect
2024-07-27 07:53:03 -04:00
from datetime import datetime
2024-09-18 22:49:14 -04:00
import socket
2024-09-20 02:35:28 -04:00
import traceback
2024-07-27 07:53:03 -04:00
2024-09-18 22:49:14 -04:00
import black
import tkinter as tk
2024-07-25 16:08:05 -04:00
from tkinter import ttk, Menu
from tkinter.scrolledtext import ScrolledText
2024-07-25 16:08:05 -04:00
import tkinter.font as tkfont
2024-07-27 07:53:03 -04:00
from tkcalendar import DateEntry # Ensure you have: pip install tkcalendar
2024-07-27 10:36:43 -04:00
from tkinter import messagebox
2024-07-25 16:08:05 -04:00
2024-07-27 10:36:43 -04:00
import pyperclip
2024-07-27 07:53:03 -04:00
from epson_print_conf import EpsonPrinter
from find_printers import PrinterScanner
2024-09-18 22:49:14 -04:00
VERSION = "2.2"
2024-07-28 13:15:10 -04:00
NO_CONF_ERROR = (
2024-09-18 22:49:14 -04:00
"[ERROR] Please select a printer model and a valid IP address,"
" or press 'Detect Printers'.\n"
2024-07-28 13:15:10 -04:00
)
def get_printer_models(input_string):
# Tokenize the string
tokens = re.split(" |/", input_string)
if not len(tokens):
return []
# Define the words to remove (uppercase, then case insensitive)
remove_tokens = {"EPSON", "SERIES"}
# Process tokens
processed_tokens = []
non_numeric_part = ""
pre_model = ""
for token in tokens:
upper_token = token.upper()
# Remove tokens that match remove_tokens
if any(word == upper_token for word in remove_tokens):
continue
if not any(char.isdigit() for char in token): # no alphanum inside
pre_model = pre_model + token + " "
continue
# Identify the non-numeric part of the first token
if not token.isnumeric() and not non_numeric_part:
non_numeric_part = "".join(c for c in token if not c.isdigit())
# if token is numeric, prepend the non-numeric part
if token.isnumeric():
processed_tokens.append(f"{pre_model}{non_numeric_part}{token}")
else:
processed_tokens.append(f"{pre_model}{token}")
if not processed_tokens and pre_model:
processed_tokens.append(pre_model.strip())
return processed_tokens
2024-07-27 10:36:43 -04:00
class ToolTip:
2024-09-18 22:49:14 -04:00
def __init__(
self,
widget,
text="widget info",
wrap_length=10,
destroy=True
):
2024-07-27 10:36:43 -04:00
self.widget = widget
self.text = text
self.wrap_length = wrap_length
self.tooltip_window = None
2024-09-18 22:49:14 -04:00
# Check and remove existing bindings if they exist
if destroy:
self.remove_existing_binding("<Enter>")
self.remove_existing_binding("<Leave>")
self.remove_existing_binding("<Button-1>")
# Set new bindings
2024-07-28 13:15:10 -04:00
widget.bind("<Enter>", self.enter, "+") # Show the tooltip on hover
widget.bind("<Leave>", self.leave, "+") # Hide the tooltip on leave
widget.bind("<Button-1>", self.leave, "+") # Hide tooltip on mouse click
2024-09-18 22:49:14 -04:00
def remove_existing_binding(self, event):
# Check if there's already a binding for the event
if self.widget.bind(event):
self.widget.unbind(event) # Remove the existing binding
2024-07-27 10:36:43 -04:00
def enter(self, event=None):
if self.tooltip_window or not self.text:
return
x, y, width, height = self.widget.bbox("insert")
x += self.widget.winfo_rootx() + 20
y += self.widget.winfo_rooty() + 20
self.tooltip_window = tw = tk.Toplevel(self.widget)
tw.wm_overrideredirect(True)
2024-07-28 13:15:10 -04:00
2024-07-27 10:36:43 -04:00
# Calculate the position for the tooltip
screen_width = self.widget.winfo_screenwidth()
screen_height = self.widget.winfo_screenheight()
2024-07-28 13:15:10 -04:00
2024-07-27 10:36:43 -04:00
tw.geometry(f"+{x}+{y + height + 2}") # Default position below the widget
2024-07-28 13:15:10 -04:00
label = tk.Label(
tw,
text=self.wrap_text(self.text),
justify="left",
background="LightYellow",
relief="solid",
borderwidth=1,
)
2024-07-27 10:36:43 -04:00
label.pack(ipadx=1)
# Check if the tooltip goes off the screen
tw.update_idletasks() # Ensures the tooltip size is calculated
tw_width = tw.winfo_width()
tw_height = tw.winfo_height()
2024-07-28 13:15:10 -04:00
2024-07-27 10:36:43 -04:00
if x + tw_width > screen_width: # If tooltip goes beyond screen width
x = screen_width - tw_width - 5
2024-07-28 13:15:10 -04:00
if (y + height + tw_height > screen_height): # If tooltip goes below screen height
2024-07-27 10:36:43 -04:00
y = y - tw_height - height - 2 # Position above the widget
tw.geometry(f"+{x}+{y}")
def leave(self, event=None):
2024-07-28 13:15:10 -04:00
if self.tooltip_window:
self.tooltip_window.destroy()
self.tooltip_window = None
2024-07-27 10:36:43 -04:00
def wrap_text(self, text):
words = text.split()
lines = []
current_line = []
for word in words:
if len(current_line) + len(word.split()) <= self.wrap_length:
current_line.append(word)
else:
2024-07-28 13:15:10 -04:00
lines.append(" ".join(current_line))
2024-07-27 10:36:43 -04:00
current_line = [word]
if current_line:
2024-07-28 13:15:10 -04:00
lines.append(" ".join(current_line))
return "\n".join(lines)
2024-07-27 10:36:43 -04:00
2024-08-08 20:41:19 -04:00
class BugFixedDateEntry(DateEntry):
"""
Fixes a bug on the calendar that does not accept mouse selection with Linux
2024-09-18 22:49:14 -04:00
Fixes a drop down bug when the DateEntry widget is not focused
2024-08-08 20:41:19 -04:00
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def drop_down(self):
2024-09-18 22:49:14 -04:00
self.focus_set() # Set focus to the DateEntry widget
2024-08-08 20:41:19 -04:00
super().drop_down()
if self._top_cal is not None and not self._calendar.winfo_ismapped():
self._top_cal.lift()
class EpsonPrinterUI(tk.Tk):
2024-09-18 22:49:14 -04:00
def __init__(
self,
model: str = None,
hostname: str = None,
conf_dict={},
replace_conf=False
):
super().__init__()
2024-07-27 07:53:03 -04:00
self.title("Epson Printer Configuration - v" + VERSION)
2024-08-07 20:32:03 -04:00
self.geometry("500x500")
self.minsize(500, 500)
2024-07-28 13:15:10 -04:00
self.printer_scanner = PrinterScanner()
2024-07-27 10:36:43 -04:00
self.ip_list = []
self.ip_list_cycle = None
2024-07-28 21:29:30 -04:00
self.conf_dict = conf_dict
self.replace_conf = replace_conf
2024-09-18 22:49:14 -04:00
self.text_dump = ""
self.mode = black.Mode(line_length=200, magic_trailing_comma=False)
self.printer = None
2024-07-26 22:01:38 -04:00
# configure the main window to be resizable
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
2024-07-27 07:53:03 -04:00
FRAME_PAD = 10
2024-07-28 13:15:10 -04:00
PAD = (3, 0)
PADX = 4
2024-07-27 07:53:03 -04:00
PADY = 5
2024-07-28 13:15:10 -04:00
# main Frame
2024-07-27 07:53:03 -04:00
main_frame = ttk.Frame(self, padding=FRAME_PAD)
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
main_frame.columnconfigure(0, weight=1)
2024-07-27 10:36:43 -04:00
main_frame.rowconfigure(3, weight=1) # Number of rows
row_n = 0
# [row 0] Container frame for the two LabelFrames Power-off timer and TI Received Time
model_ip_frame = ttk.Frame(main_frame, padding=PAD)
model_ip_frame.grid(row=row_n, column=0, pady=PADY, sticky=(tk.W, tk.E))
model_ip_frame.columnconfigure(0, weight=1) # Allow column to expand
model_ip_frame.columnconfigure(1, weight=1) # Allow column to expand
2024-09-18 22:49:14 -04:00
# BOX printer model selection
2024-07-28 13:15:10 -04:00
model_frame = ttk.LabelFrame(
model_ip_frame, text="Printer Model", padding=PAD
)
model_frame.grid(
row=0, column=0, pady=PADY, padx=(0, PADX), sticky=(tk.W, tk.E)
)
2024-07-27 10:36:43 -04:00
model_frame.columnconfigure(0, weight=0)
model_frame.columnconfigure(1, weight=1)
2024-07-28 13:15:10 -04:00
2024-09-18 22:49:14 -04:00
# Model combobox
self.model_var = tk.StringVar()
2024-07-28 21:29:30 -04:00
if (
"internal_data" in conf_dict
and "default_model" in conf_dict["internal_data"]
):
self.model_var.set(conf_dict["internal_data"]["default_model"])
2024-09-18 22:49:14 -04:00
if model:
self.model_var.set(model)
2024-07-28 13:15:10 -04:00
ttk.Label(model_frame, text="Model:").grid(
row=0, column=0, sticky=tk.W, padx=PADX
)
self.model_dropdown = ttk.Combobox(
model_frame, textvariable=self.model_var, state="readonly"
)
2024-07-28 21:29:30 -04:00
self.model_dropdown["values"] = sorted(EpsonPrinter(
conf_dict=self.conf_dict,
replace_conf=self.replace_conf
).valid_printers)
2024-07-28 13:15:10 -04:00
self.model_dropdown.grid(
row=0, column=1, pady=PADY, padx=PADX, sticky=(tk.W, tk.E)
)
ToolTip(
self.model_dropdown,
2024-09-18 22:49:14 -04:00
"Select the model of the printer, or press 'Detect Printers'.\n"
"Press F2 to dump the parameters associated to the printer model.",
2024-07-28 13:15:10 -04:00
)
self.model_dropdown.bind("<F2>", self.printer_config)
2024-07-28 13:15:10 -04:00
2024-09-18 22:49:14 -04:00
# BOX IP address
2024-07-28 13:15:10 -04:00
ip_frame = ttk.LabelFrame(
model_ip_frame, text="Printer IP Address", padding=PAD
)
ip_frame.grid(
row=0, column=1, pady=PADY, padx=(PADX, 0), sticky=(tk.W, tk.E)
)
2024-07-27 10:36:43 -04:00
ip_frame.columnconfigure(0, weight=0)
ip_frame.columnconfigure(1, weight=1)
2024-07-28 13:15:10 -04:00
2024-09-18 22:49:14 -04:00
# IP address entry
self.ip_var = tk.StringVar()
2024-07-28 21:29:30 -04:00
if (
"internal_data" in conf_dict
and "hostname" in conf_dict["internal_data"]
):
self.ip_var.set(conf_dict["internal_data"]["hostname"])
2024-09-18 22:49:14 -04:00
if hostname:
self.ip_var.set(hostname)
2024-07-28 13:15:10 -04:00
ttk.Label(ip_frame, text="IP Address:").grid(
row=0, column=0, sticky=tk.W, padx=PADX
)
self.ip_entry = ttk.Entry(ip_frame, textvariable=self.ip_var)
2024-07-28 13:15:10 -04:00
self.ip_entry.grid(
row=0, column=1, pady=PADY, padx=PADX, sticky=(tk.W, tk.E)
)
2024-07-27 10:36:43 -04:00
self.ip_entry.bind("<F2>", self.next_ip)
2024-07-28 13:15:10 -04:00
ToolTip(
self.ip_entry,
2024-09-18 22:49:14 -04:00
"Enter the IP address, or press 'Detect Printers'"
" (you can also enter part of the IP address"
" to speed up the detection),"
" or press F2 more times to get the next local IP address,"
" which can then be edited"
" (by removing the last part before pressing 'Detect Printers').",
2024-07-28 13:15:10 -04:00
)
2024-07-27 07:53:03 -04:00
2024-07-27 10:36:43 -04:00
# [row 1] Container frame for the two LabelFrames Power-off timer and TI Received Time
row_n += 1
2024-07-27 07:53:03 -04:00
container_frame = ttk.Frame(main_frame, padding=PAD)
2024-07-28 13:15:10 -04:00
container_frame.grid(
row=row_n, column=0, pady=PADY, sticky=(tk.W, tk.E)
)
2024-07-27 07:53:03 -04:00
container_frame.columnconfigure(0, weight=1) # Allow column to expand
container_frame.columnconfigure(1, weight=1) # Allow column to expand
2024-09-18 22:49:14 -04:00
# BOX Power-off timer (minutes)
2024-07-28 13:15:10 -04:00
po_timer_frame = ttk.LabelFrame(
container_frame, text="Power-off timer (minutes)", padding=PAD
)
po_timer_frame.grid(
row=0, column=0, pady=PADY, padx=(0, PADX), sticky=(tk.W, tk.E)
)
2024-07-27 07:53:03 -04:00
po_timer_frame.columnconfigure(0, weight=0) # Button column on the left
po_timer_frame.columnconfigure(1, weight=1) # Entry column
po_timer_frame.columnconfigure(2, weight=0) # Button column on the right
2024-07-28 13:15:10 -04:00
2024-07-27 07:53:03 -04:00
# Configure validation command for numeric entry
validate_cmd = self.register(self.validate_number_input)
2024-09-18 22:49:14 -04:00
# Power-off timer (minutes) - Get Button
button_width = 7
self.get_po_minutes = ttk.Button(
po_timer_frame,
text="Get",
width=button_width,
command=self.get_po_mins,
)
self.get_po_minutes.grid(
row=0, column=0, padx=PADX, pady=PADY, sticky=tk.W
)
# Power-off timer (minutes) - minutes Entry
2024-07-27 07:53:03 -04:00
self.po_timer_var = tk.StringVar()
2024-07-28 13:15:10 -04:00
self.po_timer_entry = ttk.Entry(
po_timer_frame,
textvariable=self.po_timer_var,
validate="all",
validatecommand=(validate_cmd, "%P"),
width=6,
justify="center",
)
self.po_timer_entry.grid(
row=0, column=1, pady=PADY, padx=PADX, sticky=(tk.W, tk.E)
)
2024-09-18 22:49:14 -04:00
ToolTip(
self.po_timer_entry,
"Enter a number of minutes.",
destroy=False
2024-07-28 13:15:10 -04:00
)
2024-07-27 07:53:03 -04:00
2024-09-18 22:49:14 -04:00
# Power-off timer (minutes) - Set Button
self.set_po_minutes = ttk.Button(
2024-07-28 13:15:10 -04:00
po_timer_frame,
text="Set",
width=button_width,
command=self.set_po_mins,
)
2024-09-18 22:49:14 -04:00
self.set_po_minutes.grid(
row=0, column=2, padx=PADX, pady=PADY, sticky=tk.E
)
2024-07-27 07:53:03 -04:00
2024-09-18 22:49:14 -04:00
# BOX TI Received Time (date)
2024-07-28 13:15:10 -04:00
ti_received_frame = ttk.LabelFrame(
container_frame, text="TI Received Time (date)", padding=PAD
)
ti_received_frame.grid(
row=0, column=1, pady=PADY, padx=(PADX, 0), sticky=(tk.W, tk.E)
)
2024-07-27 07:53:03 -04:00
ti_received_frame.columnconfigure(0, weight=0) # Button column on the left
2024-07-27 10:36:43 -04:00
ti_received_frame.columnconfigure(1, weight=1) # Calendar column
2024-07-27 07:53:03 -04:00
ti_received_frame.columnconfigure(2, weight=0) # Button column on the right
2024-09-18 22:49:14 -04:00
# TI Received Time - Get Button
self.get_ti_received = ttk.Button(
ti_received_frame,
text="Get",
width=button_width,
command=self.get_ti_date,
)
self.get_ti_received.grid(
row=0, column=0, padx=PADX, pady=PADY, sticky=tk.W
)
# TI Received Time - Calendar Widget
2024-08-08 20:41:19 -04:00
self.date_entry = BugFixedDateEntry(
2024-07-28 13:15:10 -04:00
ti_received_frame, date_pattern="yyyy-mm-dd"
)
self.date_entry.grid(
row=0, column=1, padx=PADX, pady=PADY, sticky=(tk.W, tk.E)
)
2024-08-08 20:41:19 -04:00
self.date_entry.delete(0, "end") # blank the field removing the current date
2024-09-18 22:49:14 -04:00
ToolTip(
self.date_entry,
"Enter a valid date with format YYYY-MM-DD.",
destroy=False
2024-07-28 13:15:10 -04:00
)
2024-07-27 07:53:03 -04:00
2024-09-18 22:49:14 -04:00
# TI Received Time - Set Button
self.set_ti_received = ttk.Button(
2024-07-28 13:15:10 -04:00
ti_received_frame,
text="Set",
width=button_width,
command=self.set_ti_date,
)
2024-09-18 22:49:14 -04:00
self.set_ti_received.grid(
row=0, column=2, padx=PADX, pady=PADY, sticky=tk.E
)
2024-07-27 07:53:03 -04:00
2024-07-27 10:36:43 -04:00
# [row 2] Buttons
row_n += 1
2024-07-27 07:53:03 -04:00
button_frame = ttk.Frame(main_frame, padding=PAD)
2024-07-27 10:36:43 -04:00
button_frame.grid(row=row_n, column=0, pady=PADY, sticky=(tk.W, tk.E))
button_frame.columnconfigure((0, 1, 2), weight=1)
2024-07-28 13:15:10 -04:00
2024-09-18 22:49:14 -04:00
# Detect Printers
2024-07-28 13:15:10 -04:00
self.detect_button = ttk.Button(
button_frame,
text="Detect Printers",
command=self.start_detect_printers,
)
self.detect_button.grid(
row=0, column=0, padx=PADX, pady=PADX, sticky=(tk.W, tk.E)
)
2024-09-18 22:49:14 -04:00
# Printer Status
2024-07-28 13:15:10 -04:00
self.status_button = ttk.Button(
button_frame, text="Printer Status", command=self.printer_status
)
self.status_button.grid(
row=0, column=1, padx=PADX, pady=PADY, sticky=(tk.W, tk.E)
)
2024-09-18 22:49:14 -04:00
# Reset Waste Ink Levels
2024-07-28 13:15:10 -04:00
self.reset_button = ttk.Button(
button_frame,
text="Reset Waste Ink Levels",
command=self.reset_waste_ink,
)
self.reset_button.grid(
row=0, column=2, padx=PADX, pady=PADX, sticky=(tk.W, tk.E)
)
2024-07-27 07:53:03 -04:00
2024-09-18 22:49:14 -04:00
# [row 3] Status display (including ScrolledText and Treeview)
2024-07-27 10:36:43 -04:00
row_n += 1
2024-07-27 07:53:03 -04:00
status_frame = ttk.LabelFrame(main_frame, text="Status", padding=PAD)
2024-07-28 13:15:10 -04:00
status_frame.grid(
row=row_n, column=0, pady=PADY, sticky=(tk.W, tk.E, tk.N, tk.S)
)
status_frame.columnconfigure(0, weight=1)
status_frame.rowconfigure(0, weight=1)
2024-07-28 13:15:10 -04:00
2024-07-25 16:08:05 -04:00
# ScrolledText widget
2024-07-28 13:15:10 -04:00
self.status_text = ScrolledText(
status_frame, wrap=tk.WORD, font=("TkDefaultFont")
)
self.status_text.grid(
row=0,
column=0,
pady=PADY,
padx=PADY,
sticky=(tk.W, tk.E, tk.N, tk.S),
)
2024-09-18 22:49:14 -04:00
self.status_text.bind("<Tab>", self.focus_next)
2024-07-27 07:53:03 -04:00
self.status_text.bind("<Key>", lambda e: "break") # disable editing text
2024-07-28 13:15:10 -04:00
self.status_text.bind(
"<Control-c>",
lambda event: self.copy_to_clipboard(self.status_text),
)
2024-07-27 07:53:03 -04:00
# self.status_text.bind("<Button-1>", lambda e: "break") # also disable the mouse
2024-07-25 16:08:05 -04:00
2024-07-27 07:53:03 -04:00
# Create a frame to contain the Treeview and its scrollbar
self.tree_frame = tk.Frame(status_frame)
self.tree_frame.grid(column=0, row=0, sticky=(tk.W, tk.E, tk.N, tk.S))
self.tree_frame.columnconfigure(0, weight=1)
self.tree_frame.rowconfigure(0, weight=1)
# Style configuration for the treeview
2024-07-25 16:08:05 -04:00
style = ttk.Style(self)
2024-07-27 07:53:03 -04:00
treeview_font = style.lookup("Treeview.Heading", "font")
2024-07-25 16:08:05 -04:00
2024-07-27 07:53:03 -04:00
# For the treeview, if the treeview_font is a tuple, split into components
if isinstance(treeview_font, tuple):
2024-07-28 13:15:10 -04:00
treeview_font_name, treeview_font_size = (
treeview_font[0],
treeview_font[1],
)
2024-07-25 16:08:05 -04:00
else:
# If font is not a tuple, it might be a font string or other format.
2024-07-28 13:15:10 -04:00
treeview_font_name, treeview_font_size = tkfont.Font().actual(
"family"
), tkfont.Font().actual("size")
style.configure(
"Treeview.Heading",
font=(treeview_font_name, treeview_font_size - 4, "bold"),
background="lightblue",
foreground="darkblue",
)
2024-07-25 16:08:05 -04:00
# Create and configure the Treeview widget
2024-07-27 07:53:03 -04:00
self.tree = ttk.Treeview(self.tree_frame, style="Treeview")
2024-07-25 16:08:05 -04:00
self.tree.grid(column=0, row=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# Create a vertical scrollbar for the Treeview
2024-07-28 13:15:10 -04:00
tree_scrollbar = ttk.Scrollbar(
self.tree_frame, orient="vertical", command=self.tree.yview
)
2024-07-25 16:08:05 -04:00
tree_scrollbar.grid(column=1, row=0, sticky=(tk.N, tk.S))
# Configure the Treeview to use the scrollbar
self.tree.configure(yscrollcommand=tree_scrollbar.set)
# Create a context menu
self.context_menu = Menu(self, tearoff=0)
2024-07-28 13:15:10 -04:00
self.context_menu.add_command(
2024-09-18 22:49:14 -04:00
label="Copy this item", command=self.copy_selected_item
)
self.context_menu.add_command(
label="Copy all items", command=self.copy_all_items
)
self.context_menu.add_command(
label="Print all items", command=self.print_items
2024-07-28 13:15:10 -04:00
)
2024-07-25 16:08:05 -04:00
# Bind the right-click event to the Treeview
self.tree.bind("<Button-3>", self.show_context_menu)
2024-07-27 07:53:03 -04:00
# Hide the Treeview initially
self.tree_frame.grid_remove()
2024-09-18 22:49:14 -04:00
self.model_var.trace('w', self.change_widget_states)
self.ip_var.trace('w', self.change_widget_states)
self.change_widget_states()
def focus_next(self, event):
event.widget.tk_focusNext().focus()
return("break")
def change_widget_states(self, index=None, value=None, op=None):
if self.ip_var.get():
self.status_button.state(["!disabled"])
self.printer = None
else:
self.status_button.state(["disabled"])
if self.ip_var.get() and self.model_var.get():
self.printer = EpsonPrinter(
conf_dict=self.conf_dict,
replace_conf=self.replace_conf,
model=self.model_var.get(),
hostname=self.ip_var.get()
)
if not self.printer:
return
if not self.printer.parm:
self.reset_printer_model()
return
if self.printer.parm.get("stats", {}).get("Power off timer"):
self.po_timer_entry.state(["!disabled"])
self.get_po_minutes.state(["!disabled"])
self.set_po_minutes.state(["!disabled"])
ToolTip(self.get_po_minutes, "")
else:
self.po_timer_entry.state(["disabled"])
self.get_po_minutes.state(["disabled"])
self.set_po_minutes.state(["disabled"])
ToolTip(
self.get_po_minutes,
"Feature not defined in the printer configuration."
)
if self.printer.parm.get("stats", {}).get("First TI received time"):
self.date_entry.state(["!disabled"])
self.get_ti_received.state(["!disabled"])
self.set_ti_received.state(["!disabled"])
ToolTip(self.get_ti_received, "")
else:
self.date_entry.state(["disabled"])
self.get_ti_received.state(["disabled"])
self.set_ti_received.state(["disabled"])
ToolTip(
self.get_ti_received,
"Feature not defined in the printer configuration."
)
if self.printer.reset_waste_ink_levels(dry_run=True):
self.reset_button.state(["!disabled"])
ToolTip(
self.reset_button,
"Ensure you really want this before pressing this key."
)
else:
self.reset_button.state(["disabled"])
ToolTip(
self.reset_button,
"Feature not defined in the printer configuration."
)
else:
self.po_timer_entry.state(["disabled"])
self.get_po_minutes.state(["disabled"])
self.set_po_minutes.state(["disabled"])
self.date_entry.state(["disabled"])
self.get_ti_received.state(["disabled"])
self.set_ti_received.state(["disabled"])
self.reset_button.state(["disabled"])
self.update_idletasks()
2024-07-27 10:36:43 -04:00
def next_ip(self, event):
ip = self.ip_var.get()
if self.ip_list_cycle == None:
self.ip_list = self.printer_scanner.get_all_printers(local=True)
self.ip_list_cycle = 0
if not self.ip_list:
return
self.ip_var.set(self.ip_list[self.ip_list_cycle])
self.ip_list_cycle += 1
if self.ip_list_cycle >= len(self.ip_list):
self.ip_list_cycle = None
def copy_to_clipboard(self, text_widget):
try:
text = text_widget.selection_get()
pyperclip.copy(text)
except tk.TclError:
pass
return "break"
2024-09-20 02:35:28 -04:00
def handle_printer_error(self, e):
self.show_status_text_view()
if isinstance(e, TimeoutError):
self.status_text.insert(
2024-09-20 04:47:35 -04:00
tk.END, f"[ERROR] printer is unreachable or offline.\n"
2024-09-20 02:35:28 -04:00
)
else:
self.status_text.insert(
2024-09-20 04:47:35 -04:00
tk.END, f"[ERROR] {e}\n{traceback.format_exc()}\n"
2024-09-20 02:35:28 -04:00
)
2024-07-28 13:15:10 -04:00
def get_po_mins(self, cursor=True):
if cursor:
self.config(cursor="watch")
self.update()
current_function_name = inspect.stack()[0][3]
method_to_call = getattr(self, current_function_name)
self.after(100, lambda: method_to_call(cursor=False))
return
2024-07-27 07:53:03 -04:00
self.show_status_text_view()
ip_address = self.ip_var.get()
2024-09-18 22:49:14 -04:00
if not self._is_valid_ip(ip_address):
2024-07-28 13:15:10 -04:00
self.status_text.insert(tk.END, NO_CONF_ERROR)
self.config(cursor="")
self.update()
2024-07-27 07:53:03 -04:00
return
2024-09-18 22:49:14 -04:00
if not self.printer:
return
if not self.printer.parm.get("stats", {}).get("Power off timer"):
2024-08-08 20:41:19 -04:00
self.status_text.insert(
tk.END,
f"[ERROR]: Missing 'Power off timer' in configuration\n",
)
self.config(cursor="")
self.update_idletasks()
return
2024-07-27 07:53:03 -04:00
try:
2024-09-18 22:49:14 -04:00
po_timer = self.printer.stats()["stats"]["Power off timer"]
2024-07-28 13:15:10 -04:00
self.status_text.insert(
tk.END, f"[INFO] Power off timer: {po_timer} minutes.\n"
)
2024-07-27 07:53:03 -04:00
self.po_timer_var.set(po_timer)
except Exception as e:
2024-09-20 02:35:28 -04:00
self.handle_printer_error(e)
2024-07-28 13:15:10 -04:00
finally:
self.config(cursor="")
self.update_idletasks()
def set_po_mins(self, cursor=True):
if cursor:
self.config(cursor="watch")
self.update()
current_function_name = inspect.stack()[0][3]
method_to_call = getattr(self, current_function_name)
self.after(100, lambda: method_to_call(cursor=False))
return
2024-07-27 07:53:03 -04:00
self.show_status_text_view()
ip_address = self.ip_var.get()
2024-09-18 22:49:14 -04:00
if not self._is_valid_ip(ip_address):
2024-07-28 13:15:10 -04:00
self.status_text.insert(tk.END, NO_CONF_ERROR)
self.config(cursor="")
self.update_idletasks()
2024-07-27 07:53:03 -04:00
return
2024-09-18 22:49:14 -04:00
if not self.printer:
return
if not self.printer.parm.get("stats", {}).get("Power off timer"):
2024-08-08 20:41:19 -04:00
self.status_text.insert(
tk.END,
f"[ERROR]: Missing 'Power off timer' in configuration\n",
)
2024-07-28 13:15:10 -04:00
self.config(cursor="")
self.update_idletasks()
2024-08-08 20:41:19 -04:00
return
po_timer = self.po_timer_var.get()
self.config(cursor="")
self.update_idletasks()
if not po_timer.isnumeric():
2024-07-28 13:15:10 -04:00
self.status_text.insert(
2024-08-08 20:41:19 -04:00
tk.END, "[ERROR] Please Use a valid value for minutes.\n"
2024-07-28 13:15:10 -04:00
)
2024-08-08 20:41:19 -04:00
return
self.status_text.insert(
tk.END, f"[INFO] Set Power off timer: {po_timer} minutes.\n"
)
response = messagebox.askyesno(
"Confirm Action", "Are you sure you want to proceed?"
)
if response:
try:
2024-09-18 22:49:14 -04:00
self.printer.write_poweroff_timer(int(po_timer))
2024-08-08 20:41:19 -04:00
except Exception as e:
2024-09-20 02:35:28 -04:00
self.handle_printer_error(e)
2024-08-08 20:41:19 -04:00
else:
2024-07-28 13:15:10 -04:00
self.status_text.insert(
2024-08-08 20:41:19 -04:00
tk.END, f"[WARNING] Set Power off timer aborted.\n"
2024-07-28 13:15:10 -04:00
)
2024-08-08 20:41:19 -04:00
self.config(cursor="")
self.update_idletasks()
2024-07-28 13:15:10 -04:00
def get_ti_date(self, cursor=True):
if cursor:
self.config(cursor="watch")
self.update()
current_function_name = inspect.stack()[0][3]
method_to_call = getattr(self, current_function_name)
self.after(100, lambda: method_to_call(cursor=False))
return
2024-07-27 07:53:03 -04:00
self.show_status_text_view()
ip_address = self.ip_var.get()
2024-09-18 22:49:14 -04:00
if not self._is_valid_ip(ip_address):
2024-07-28 13:15:10 -04:00
self.status_text.insert(tk.END, NO_CONF_ERROR)
self.config(cursor="")
self.update_idletasks()
2024-07-27 07:53:03 -04:00
return
2024-09-18 22:49:14 -04:00
if not self.printer:
return
if not self.printer.parm.get("stats", {}).get("First TI received time"):
2024-08-08 20:41:19 -04:00
self.status_text.insert(
tk.END,
f"[ERROR]: Missing 'First TI received time' in configuration\n",
)
self.config(cursor="")
self.update_idletasks()
return
2024-07-27 07:53:03 -04:00
try:
2024-07-28 13:15:10 -04:00
date_string = datetime.strptime(
2024-09-18 22:49:14 -04:00
self.printer.stats(
)["stats"]["First TI received time"], "%d %b %Y"
2024-07-28 13:15:10 -04:00
).strftime("%Y-%m-%d")
self.status_text.insert(
tk.END,
f"[INFO] First TI received time (YYYY-MM-DD): {date_string}.\n",
)
2024-07-27 07:53:03 -04:00
self.date_entry.set_date(date_string)
except Exception as e:
2024-09-20 02:35:28 -04:00
self.handle_printer_error(e)
2024-07-28 13:15:10 -04:00
finally:
self.config(cursor="")
self.update_idletasks()
def set_ti_date(self, cursor=True):
if cursor:
self.config(cursor="watch")
self.update()
current_function_name = inspect.stack()[0][3]
method_to_call = getattr(self, current_function_name)
self.after(100, lambda: method_to_call(cursor=False))
return
2024-07-27 07:53:03 -04:00
self.show_status_text_view()
ip_address = self.ip_var.get()
2024-09-18 22:49:14 -04:00
if not self._is_valid_ip(ip_address):
2024-07-28 13:15:10 -04:00
self.status_text.insert(tk.END, NO_CONF_ERROR)
self.config(cursor="")
self.update_idletasks()
2024-07-27 07:53:03 -04:00
return
2024-09-18 22:49:14 -04:00
if not self.printer:
return
if not self.printer.parm.get("stats", {}).get("First TI received time"):
2024-07-28 13:15:10 -04:00
self.status_text.insert(
tk.END,
2024-08-08 20:41:19 -04:00
f"[ERROR]: Missing 'First TI received time' in configuration\n",
2024-07-28 13:15:10 -04:00
)
2024-08-08 20:41:19 -04:00
self.config(cursor="")
self.update_idletasks()
return
date_string = self.date_entry.get_date()
self.status_text.insert(
tk.END,
2024-09-18 22:49:14 -04:00
f"[INFO] Set 'First TI received time' (YYYY-MM-DD) to: "
f"{date_string.strftime('%Y-%m-%d')}.\n",
2024-08-08 20:41:19 -04:00
)
response = messagebox.askyesno(
"Confirm Action", "Are you sure you want to proceed?"
)
if response:
try:
2024-09-18 22:49:14 -04:00
self.printer.write_first_ti_received_time(
2024-07-28 13:15:10 -04:00
date_string.year, date_string.month, date_string.day
)
2024-08-08 20:41:19 -04:00
except Exception as e:
2024-09-20 02:35:28 -04:00
self.handle_printer_error(e)
2024-08-08 20:41:19 -04:00
else:
2024-07-28 13:15:10 -04:00
self.status_text.insert(
tk.END,
2024-08-08 20:41:19 -04:00
f"[WARNING] Change of 'First TI received time' aborted.\n",
2024-07-28 13:15:10 -04:00
)
2024-08-08 20:41:19 -04:00
self.config(cursor="")
self.update_idletasks()
2024-07-27 07:53:03 -04:00
def validate_number_input(self, new_value):
# This function will be called with the new input value
if new_value == "" or new_value.isdigit():
return True
else:
return False
2024-07-25 16:08:05 -04:00
def show_status_text_view(self):
"""Show the status frame and hide the Treeview."""
2024-07-27 07:53:03 -04:00
self.tree_frame.grid_remove()
2024-07-25 16:08:05 -04:00
self.status_text.grid()
def show_treeview(self):
"""Show the Treeview and hide the status frame."""
self.status_text.grid_remove()
2024-07-27 07:53:03 -04:00
self.tree_frame.grid()
2024-07-25 16:08:05 -04:00
2024-07-28 13:15:10 -04:00
def printer_status(self, cursor=True):
if cursor:
self.config(cursor="watch")
self.update()
current_function_name = inspect.stack()[0][3]
method_to_call = getattr(self, current_function_name)
self.after(100, lambda: method_to_call(cursor=False))
return
2024-07-25 16:08:05 -04:00
self.show_status_text_view()
model = self.model_var.get()
ip_address = self.ip_var.get()
2024-08-07 20:32:03 -04:00
if not self._is_valid_ip(ip_address):
self.status_text.insert(
tk.END,
2024-09-18 22:49:14 -04:00
"[ERROR] Please enter a valid IP address, or "
"press 'Detect Printers'.\n"
2024-08-07 20:32:03 -04:00
)
2024-07-28 13:15:10 -04:00
self.config(cursor="")
self.update_idletasks()
return
2024-07-28 21:29:30 -04:00
printer = EpsonPrinter(
conf_dict=self.conf_dict,
replace_conf=self.replace_conf,
model=model,
hostname=ip_address
)
2024-09-18 22:49:14 -04:00
if not printer:
return
try:
2024-09-18 22:49:14 -04:00
self.text_dump = black.format_str(
f'"{printer.model}": ' + repr(printer.stats()), mode=self.mode
)
2024-07-25 16:08:05 -04:00
self.show_treeview()
2024-07-28 13:15:10 -04:00
# Configure tags
self.tree.tag_configure("key", foreground="black")
self.tree.tag_configure("key_value", foreground="dark blue")
self.tree.tag_configure("value", foreground="blue")
self.tree.heading("#0", text="Status Information", anchor="w")
2024-07-28 13:15:10 -04:00
2024-07-25 16:08:05 -04:00
# Populate the Treeview
self.tree.delete(*self.tree.get_children())
2024-07-28 13:15:10 -04:00
self.populate_treeview("", self.tree, printer.stats())
2024-07-25 16:08:05 -04:00
# Expand all nodes
self.expand_all(self.tree)
except Exception as e:
2024-09-20 02:35:28 -04:00
self.handle_printer_error(e)
2024-07-28 13:15:10 -04:00
finally:
self.config(cursor="")
self.update_idletasks()
2024-09-18 22:49:14 -04:00
def reset_printer_model(self):
self.show_status_text_view()
if self.model_var.get():
self.status_text.insert(
tk.END,
'[ERROR]: Unknown printer model '
f'"{self.model_var.get()}"\n',
)
else:
self.status_text.insert(
tk.END,
'[ERROR]: Select a valid printer model.\n'
)
self.config(cursor="")
self.update()
self.model_var.set("")
def printer_config(self, cursor=True):
2024-09-18 22:49:14 -04:00
"""
Pressing F2 dumps the printer configuration
"""
model = self.model_var.get()
printer = EpsonPrinter(
conf_dict=self.conf_dict,
replace_conf=self.replace_conf,
model=model
)
2024-09-18 22:49:14 -04:00
if not printer:
return
if not printer.parm:
self.reset_printer_model()
return
try:
2024-09-18 22:49:14 -04:00
self.text_dump = black.format_str(
f'"{printer.model}": ' + repr(printer.parm),
mode=self.mode
)
self.show_treeview()
# Configure tags
self.tree.tag_configure("key", foreground="black")
self.tree.tag_configure("key_value", foreground="dark blue")
self.tree.tag_configure("value", foreground="blue")
self.tree.heading("#0", text="Printer parameters", anchor="w")
# Populate the Treeview
self.tree.delete(*self.tree.get_children())
self.populate_treeview("", self.tree, printer.parm)
# Expand all nodes
self.expand_all(self.tree)
except Exception as e:
2024-09-20 02:35:28 -04:00
self.handle_printer_error(e)
finally:
self.update_idletasks()
2024-07-28 13:15:10 -04:00
def reset_waste_ink(self, cursor=True):
if cursor:
self.config(cursor="watch")
self.update()
current_function_name = inspect.stack()[0][3]
method_to_call = getattr(self, current_function_name)
self.after(100, lambda: method_to_call(cursor=False))
return
2024-07-25 16:08:05 -04:00
self.show_status_text_view()
ip_address = self.ip_var.get()
2024-09-18 22:49:14 -04:00
if not self._is_valid_ip(ip_address):
2024-07-28 13:15:10 -04:00
self.status_text.insert(tk.END, NO_CONF_ERROR)
self.config(cursor="")
self.update_idletasks()
return
2024-08-08 20:41:19 -04:00
response = messagebox.askyesno(
"Confirm Action", "Are you sure you want to proceed?"
)
2024-09-18 22:49:14 -04:00
if not self.printer:
return
2024-08-08 20:41:19 -04:00
if response:
try:
2024-09-18 22:49:14 -04:00
self.printer.reset_waste_ink_levels()
2024-07-28 13:15:10 -04:00
self.status_text.insert(
tk.END,
"[INFO] Waste ink levels have been reset."
" Perform a power cycle of the printer now.\n"
2024-07-28 13:15:10 -04:00
)
2024-08-08 20:41:19 -04:00
except Exception as e:
2024-09-20 02:35:28 -04:00
self.handle_printer_error(e)
2024-08-08 20:41:19 -04:00
else:
self.status_text.insert(
tk.END, f"[WARNING] Waste ink levels reset aborted.\n"
)
self.config(cursor="")
self.update_idletasks()
2024-07-28 13:15:10 -04:00
def start_detect_printers(self):
2024-07-25 16:08:05 -04:00
self.show_status_text_view()
2024-07-28 13:15:10 -04:00
self.status_text.insert(
tk.END, "[INFO] Detecting printers... (this might take a while)\n"
)
# run printer detection in new thread, as it can take a while
2024-07-28 13:15:10 -04:00
threading.Thread(target=self.detect_printers_thread).start()
def detect_printers_thread(self, cursor=True):
if cursor:
self.config(cursor="watch")
self.update()
current_function_name = inspect.stack()[0][3]
method_to_call = getattr(self, current_function_name)
self.after(100, lambda: method_to_call(cursor=False))
return
self.detect_button.config(state=tk.DISABLED) # disable button while processing
2024-07-25 16:08:05 -04:00
self.show_status_text_view()
try:
2024-07-28 13:15:10 -04:00
printers = self.printer_scanner.get_all_printers(
self.ip_var.get().strip()
)
if len(printers) > 0:
2024-07-26 22:01:38 -04:00
if len(printers) == 1:
2024-07-28 13:15:10 -04:00
self.status_text.insert(
tk.END,
2024-09-18 22:49:14 -04:00
f"[INFO] Found printer '{printers[0]['name']}' "
f"at {printers[0]['ip']} "
f"(hostname: {printers[0]['hostname']})\n",
2024-07-28 13:15:10 -04:00
)
self.ip_var.set(printers[0]["ip"])
for model in get_printer_models(printers[0]["name"]):
2024-07-28 21:29:30 -04:00
if model in EpsonPrinter(
conf_dict=self.conf_dict,
replace_conf=self.replace_conf
).valid_printers:
2024-07-26 22:01:38 -04:00
self.model_var.set(model)
break
if self.model_var.get() == "":
self.status_text.insert(
tk.END,
f'[ERROR] Printer model unknown.\n'
)
self.model_var.set("")
2024-07-26 22:01:38 -04:00
else:
2024-07-28 13:15:10 -04:00
self.status_text.insert(
tk.END, f"[INFO] Found {len(printers)} printers:\n"
)
2024-07-26 22:01:38 -04:00
for printer in printers:
2024-07-28 13:15:10 -04:00
self.status_text.insert(
tk.END,
2024-09-18 22:49:14 -04:00
f"[INFO] {printer['name']} found at {printer['ip']}"
f" (hostname: {printer['hostname']})\n",
2024-07-28 13:15:10 -04:00
)
else:
self.status_text.insert(tk.END, "[WARN] No printers found.\n")
except Exception as e:
2024-09-20 02:35:28 -04:00
self.handle_printer_error(e)
finally:
2024-07-28 13:15:10 -04:00
self.detect_button.config(state=tk.NORMAL) # enable button after processing
self.config(cursor="")
self.update_idletasks()
2024-07-28 13:15:10 -04:00
def _is_valid_ip(self, ip):
try:
ip = ipaddress.ip_address(ip)
return True
except ValueError:
return False
2024-07-25 16:08:05 -04:00
def is_simple_type(self, data):
return isinstance(data, (str, int, float, bool))
def contains_parentheses(self, data):
"""Check if a string representation contains parentheses."""
if isinstance(data, (list, tuple, set)):
for item in data:
if isinstance(item, (tuple, list, set)):
return True
2024-07-28 13:15:10 -04:00
if isinstance(item, str) and ("(" in item or ")" in item):
2024-07-25 16:08:05 -04:00
return True
return False
def populate_treeview(self, parent, treeview, data):
if isinstance(data, dict):
for key, value in data.items():
if isinstance(value, (dict, list, set, tuple)):
2024-07-28 13:15:10 -04:00
node = treeview.insert(
parent, "end", text=key, tags=("key",)
)
2024-07-25 16:08:05 -04:00
self.populate_treeview(node, treeview, value)
else:
2024-07-28 13:15:10 -04:00
treeview.insert(
parent,
"end",
text=f"{key}: {value}",
tags=("key_value"),
)
2024-07-25 16:08:05 -04:00
elif isinstance(data, list):
2024-07-28 13:15:10 -04:00
if all(
self.is_simple_type(item) for item in data
) and not self.contains_parentheses(data):
treeview.insert(
parent,
"end",
text=", ".join(map(str, data)),
tags=("value",),
)
2024-07-25 16:08:05 -04:00
else:
for item in data:
if isinstance(item, (dict, list, set, tuple)):
self.populate_treeview(parent, treeview, item)
else:
2024-07-28 13:15:10 -04:00
treeview.insert(
parent, "end", text=str(item), tags=("value",)
)
2024-07-25 16:08:05 -04:00
elif isinstance(data, set):
if not self.contains_parentheses(data):
2024-07-28 13:15:10 -04:00
treeview.insert(
parent,
"end",
text=", ".join(map(str, data)),
tags=("value",),
)
2024-07-25 16:08:05 -04:00
else:
for item in data:
2024-07-28 13:15:10 -04:00
treeview.insert(
parent, "end", text=str(item), tags=("value",)
)
2024-07-25 16:08:05 -04:00
elif isinstance(data, tuple):
2024-07-28 13:15:10 -04:00
treeview.insert(parent, "end", text=str(data), tags=("value",))
2024-07-25 16:08:05 -04:00
else:
2024-07-28 13:15:10 -04:00
treeview.insert(parent, "end", text=str(data), tags=("value",))
2024-07-25 16:08:05 -04:00
def expand_all(self, treeview):
def recursive_expand(item):
treeview.item(item, open=True)
children = treeview.get_children(item)
for child in children:
recursive_expand(child)
root_children = treeview.get_children()
for child in root_children:
recursive_expand(child)
def show_context_menu(self, event):
"""Show the context menu."""
# Select the item under the cursor
item = self.tree.identify_row(event.y)
if item:
self.tree.selection_set(item)
self.context_menu.post(event.x_root, event.y_root)
def copy_selected_item(self):
"""Copy the selected Treeview item text to the clipboard."""
selected_item = self.tree.selection()
if selected_item:
item_text = self.tree.item(selected_item[0], "text")
self.clipboard_clear()
self.clipboard_append(item_text)
2024-09-18 22:49:14 -04:00
def copy_all_items(self):
"""Copy all items to the clipboard."""
self.clipboard_clear()
self.clipboard_append(self.text_dump)
def print_items(self):
exit_packet_mode = bytes([
0x00, 0x00, 0x00, 0x1B, 0x01, 0x40, 0x45, 0x4A, 0x4C, 0x20,
0x31, 0x32, 0x38, 0x34, 0x2E, 0x34, 0x0A, 0x40, 0x45, 0x4A,
0x4C, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0A, 0x00, 0x00, 0x00,
])
2024-09-18 22:49:14 -04:00
"""Print items."""
self.clipboard_append(self.text_dump)
ip_address = self.ip_var.get()
if not self._is_valid_ip(ip_address):
return
# Send the message to the printer
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.connect((ip_address, 9100))
sock.sendall(
exit_packet_mode + b"\x1B\x40" # Initialize printer
+ b"Printer configuration\n"
+ self.text_dump.encode('utf-8')
+ b"\f" # Form feed
)
2024-09-18 22:49:14 -04:00
2024-07-26 22:01:38 -04:00
2024-08-13 12:01:12 -04:00
def main():
2024-07-28 21:29:30 -04:00
import argparse
import pickle
parser = argparse.ArgumentParser(
epilog='epson_print_conf GUI'
)
2024-09-18 22:49:14 -04:00
parser.add_argument(
'-m',
'--model',
dest='model',
action="store",
help='Printer model. Example: -m XP-205',
default=None
)
parser.add_argument(
'-a',
'--address',
dest='hostname',
action="store",
help='Printer host name or IP address. (Example: -a 192.168.1.87)',
default=None
)
2024-07-28 21:29:30 -04:00
parser.add_argument(
'-P',
"--pickle",
dest='pickle',
type=argparse.FileType('rb'),
2024-07-29 19:18:02 -04:00
help="Load a pickle configuration archive saved by parse_devices.py",
2024-07-28 21:29:30 -04:00
default=None,
nargs=1,
metavar='PICKLE_FILE'
)
parser.add_argument(
'-O',
"--override",
dest='override',
action='store_true',
2024-07-29 19:18:02 -04:00
help="Replace the default configuration with the one in the pickle "
"file instead of merging (default is to merge)",
2024-07-28 21:29:30 -04:00
)
args = parser.parse_args()
conf_dict = {}
if args.pickle:
conf_dict = pickle.load(args.pickle[0])
2024-09-18 22:49:14 -04:00
return EpsonPrinterUI(
model=args.model,
hostname=args.hostname,
conf_dict=conf_dict,
replace_conf=args.override
)
2024-08-13 12:01:12 -04:00
if __name__ == "__main__":
try:
main().mainloop()
2024-09-18 22:49:14 -04:00
except KeyboardInterrupt:
2024-08-08 20:41:19 -04:00
print("\nInterrupted.")
sys.exit(0)