diff --git a/Search.py b/Search.py index 7c326f9..4cf5931 100644 --- a/Search.py +++ b/Search.py @@ -1,377 +1,380 @@ -#!/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_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("600x300") - 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("600x300") - 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("1200x1000") - 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("900x1000") - - 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__": +#!/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() \ No newline at end of file