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

Read Entire Article