Skip to content

Instantly share code, notes, and snippets.

@qianlifeng
Last active December 12, 2025 13:31
Show Gist options
  • Select an option

  • Save qianlifeng/43e8f545017fddea69e1198468d76776 to your computer and use it in GitHub Desktop.

Select an option

Save qianlifeng/43e8f545017fddea69e1198468d76776 to your computer and use it in GitHub Desktop.
Wox.Plugin.Script.RecentFile
#!/usr/bin/env python3
# {
# "Id": "d4cf7d41-5cc2-4c21-8ee6-fbd6ca950c6a",
# "Name": "Recent Files",
# "Author": "qianlifeng",
# "Version": "1.0.0",
# "MinWoxVersion": "2.0.0",
# "Description": "List recently used files.",
# "Icon": "emoji:🕘",
# "TriggerKeywords": ["recent"],
# "SupportedOS": ["windows"]
# }
"""
Wox Python Script Plugin for Recent Files
This plugin lists recently used files on Windows.
"""
from __future__ import annotations
import datetime
import json
import os
import subprocess
import sys
from pathlib import Path
from typing import Any, List, TypedDict
class WoxPluginBase:
"""Wox plugin base class for script plugins. Don't modify this class directly."""
class ActionItem(TypedDict, total=False):
"""Type hint for an action in a result."""
id: str
name: str
text: str
url: str
path: str
message: str
data: Any
class QueryResult(TypedDict, total=False):
"""Type hint for a query result item."""
title: str
subtitle: str
icon: str
"""
support following icon formats:
- "base64:data:image/png;base64,xxx"
- "emoji:😀"
- "svg:<svg>...</svg>"
- "fileicon:/absolute/path/to/file" (get system file icon)
- "absolute:/absolute/path/to/image.png"
- "url:https://example.com/image.png"
"""
score: int
actions: List[WoxPluginBase.ActionItem]
class ActionResult(TypedDict, total=False):
"""Type hint for action method return value."""
action: str
message: str
def __init__(self):
self.log_file_path = __file__ + ".log"
def log(self, message: str) -> None:
"""Log message to log file for debugging."""
ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
message = f"[{ts}] {message}"
if self.is_invoke_from_wox():
with open(self.log_file_path, "a", encoding="utf-8") as f:
f.write(f"{message}\n")
else:
print(f"LOG: {message}")
def is_invoke_from_wox(self) -> bool:
"""Check if the script is invoked from Wox."""
return "WOX_PLUGIN_ID" in os.environ
def _build_response(self, result: Any, request_id: Any) -> dict:
"""Build a successful JSON-RPC 2.0 response."""
return {"jsonrpc": "2.0", "result": result, "id": request_id}
def _build_error_response(
self, code: int, message: str, data: Any = None, request_id: Any = None
) -> dict:
"""Build a JSON-RPC 2.0 error response."""
error = {"code": code, "message": message}
if data is not None:
error["data"] = data
return {"jsonrpc": "2.0", "error": error, "id": request_id}
def query(
self, raw_query: str, trigger_keyword: str, command: str, search: str
) -> List[WoxPluginBase.QueryResult]:
"""
Process user queries and return results.
Args:
raw_query: The full raw query string entered by the user
trigger_keyword: The trigger keyword that activated this plugin
command: The command part of the query (after trigger keyword)
search: The search part of the query (after command)
take `wpm install plugin-name` as an example
raw_query: wpm install plugin-name
trigger_keyword: wpm
command: install
search: plugin-name
Returns:
List of result dictionaries
"""
# Default implementation - override in your plugin class
return [
{
"title": "Hello Wox!",
"subtitle": "This is a default result. Override the query method in your plugin class.",
"score": 100,
"actions": [
{"name": "Copy", "id": "copy-to-clipboard", "text": "Hello Wox!"}
],
}
]
def action(self, action_id: str, data: Any) -> None:
"""
Handle action requests (OPTIONAL - only needed for custom actions)
Built-in actions (copy-to-clipboard, open-url, open-directory, notify) are handled
automatically by Wox. You only need to implement this method if you have custom actions.
Note: This method is called as a hook for ALL actions (built-in and custom), so you can
optionally add additional logic for built-in actions if needed.
Args:
action_id: The ID of the action
data: Additional data for the action
"""
# Default implementation - override for custom actions
if action_id == "custom-action":
# Custom action handling logic can be added here
pass
def handle_query(self, params, request_id):
"""Internal method to handle query requests"""
search = params.get("search", "")
trigger_keyword = params.get("trigger_keyword", "")
command = params.get("command", "")
raw_query = params.get("raw_query", "")
results = self.query(raw_query, trigger_keyword, command, search)
return self._build_response({"items": results}, request_id)
def handle_action(self, params, request_id):
"""Internal method to handle action requests"""
action_id = params.get("id", "")
action_data = params.get("data", "")
self.action(action_id, action_data)
return self._build_response({}, request_id)
def run(self):
"""Main entry point for the script plugin"""
# Parse input
if self.is_invoke_from_wox():
# Running from Wox - read from stdin
try:
stdin_text = sys.stdin.read()
except Exception:
return 1
else:
# Manual testing mode
print("Manual mode - please enter query:")
query_input = input()
stdin_text = f'{{"jsonrpc": "2.0", "method": "query", "params": {{"query": "{query_input}"}}, "id": 1}}'
# Parse JSON-RPC 2.0 request
try:
request = json.loads(stdin_text)
except json.JSONDecodeError as e:
error_response = self._build_error_response(
-32700, "Parse error", str(e), None
)
print(json.dumps(error_response, ensure_ascii=False))
return 1
# Validate JSON-RPC 2.0 format
if request.get("jsonrpc") != "2.0":
error_response = self._build_error_response(
-32600, "Invalid Request", None, request.get("id")
)
print(json.dumps(error_response, ensure_ascii=False))
return 1
method = request.get("method")
params = request.get("params", {})
request_id = request.get("id")
# Handle different methods
if method == "query":
response = self.handle_query(params, request_id)
elif method == "action":
response = self.handle_action(params, request_id)
else:
# Method not found
response = self._build_error_response(
-32601,
"Method not found",
f"Method '{method}' not supported",
request_id,
)
# Output response
print(json.dumps(response, ensure_ascii=False))
return 0
class RecentFilePlugin(WoxPluginBase):
def _parse_lnk_targets_batch(self, lnk_files: List[Path]) -> dict:
"""Parse multiple .lnk files in a single PowerShell call for better performance."""
if not lnk_files:
return {}
try:
# Build PowerShell script to parse all shortcuts at once
ps_lines = ['[Console]::OutputEncoding = [System.Text.Encoding]::UTF8']
ps_lines.append('$shell = New-Object -ComObject WScript.Shell')
for i, lnk_path in enumerate(lnk_files):
# Escape quotes in path
escaped_path = str(lnk_path).replace('"', '`"')
ps_lines.append(f'try {{ $target = $shell.CreateShortcut("{escaped_path}").TargetPath; Write-Output "IDX{i}|$target" }} catch {{ Write-Output "IDX{i}|" }}')
ps_script = '; '.join(ps_lines)
result = subprocess.run(
["powershell", "-NoProfile", "-Command", ps_script],
capture_output=True,
text=True,
timeout=5,
encoding="utf-8",
errors="replace"
)
# Parse results
targets = {}
if result.stdout:
for line in result.stdout.strip().split('\n'):
if '|' in line:
idx_part, target = line.split('|', 1)
if idx_part.startswith('IDX'):
idx = int(idx_part[3:])
target = target.strip()
if target and Path(target).exists():
targets[idx] = target
return targets
except Exception as e:
self.log(f"batch parse error: {e}")
return {}
def _parse_lnk_target(self, path: Path) -> str:
"""Parse .lnk file using PowerShell COM interface."""
try:
# Use PowerShell to call Windows Shell COM interface
ps_script = f'[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; (New-Object -ComObject WScript.Shell).CreateShortcut("{path}").TargetPath'
result = subprocess.run(
["powershell", "-NoProfile", "-Command", ps_script],
capture_output=True,
text=True,
timeout=2,
encoding="utf-8",
errors="replace"
)
target = result.stdout.strip() if result.stdout else ""
if target and Path(target).exists():
return target
return ""
except Exception as e:
self.log(f"parse error {path}: {e}")
return ""
def _get_recent_files(self, max_items: int, query: str = "") -> List[WoxPluginBase.QueryResult]:
appdata = os.environ.get("APPDATA", "")
if not appdata:
self.log("APPDATA missing")
return []
recent_folder = Path(appdata) / "Microsoft" / "Windows" / "Recent"
if not recent_folder.is_dir():
self.log(f"recent folder not found: {recent_folder}")
return []
lnk_files = list(recent_folder.glob("*.lnk"))
# Sort by modification time (when the shortcut was created/updated)
lnk_files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
# Pre-filter by lnk filename if query exists
# .lnk files usually have the same name as the target file
query_lower = query.lower() if query else ""
if query_lower:
filtered_lnk_files = [
lnk for lnk in lnk_files
if query_lower in lnk.stem.lower()
]
# If filtering reduces too much, expand to check full lnk path
if len(filtered_lnk_files) < max_items * 2:
filtered_lnk_files = [
lnk for lnk in lnk_files
if query_lower in str(lnk).lower()
]
lnk_files = filtered_lnk_files
self.log(f"pre-filtered to {len(lnk_files)} lnk files with query={query!r}")
# Parse only what we need (with buffer for directories)
parse_count = min(max_items * 2, len(lnk_files))
lnk_to_parse = lnk_files[:parse_count]
targets = self._parse_lnk_targets_batch(lnk_to_parse)
self.log(f"parsed {len(targets)} targets from {len(lnk_to_parse)} lnk files")
# Build results
results: List[WoxPluginBase.QueryResult] = []
for idx, lnk in enumerate(lnk_to_parse):
if len(results) >= max_items:
break
if idx not in targets:
continue
target = targets[idx]
# Skip directories, only show files
if not os.path.isfile(target):
continue
# Double-check query filter on actual target path (in case lnk name differs)
if query_lower:
filename = Path(target).name.lower()
fullpath = target.lower()
if query_lower not in filename and query_lower not in fullpath:
continue
results.append({
"title": Path(target).name,
"subtitle": target,
"icon": "fileicon:" + target,
"score": 100,
"actions": [
{"name": "Open", "id": "open-file", "data": target}
],
})
return results
def query(self, raw_query: str, trigger_keyword: str, command: str, search: str) -> List[WoxPluginBase.QueryResult]:
return self._get_recent_files(10, search)
def action(self, action_id: str, data: Any) -> None:
if action_id == "open-file":
self.log(f"action: open-file {data}")
file_path = data
if os.path.isfile(file_path):
os.startfile(file_path)
if __name__ == "__main__":
sys.exit(RecentFilePlugin().run())
@qianlifeng
Copy link
Author

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment