Command Palette Launcher (VS Code Style)

 """

Command Palette Launcher (VS Code style)

Tech: tkinter, os, keyboard, difflib


Features:

- 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 os

import sys

import threading

import platform

import subprocess

from pathlib import Path

import tkinter as tk

from tkinter import ttk, filedialog, messagebox

from difflib import get_close_matches


# Optional global hotkey package

try:

    import keyboard  # pip install keyboard

    KEYBOARD_AVAILABLE = True

except 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()


No comments: