import os
import subprocess
import threading
import tkinter as tk
from tkinter import messagebox, scrolledtext, ttk
# === CONFIG ===
PROJECT_ROOT = r"F:\Documents\mercurial_project"
CONDA_PYTHON = r"C:\Users\Miguel\anaconda3\envs\mercurial\python.exe"
# === DARK THEME COLORS ===
BG_COLOR = "#1e1e1e"
FG_COLOR = "#d4d4d4"
ACCENT_COLOR = "#007acc"
LISTBOX_BG = "#252526"
OUTPUT_BG = "#1e1e1e"
BUTTON_BG = "#3c3c3c"
BUTTON_ACTIVE = "#505050"
ENTRY_BG = "#2d2d2d"
PLACEHOLDER_COLOR = "#888888"
# Force UTF-8 globally
os.environ["PYTHONUTF8"] = "1"
try:
import sys
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
except Exception:
pass
[docs]
class ScriptLauncher:
def __init__(self, root):
self.root = root
self.root.title("Python Launcher IDE ๐ฅ")
self.root.geometry("950x650")
self.root.configure(bg=BG_COLOR)
# Scripts storage
self.scripts = []
self.filtered_scripts = []
self.script_to_category = {}
# โ
Track running process
self.current_process = None
# === SEARCH BAR ===
self.search_var = tk.StringVar()
self.search_entry = tk.Entry(
root,
textvariable=self.search_var,
bg=ENTRY_BG,
fg=PLACEHOLDER_COLOR, # start as placeholder color
insertbackground="white",
relief=tk.FLAT,
)
self.search_entry.pack(fill="x", padx=10, pady=5)
# Set placeholder text
self.placeholder_text = "Search scripts..."
self.search_entry.insert(0, self.placeholder_text)
# Bind focus events
self.search_entry.bind("<FocusIn>", self._clear_placeholder)
self.search_entry.bind("<FocusOut>", self._add_placeholder)
# === MAIN FRAME ===
main_frame = tk.Frame(root, bg=BG_COLOR)
main_frame.pack(fill="both", expand=True)
# === CATEGORY TREE ===
self.tree = ttk.Treeview(main_frame)
self.tree.pack(side="left", fill="y", padx=5, pady=5)
self.tree.bind("<<TreeviewSelect>>", self.on_tree_select)
style = ttk.Style()
style.theme_use("default")
style.configure(
"Treeview",
background=LISTBOX_BG,
foreground=FG_COLOR,
fieldbackground=LISTBOX_BG,
highlightthickness=0,
borderwidth=0,
)
style.map(
"Treeview", background=[("selected", ACCENT_COLOR)], foreground=[("selected", "white")]
)
# === SCRIPT LIST ===
self.listbox = tk.Listbox(
main_frame, bg=LISTBOX_BG, fg=FG_COLOR, selectbackground=ACCENT_COLOR, borderwidth=0
)
self.listbox.pack(side="left", fill="both", expand=True, padx=5, pady=5)
# === BUTTONS ===
btn_frame = tk.Frame(root, bg="#151515", height=45)
btn_frame.pack(fill="x")
btn_frame.pack_propagate(False)
run_btn = IDEButton(
btn_frame,
text="๐ Run",
command=self.run_script,
fg="white",
bg="#1f7a1f",
hover_bg="#2aa82a",
activebackground="#145214",
relief=tk.FLAT,
)
run_btn.pack(side="left", padx=8, pady=8)
ToolTip(run_btn, "Run selected script")
refresh_btn = IDEButton(
btn_frame,
text="๐ Refresh",
command=self.load_scripts,
fg="white",
bg="#1f4e79",
hover_bg="#2f6fa8",
activebackground="#163a5c",
relief=tk.FLAT,
)
refresh_btn.pack(side="left", padx=8, pady=8)
ToolTip(refresh_btn, "Reload all scripts from disk")
clear_btn = IDEButton(
btn_frame,
text="๐งน Clear",
command=self.clear_output,
fg="white",
bg="#8a5a00",
hover_bg="#a86d00",
activebackground="#6b4500",
relief=tk.FLAT,
)
clear_btn.pack(side="left", padx=8, pady=8)
ToolTip(clear_btn, "Clear output console")
kill_btn = IDEButton(
btn_frame,
text="๐ Kill",
command=self.kill_script,
fg="white",
bg="#8b0000",
hover_bg="#a00000",
activebackground="#5c0000",
relief=tk.FLAT,
)
kill_btn.pack(side="left", padx=8, pady=8)
ToolTip(kill_btn, "Force stop running script")
# === OUTPUT ===
self.output = scrolledtext.ScrolledText(
root, height=15, bg=OUTPUT_BG, fg=FG_COLOR, insertbackground="white", borderwidth=0
)
self.output.pack(fill="both", expand=True, padx=10, pady=10)
# === PROGRESS BAR ===
self.progress = ttk.Progressbar(root, mode="indeterminate")
self.progress.pack(fill="x", padx=10, pady=(0, 10))
# Load scripts and bind search
self.load_scripts()
self.search_var.trace_add("write", self.filter_scripts)
[docs]
def load_scripts(self):
self.scripts.clear()
self.script_to_category.clear()
self.tree.delete(*self.tree.get_children())
root_node = self.tree.insert("", "end", text="All Scripts", open=True)
for root_dir, _, files in os.walk(PROJECT_ROOT):
for file in files:
if file.endswith(".py") and file != "__init__.py":
full_path = os.path.join(root_dir, file)
self.scripts.append(full_path)
parts = os.path.relpath(full_path, PROJECT_ROOT).split(os.sep)[:-1]
parent = root_node
for part in parts:
children = self.tree.get_children(parent)
found = next(
(c for c in children if self.tree.item(c, "text") == part), None
)
parent = found or self.tree.insert(parent, "end", text=part, open=True)
script_id = self.tree.insert(parent, "end", text=file)
self.script_to_category[script_id] = full_path
self.show_scripts(self.scripts)
[docs]
def on_tree_select(self, event):
selected = self.tree.selection()
if not selected:
return
script_paths = []
for node in selected:
if node in self.script_to_category:
script_paths.append(self.script_to_category[node])
else:
script_paths.extend(self.get_all_scripts_in_node(node))
self.show_scripts(script_paths)
[docs]
def get_all_scripts_in_node(self, node):
scripts = []
for child in self.tree.get_children(node):
if child in self.script_to_category:
scripts.append(self.script_to_category[child])
else:
scripts.extend(self.get_all_scripts_in_node(child))
return scripts
[docs]
def show_scripts(self, script_list):
self.filtered_scripts = script_list
self.listbox.delete(0, tk.END)
for path in script_list:
self.listbox.insert(tk.END, os.path.relpath(path, PROJECT_ROOT))
[docs]
def filter_scripts(self, *args):
query = self.search_var.get().lower().strip()
if not query or query == self.placeholder_text.lower():
self.show_scripts(self.scripts)
return
filtered = [s for s in self.scripts if query in os.path.relpath(s, PROJECT_ROOT).lower()]
self.show_scripts(filtered)
[docs]
def run_script(self):
selection = self.listbox.curselection()
if not selection:
messagebox.showwarning("No selection", "Pick a script first ๐")
return
script_path = self.filtered_scripts[selection[0]]
self.output.delete(1.0, tk.END)
self.progress.start(50)
threading.Thread(target=self.execute_script, args=(script_path,), daemon=True).start()
[docs]
def execute_script(self, script_path):
env = os.environ.copy()
env["PYTHONUTF8"] = "1"
env["PYTHONPATH"] = PROJECT_ROOT
self.current_process = subprocess.Popen(
[CONDA_PYTHON, script_path],
cwd=PROJECT_ROOT,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
encoding="utf-8",
errors="replace",
)
for line in self.current_process.stdout:
self._update_output(line)
self.current_process.wait()
self._update_output(f"\n[Done] Exit code: {self.current_process.returncode}\n")
self.progress.stop()
self.current_process = None
[docs]
def clear_output(self):
self.output.delete(1.0, tk.END)
self.progress.stop()
# โ
KILLSWITCH LOGIC
[docs]
def kill_script(self):
if self.current_process and self.current_process.poll() is None:
try:
self.current_process.terminate()
self._update_output("\n[!] Script terminated by user\n")
except Exception as e:
self._update_output(f"\n[Error killing process]: {e}\n")
finally:
self.progress.stop()
else:
messagebox.showinfo("No running script", "Nothing is currently running ๐")
def _update_output(self, text):
def inner():
self.output.insert(tk.END, text)
self.output.see(tk.END)
self.output.after(0, inner)
def _clear_placeholder(self, event):
if self.search_entry.get() == self.placeholder_text:
self.search_entry.delete(0, tk.END)
self.search_entry.config(fg=FG_COLOR)
def _add_placeholder(self, event):
if not self.search_entry.get():
self.search_entry.insert(0, self.placeholder_text)
self.search_entry.config(fg=PLACEHOLDER_COLOR)
if __name__ == "__main__":
if not os.path.exists(PROJECT_ROOT):
print("Invalid PROJECT_ROOT path!")
exit()
if not os.path.exists(CONDA_PYTHON):
print("Invalid CONDA_PYTHON path!")
exit()
root = tk.Tk()
app = ScriptLauncher(root)
root.mainloop()