#!/usr/bin/env python3 import os import tkinter as tk from tkinter import filedialog, messagebox, scrolledtext import tkinter.font as tkfont import shutil import ctypes import string import configparser # ============================================================ # CONFIG: config = configparser.ConfigParser() config.read(r"C:\ProgramData\Program-Search\config.ini") SEARCH_WINDOW_SIZE = config["General"]["SEARCH_WINDOW_SIZE"] RESULTS_WINDOW_SIZE = config["General"]["RESULTS_WINDOW_SIZE"] PREVIEW_WINDOW_SIZE = config["General"]["PREVIEW_WINDOW_SIZE"] SEARCH_ROOT = config["Paths"]["SEARCH_ROOT"] TRASH_DIR = config["Paths"]["TRASH_DIR"] TRASH_ENABLED = config.getboolean("Features", "TRASH_ENABLED") EDITOR_ENABLED = config.getboolean("Features", "EDITOR_ENABLED") MANUEAL_REPLACE_ENABLED = config.getboolean("Features", "MANUEAL_REPLACE_ENABLED") CLOSE_ALL_ENABLED = config.getboolean("Features", "CLOSE_ALL_ENABLED") AUTO_CLOSE_SECONDS = config.getint("Features", "AUTO_CLOSE_SECONDS") Extension = config["Copy"]["Extension"] # ============================================================ os.makedirs(TRASH_DIR, exist_ok=True) def custom_messagebox(title, message, kind="info"): win = tk.Toplevel() win.title(title) win.geometry(SEARCH_WINDOW_SIZE) win.grab_set() win.focus_force() frame = tk.Frame(win, padx=20, pady=20) frame.pack(expand=True, fill="both") center_window(win) tk.Label(frame, text=message, font=("Consolas", 24), wraplength=550).pack(pady=20) def close(): win.destroy() if kind == "yesno": result = {"value": False} IsInfo = False def yes(): result["value"] = True win.destroy() def no(): result["value"] = False win.destroy() tk.Button(frame, text="Yes", font=("Consolas", 24), padx=10, pady=10, command=yes).pack(side="left", padx=16) tk.Button(frame, text="No", font=("Consolas", 24), padx=10, pady=10, command=no).pack(side="right", padx=16) win.wait_window() return result["value"] else: tk.Button(frame, text="OK", font=("Consolas", 24), padx=10, pady=10, command=close).pack(pady=20) win.bind("", lambda event: close()) win.wait_window() def center_window(win): win.update_idletasks() w = win.winfo_width() h = win.winfo_height() sw = win.winfo_screenwidth() sh = win.winfo_screenheight() x = (sw // 2) - (w // 2) y = (sh // 2) - (h // 2) win.geometry(f"{w}x{h}+{x}+{y}") def find_external_drives(): drives = [] DRIVE_REMOVABLE = 2 for letter in string.ascii_uppercase: drive = f"{letter}:\\" if os.path.exists(drive): dtype = ctypes.windll.kernel32.GetDriveTypeW(drive) if dtype == DRIVE_REMOVABLE: drives.append(drive) return drives def close_all(root): for w in root.winfo_children(): try: w.destroy() except: pass root.destroy() def popup_search_input(): win = tk.Toplevel() win.title("Search") win.geometry(SEARCH_WINDOW_SIZE) center_window(win) tk.Label(win, text="Enter program number, part number, or name:").pack(pady=10) entry = tk.Entry(win) entry.pack(pady=5) entry.focus_set() entry.bind("", lambda event: submit()) win.bind("", lambda event: submit()) result = {"value": None} def submit(): result["value"] = entry.get().strip().lower() win.destroy() tk.Button(win, text="Search", command=submit, padx=20, pady=20).pack(pady=60) win.wait_window(); return result["value"] def search_files(term): matches = [] for root, dirs, files in os.walk(SEARCH_ROOT): for fname in files: path = os.path.join(root, fname) fname_lower = fname.lower() filename_match = term in fname_lower contents_match = False try: with open(path, "r", errors="ignore") as f: if term in f.read().lower(): contents_match = True except: pass if filename_match or contents_match: matches.append(path) return matches def popup_results_list(matches): win = tk.Toplevel() win.title("Matches Found") win.geometry(RESULTS_WINDOW_SIZE) center_window(win) tk.Label(win, text="Select files to preview:").pack(pady=10) lb = tk.Listbox(win, selectmode=tk.MULTIPLE) lb.pack(expand=True, fill="both") for m in matches: lb.insert(tk.END, m) result = {"selected": []} def select_all(): lb.select_set(0, tk.END) def submit(): sel = lb.curselection() result["selected"] = [matches[i] for i in sel] win.destroy() tk.Button(win, text="Open Selected", command=submit, padx=20, pady=10).pack(side ="right", padx=20, pady=10) tk.Button(win, text="Select All", command=select_all, padx=20, pady=10).pack(side ="left", padx=20, pady=10) lb.bind("", lambda event: submit()) win.bind("", lambda event: submit()) win.wait_window(); return result["selected"] def popup_file_preview(path, on_close_callback): win = tk.Toplevel() win.title(path) win.geometry(PREVIEW_WINDOW_SIZE) box = scrolledtext.ScrolledText(win, wrap=tk.WORD) box.pack(expand=True, fill="both") try: with open(path, "r", errors="ignore") as f: original_text = f.read() box.insert(tk.END, original_text) box.edit_modified(False) except Exception as e: original_text = "" box.insert(tk.END, f"Error reading file:\n{e}") modified = {"changed": False} def mark_modified(event=None): if box.edit_modified(): modified["changed"] = True box.edit_modified(False) if EDITOR_ENABLED: box.bind("<>", mark_modified) else: box.configure(state="disabled") frame = tk.Frame(win) frame.pack(fill="x", pady=10) def save_file(): try: with open(path, "w", errors="ignore") as f: f.write(box.get("1.0", tk.END)) modified["changed"] = False custom_messagebox("Saved", f"File saved:\n{path}") except Exception as e: custom_messagebox("Save Error", str(e)) def copy_to_external(): if EDITOR_ENABLED and modified["changed"]: ask = custom_messagebox("Unsaved Changes", "You have unsaved changes.\nSave before copying?", kind = "yesno") if ask: save_file() drives = find_external_drives() if not drives: custom_messagebox("No External Drive Found", "No external drive detected."); return base_name = os.path.splitext(os.path.basename(path))[0] new_name = base_name + Extension copied = [] for d in drives: try: dest = os.path.join(d, new_name) shutil.copy2(path, dest) copied.append(dest) except Exception as e: custom_messagebox("Copy Error", f"Could not copy to {d}:\n{e}") if copied: custom_messagebox("Copied Successfully", "Copied to:\n\n" + "\n".join(copied)) win.destroy() on_close_callback() close_all(win.master) def move_to_trash(): confirm = custom_messagebox("Move to Trash",f"Are you sure you want to move this file to trash?\n\n{path}", kind = "yesno") if confirm: try: rel_path = os.path.relpath(path, SEARCH_ROOT) dest_path = os.path.join(TRASH_DIR, rel_path) os.makedirs(os.path.dirname(dest_path), exist_ok=True) shutil.move(path, dest_path) custom_messagebox("Trashed", f"Moved to trash:\n{dest_path}") win.destroy() on_close_callback() except Exception as e: custom_messagebox("Trash Error", str(e)) def manual_replace(): drives = find_external_drives() if not drives: custom_messagebox("No External Drive Found","No external drive detected."); return base_name = os.path.splitext(os.path.basename(path))[0] new_name = base_name + Extension replacement_path = None for d in drives: candidate = os.path.join(d, new_name) if os.path.exists(candidate): replacement_path = candidate; break if not replacement_path: custom_messagebox("File Not Found",f"No replacement file named:\n\n{new_name}\n\nwas found on any external drive."); return try: with open(replacement_path,"r",errors="ignore") as f: new_contents = f.read() except Exception as e: custom_messagebox("Read Error",f"Could not read replacement file:\n{e}"); return try: with open(path,"w",errors="ignore") as f: f.write(new_contents) except Exception as e: custom_messagebox("Write Error",f"Could not overwrite original file:\n{e}"); return box.delete("1.0",tk.END); box.insert(tk.END,new_contents); box.edit_modified(False); modified["changed"] = False custom_messagebox("Replaced Successfully",f"File replaced using:\n{replacement_path}") def close(): if EDITOR_ENABLED and modified["changed"]: ask = custom_messagebox("Unsaved Changes", "You have unsaved changes.\nSave before closing?", kind = "yesno") if ask: save_file() win.destroy() on_close_callback() def auto_close(): if win.winfo_exists(): win.destroy() on_close_callback() def close_all_button(): ask = custom_messagebox("WARNING!", "Are you sure you want to cloose all windows?", kind = "yesno") if ask: close_all(win.master) win.protocol("WM_DELETE_WINDOW", close) win.after(AUTO_CLOSE_SECONDS * 1000, auto_close) if EDITOR_ENABLED: tk.Button(win, text="Save", fg="green", command=save_file, padx=15, pady=8).pack(side ="left",padx=10, pady=10) if TRASH_ENABLED == True: tk.Button(win, text="Move to Trash", fg="red", command=move_to_trash, padx=15, pady=8).pack(side ="left",padx=10, pady=10) tk.Button(win, text="Copy to External Drive", command=copy_to_external, padx=15, pady=8).pack(side ="left",padx=10, pady=10) tk.Button(win, text="Close", command=close, padx=15, pady=8).pack(side ="right",padx=10, pady=10) if CLOSE_ALL_ENABLED: tk.Button(win, text="Close All", command=lambda: close_all_button(), padx=15, pady=8).pack(side ="right",padx=10, pady=10) if MANUEAL_REPLACE_ENABLED == True: tk.Button(win, text="Replace from External Drive", command=manual_replace, padx=15, pady=8).pack(side ="right",padx=10, pady=10) def main(): root = tk.Tk() root.withdraw() default_font = tkfont.nametofont("TkDefaultFont") default_font.configure(family="Consolas", size=18) text_font = tkfont.nametofont("TkTextFont") text_font.configure(family="Consolas", size=18) fixed_font = tkfont.nametofont("TkFixedFont") fixed_font.configure(family="Consolas", size=18) root.option_add("*Dialog.msg.font", "Consolas 18") root.option_add("*Dialog.msg.width", 60) root.option_add("*Dialog.msg.height", 10) root.option_add("*Dialog.msg.wrapLength", 600) while True: search_term = popup_search_input() if not search_term: root.destroy() return matches = search_files(search_term) if matches: break custom_messagebox("No Results", "No matching files found.") selected = popup_results_list(matches) if not selected: root.destroy() return open_windows = {"count": len(selected)} def window_closed(): open_windows["count"] -= 1 if open_windows["count"] == 0: root.destroy() for path in selected: popup_file_preview(path, window_closed) root.mainloop() if __name__ == "__main__": main()