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