Command Palette Launcher (VS Code Style)
a month ago
12
"""Command Palette Launcher (VS Code style)Tech: tkinter, os, keyboard, difflibFeatures:- Ctrl+P to open palette (global using 'keyboard', and also inside Tk window)- Index files from a folder for quick search- Fuzzy search using difflib- Open files (os.startfile on Windows / xdg-open on Linux / open on macOS)- Add custom commands (open app, shell command)- Demo includes uploaded file path: /mnt/data/image.png"""import osimport sysimport threadingimport platformimport subprocessfrom pathlib import Pathimport tkinter as tkfrom tkinter import ttk, filedialog, messageboxfrom difflib import get_close_matches# Optional global hotkey packagetry: import keyboard # pip install keyboard KEYBOARD_AVAILABLE = Trueexcept Exception: KEYBOARD_AVAILABLE = False# Demo uploaded file path (from your session)DEMO_FILE = "/mnt/data/image.png"# ------------------------------# Utility functions# ------------------------------def open_path(path): """Open a file or folder using the OS default application.""" p = str(path) if platform.system() == "Windows": os.startfile(p) elif platform.system() == "Darwin": # macOS subprocess.Popen(["open", p]) else: # Linux and others subprocess.Popen(["xdg-open", p])def is_executable_file(path): try: return os.access(path, os.X_OK) and Path(path).is_file() except Exception: return False# ------------------------------# Indexer# ------------------------------class FileIndexer: def __init__(self): self.items = [] # list of {"title": ..., "path": ..., "type": "file"|"cmd"} # preload demo item if exists if Path(DEMO_FILE).exists(): self.add_item(title=Path(DEMO_FILE).name, path=str(DEMO_FILE), typ="file") def add_item(self, title, path, typ="file"): rec = {"title": title, "path": path, "type": typ} self.items.append(rec) def index_folder(self, folder, max_files=5000): """Recursively index a folder (stop at max_files).""" folder = Path(folder) count = 0 for root, dirs, files in os.walk(folder): for f in files: try: fp = Path(root) / f self.add_item(title=f, path=str(fp), typ="file") count += 1 if count >= max_files: return count except Exception: continue return count def add_common_apps(self): """Add some common app commands (platform-specific).""" sysplat = platform.system() apps = [] if sysplat == "Windows": # common Windows apps (paths may vary) apps = [ ("Notepad", "notepad.exe"), ("Calculator", "calc.exe"), ("Paint", "mspaint.exe"), ] elif sysplat == "Darwin": apps = [ ("TextEdit", "open -a TextEdit"), ("Calculator", "open -a Calculator"), ] else: # Linux apps = [ ("Gedit", "gedit"), ("Calculator", "gnome-calculator"), ] for name, cmd in apps: self.add_item(title=name, path=cmd, typ="cmd") def search(self, query, limit=15): """Simple fuzzy search: look for substrings first, then difflib matches.""" q = query.strip().lower() if not q: # return top items return self.items[:limit] # substring matches (higher priority) substr_matches = [it for it in self.items if q in it["title"].lower() or q in it["path"].lower()] if len(substr_matches) >= limit: return substr_matches[:limit] # prepare list of titles for difflib titles = [it["title"] for it in self.items] close = get_close_matches(q, titles, n=limit, cutoff=0.4) # map back to records preserving order (titles may repeat) close_records = [] for t in close: for it in self.items: if it["title"] == t and it not in close_records: close_records.append(it) break # combine substring + close matches, ensure uniqueness results = [] seen = set() for it in substr_matches + close_records: key = (it["title"], it["path"]) if key not in seen: results.append(it) seen.add(key) if len(results) >= limit: break return results# ------------------------------# GUI# ------------------------------class CommandPalette(tk.Toplevel): def __init__(self, master, indexer: FileIndexer): super().__init__(master) self.indexer = indexer self.title("Command Palette") self.geometry("700x380") self.transient(master) self.grab_set() # modal self.resizable(False, False) # Styling self.configure(bg="#2b2b2b") # Search box self.search_var = tk.StringVar() search_entry = ttk.Entry(self, textvariable=self.search_var, font=("Consolas", 14), width=60) search_entry.pack(padx=12, pady=(12,6)) search_entry.focus_set() search_entry.bind("<KeyRelease>", self.on_search_key) search_entry.bind("<Escape>", lambda e: self.close()) search_entry.bind("<Return>", lambda e: self.open_selected()) # Results list self.tree = ttk.Treeview(self, columns=("title","path","type"), show="headings", height=12) self.tree.heading("title", text="Title") self.tree.heading("path", text="Path / Command") self.tree.heading("type", text="Type") self.tree.column("title", width=250) self.tree.column("path", width=350) self.tree.column("type", width=80, anchor="center") self.tree.pack(padx=12, pady=6, fill="both", expand=True) self.tree.bind("<Double-1>", lambda e: self.open_selected()) self.tree.bind("<Return>", lambda e: self.open_selected()) # Bottom buttons btn_frame = ttk.Frame(self) btn_frame.pack(fill="x", padx=12, pady=(0,12)) ttk.Button(btn_frame, text="Open Folder to Index", command=self.browse_and_index).pack(side="left") ttk.Button(btn_frame, text="Add Command", command=self.add_command_dialog).pack(side="left", padx=6) ttk.Button(btn_frame, text="Close (Esc)", command=self.close).pack(side="right") # initial populate self.update_results(self.indexer.items[:50]) def on_search_key(self, event=None): q = self.search_var.get() results = self.indexer.search(q, limit=50) self.update_results(results) # keep the first row selected children = self.tree.get_children() if children: self.tree.selection_set(children[0]) self.tree.focus(children[0]) def update_results(self, records): # clear for r in self.tree.get_children(): self.tree.delete(r) for rec in records: self.tree.insert("", "end", values=(rec["title"], rec["path"], rec["type"])) def open_selected(self): sel = self.tree.selection() if not sel: return vals = self.tree.item(sel[0])["values"] title, path, typ = vals try: if typ == "file": open_path(path) elif typ == "cmd": # if it's a shell command, run it # Allow both simple exe names and complex shell commands if platform.system() == "Windows": subprocess.Popen(path, shell=True) else: subprocess.Popen(path.split(), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) else: # fallback attempt open_path(path) except Exception as e: messagebox.showerror("Open failed", f"Could not open {path}\n\n{e}") finally: self.close() def browse_and_index(self): folder = filedialog.askdirectory() if not folder: return count = self.indexer.index_folder(folder) messagebox.showinfo("Indexed", f"Indexed approx {count} files from {folder}") # refresh results self.on_search_key() def add_command_dialog(self): dlg = tk.Toplevel(self) dlg.title("Add Command / App") dlg.geometry("500x150") tk.Label(dlg, text="Title:").pack(anchor="w", padx=8, pady=(8,0)) title_e = ttk.Entry(dlg, width=60) title_e.pack(padx=8) tk.Label(dlg, text="Command or Path:").pack(anchor="w", padx=8, pady=(8,0)) path_e = ttk.Entry(dlg, width=60) path_e.pack(padx=8) def add(): t = title_e.get().strip() or Path(path_e.get()).name p = path_e.get().strip() if not p: messagebox.showwarning("Input", "Please provide a command or path") return typ = "cmd" if (" " in p or os.sep not in p and not Path(p).exists()) else "file" self.indexer.add_item(title=t, path=p, typ=typ) dlg.destroy() self.on_search_key() ttk.Button(dlg, text="Add", command=add).pack(pady=8) def close(self): try: self.grab_release() except: pass self.destroy()# ------------------------------# Main App Window# ------------------------------class PaletteApp: def __init__(self, root): self.root = root root.title("Command Palette Launcher") root.geometry("700x120") self.indexer = FileIndexer() self.indexer.add_common_apps() # Add a few demo entries (including uploaded file path) if Path(DEMO_FILE).exists(): self.indexer.add_item(title=Path(DEMO_FILE).name, path=str(DEMO_FILE), typ="file") # Top UI frame = ttk.Frame(root, padding=12) frame.pack(fill="both", expand=True) ttk.Label(frame, text="Press Ctrl+P to open command palette", font=("Arial", 12)).pack(anchor="w") ttk.Button(frame, text="Open Palette (Ctrl+P)", command=self.open_palette).pack(pady=10, anchor="w") ttk.Button(frame, text="Index Folder", command=self.index_folder).pack(side="left") ttk.Button(frame, text="Exit", command=root.quit).pack(side="right") # register global hotkey in a background thread (if available) if KEYBOARD_AVAILABLE: t = threading.Thread(target=self.register_global_hotkey, daemon=True) t.start() else: print("keyboard package not available — global hotkey disabled. Use app's Ctrl+P instead.") # bind Ctrl+P inside the Tk window too root.bind_all("<Control-p>", lambda e: self.open_palette()) def open_palette(self): # open modal CommandPalette cp = CommandPalette(self.root, self.indexer) def index_folder(self): folder = filedialog.askdirectory() if not folder: return count = self.indexer.index_folder(folder) messagebox.showinfo("Indexed", f"Indexed approx {count} files") def register_global_hotkey(self): """ Register Ctrl+P as a global hotkey using keyboard module. When pressed, we must bring the Tk window to front and open palette. """ try: # On some systems, keyboard requires admin privileges. If it fails, we catch and disable. keyboard.add_hotkey("ctrl+p", lambda: self.trigger_from_global()) keyboard.wait() # keep the listener alive except Exception as e: print("Global hotkey registration failed:", e) def trigger_from_global(self): # Because keyboard runs in another thread, schedule UI work in Tk mainloop try: self.root.after(0, self.open_palette) # Try to bring window to front try: self.root.lift() self.root.attributes("-topmost", True) self.root.after(500, lambda: self.root.attributes("-topmost", False)) except Exception: pass except Exception as e: print("Error triggering palette:", e)# ------------------------------# Run# ------------------------------def main(): root = tk.Tk() app = PaletteApp(root) root.mainloop()if __name__ == "__main__": main()
View Entire Post
-
Homepage
-
Payroll
- Command Palette Launcher (VS Code Style)