mirror of https://github.com/Wox-launcher/Wox
feat(script_plugin): Add script plugin support
- Introduced a new runtime type for script plugins in `runtime.go`. - Created script plugin templates for JavaScript, Python, and Bash in the resource directory. - Implemented functionality to create script plugins in `wpm.go`, including handling of template files. - Added a new `ScriptHost` to manage script plugin execution and communication via JSON-RPC. - Enhanced the `Location` utility to manage directories for user script plugins and templates. - Updated resource extraction to include script plugin templates. - Implemented JSON-RPC handling in script templates for query and action methods.
This commit is contained in:
parent
a6b014823d
commit
d488d98eec
|
@ -0,0 +1,362 @@
|
|||
package host
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
"wox/plugin"
|
||||
"wox/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
host := &ScriptHost{}
|
||||
plugin.AllHosts = append(plugin.AllHosts, host)
|
||||
}
|
||||
|
||||
type ScriptHost struct {
|
||||
// Script host doesn't need persistent connections like websocket hosts
|
||||
}
|
||||
|
||||
func (s *ScriptHost) GetRuntime(ctx context.Context) plugin.Runtime {
|
||||
return plugin.PLUGIN_RUNTIME_SCRIPT
|
||||
}
|
||||
|
||||
func (s *ScriptHost) Start(ctx context.Context) error {
|
||||
// Script host doesn't need to start any background processes
|
||||
util.GetLogger().Info(ctx, "Script host started")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ScriptHost) Stop(ctx context.Context) {
|
||||
// Script host doesn't need to stop any background processes
|
||||
util.GetLogger().Info(ctx, "Script host stopped")
|
||||
}
|
||||
|
||||
func (s *ScriptHost) IsStarted(ctx context.Context) bool {
|
||||
// Script host is always "started" since it doesn't maintain persistent connections
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *ScriptHost) LoadPlugin(ctx context.Context, metadata plugin.Metadata, pluginDirectory string) (plugin.Plugin, error) {
|
||||
// For script plugins, the actual script file is in the user script plugins directory
|
||||
userScriptPluginDirectory := util.GetLocation().GetUserScriptPluginsDirectory()
|
||||
scriptPath := filepath.Join(userScriptPluginDirectory, metadata.Entry)
|
||||
|
||||
// Check if script file exists
|
||||
if _, err := os.Stat(scriptPath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("script file not found: %s", scriptPath)
|
||||
}
|
||||
|
||||
// Make sure script is executable
|
||||
if err := os.Chmod(scriptPath, 0755); err != nil {
|
||||
util.GetLogger().Warn(ctx, fmt.Sprintf("Failed to make script executable: %s", err.Error()))
|
||||
}
|
||||
|
||||
util.GetLogger().Info(ctx, fmt.Sprintf("Loaded script plugin: %s", metadata.Name))
|
||||
return NewScriptPlugin(metadata, scriptPath), nil
|
||||
}
|
||||
|
||||
func (s *ScriptHost) UnloadPlugin(ctx context.Context, metadata plugin.Metadata) {
|
||||
// Script plugins don't need explicit unloading since they're not persistent
|
||||
util.GetLogger().Info(ctx, fmt.Sprintf("Unloaded script plugin: %s", metadata.Name))
|
||||
}
|
||||
|
||||
// ScriptPlugin represents a script-based plugin
|
||||
type ScriptPlugin struct {
|
||||
metadata plugin.Metadata
|
||||
scriptPath string
|
||||
}
|
||||
|
||||
func NewScriptPlugin(metadata plugin.Metadata, scriptPath string) *ScriptPlugin {
|
||||
return &ScriptPlugin{
|
||||
metadata: metadata,
|
||||
scriptPath: scriptPath,
|
||||
}
|
||||
}
|
||||
|
||||
func (sp *ScriptPlugin) Init(ctx context.Context, initParams plugin.InitParams) {
|
||||
// Script plugins don't need initialization since they're executed on-demand
|
||||
util.GetLogger().Debug(ctx, fmt.Sprintf("Script plugin %s initialized", sp.metadata.Name))
|
||||
}
|
||||
|
||||
func (sp *ScriptPlugin) Query(ctx context.Context, query plugin.Query) []plugin.QueryResult {
|
||||
// Prepare JSON-RPC request
|
||||
request := map[string]interface{}{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "query",
|
||||
"params": map[string]interface{}{
|
||||
"search": query.Search,
|
||||
"trigger_keyword": query.TriggerKeyword,
|
||||
"command": query.Command,
|
||||
"raw_query": query.RawQuery,
|
||||
},
|
||||
"id": util.GetContextTraceId(ctx),
|
||||
}
|
||||
|
||||
// Execute script and get results
|
||||
results, err := sp.executeScript(ctx, request)
|
||||
if err != nil {
|
||||
util.GetLogger().Error(ctx, fmt.Sprintf("Script plugin %s query failed: %s", sp.metadata.Name, err.Error()))
|
||||
return []plugin.QueryResult{}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// executeScript executes the script with the given JSON-RPC request and returns the results
|
||||
func (sp *ScriptPlugin) executeScript(ctx context.Context, request map[string]interface{}) ([]plugin.QueryResult, error) {
|
||||
// Convert request to JSON
|
||||
requestJSON, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
// Determine the interpreter based on file extension
|
||||
interpreter, err := sp.getInterpreter()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Prepare command
|
||||
var cmd *exec.Cmd
|
||||
if interpreter != "" {
|
||||
cmd = exec.CommandContext(ctx, interpreter, sp.scriptPath)
|
||||
} else {
|
||||
cmd = exec.CommandContext(ctx, sp.scriptPath)
|
||||
}
|
||||
|
||||
// Set up stdin with the JSON-RPC request
|
||||
cmd.Stdin = strings.NewReader(string(requestJSON))
|
||||
|
||||
// Set timeout for script execution
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
cmd = exec.CommandContext(timeoutCtx, cmd.Args[0], cmd.Args[1:]...)
|
||||
cmd.Stdin = strings.NewReader(string(requestJSON))
|
||||
|
||||
// Execute script
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("script execution failed: %w", err)
|
||||
}
|
||||
|
||||
// Parse JSON-RPC response
|
||||
var response map[string]interface{}
|
||||
if err := json.Unmarshal(output, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse script response: %w", err)
|
||||
}
|
||||
|
||||
// Check for JSON-RPC error
|
||||
if errorData, exists := response["error"]; exists {
|
||||
return nil, fmt.Errorf("script returned error: %v", errorData)
|
||||
}
|
||||
|
||||
// Extract results
|
||||
result, exists := response["result"]
|
||||
if !exists {
|
||||
return []plugin.QueryResult{}, nil
|
||||
}
|
||||
|
||||
resultMap, ok := result.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid result format")
|
||||
}
|
||||
|
||||
items, exists := resultMap["items"]
|
||||
if !exists {
|
||||
return []plugin.QueryResult{}, nil
|
||||
}
|
||||
|
||||
itemsArray, ok := items.([]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid items format")
|
||||
}
|
||||
|
||||
// Convert items to QueryResult
|
||||
var queryResults []plugin.QueryResult
|
||||
for _, item := range itemsArray {
|
||||
itemMap, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
queryResult := plugin.QueryResult{
|
||||
Title: getStringFromMap(itemMap, "title"),
|
||||
SubTitle: getStringFromMap(itemMap, "subtitle"),
|
||||
Score: int64(getFloatFromMap(itemMap, "score")),
|
||||
}
|
||||
|
||||
// Handle action if present
|
||||
if actionData, exists := itemMap["action"]; exists {
|
||||
if actionMap, ok := actionData.(map[string]interface{}); ok {
|
||||
queryResult.Actions = []plugin.QueryResultAction{
|
||||
{
|
||||
Name: "Execute",
|
||||
Action: func(ctx context.Context, actionContext plugin.ActionContext) {
|
||||
sp.executeAction(ctx, actionMap)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
queryResults = append(queryResults, queryResult)
|
||||
}
|
||||
|
||||
return queryResults, nil
|
||||
}
|
||||
|
||||
// executeAction executes an action from a script plugin result
|
||||
func (sp *ScriptPlugin) executeAction(ctx context.Context, actionData map[string]interface{}) {
|
||||
// Prepare JSON-RPC request for action
|
||||
request := map[string]interface{}{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "action",
|
||||
"params": map[string]interface{}{
|
||||
"id": getStringFromMap(actionData, "id"),
|
||||
"data": getStringFromMap(actionData, "data"),
|
||||
},
|
||||
"id": util.GetContextTraceId(ctx),
|
||||
}
|
||||
|
||||
// Execute script for action
|
||||
err := sp.executeScriptAction(ctx, request)
|
||||
if err != nil {
|
||||
util.GetLogger().Error(ctx, fmt.Sprintf("Script plugin %s action failed: %s", sp.metadata.Name, err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
// executeScriptAction executes the script for action requests
|
||||
func (sp *ScriptPlugin) executeScriptAction(ctx context.Context, request map[string]interface{}) error {
|
||||
// Convert request to JSON
|
||||
requestJSON, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
// Determine the interpreter based on file extension
|
||||
interpreter, err := sp.getInterpreter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Prepare command
|
||||
var cmd *exec.Cmd
|
||||
if interpreter != "" {
|
||||
cmd = exec.CommandContext(ctx, interpreter, sp.scriptPath)
|
||||
} else {
|
||||
cmd = exec.CommandContext(ctx, sp.scriptPath)
|
||||
}
|
||||
|
||||
// Set up stdin with the JSON-RPC request
|
||||
cmd.Stdin = strings.NewReader(string(requestJSON))
|
||||
|
||||
// Set timeout for script execution
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
cmd = exec.CommandContext(timeoutCtx, cmd.Args[0], cmd.Args[1:]...)
|
||||
cmd.Stdin = strings.NewReader(string(requestJSON))
|
||||
|
||||
// Execute script
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("script execution failed: %w", err)
|
||||
}
|
||||
|
||||
// Parse JSON-RPC response
|
||||
var response map[string]interface{}
|
||||
if err := json.Unmarshal(output, &response); err != nil {
|
||||
return fmt.Errorf("failed to parse script response: %w", err)
|
||||
}
|
||||
|
||||
// Check for JSON-RPC error
|
||||
if errorData, exists := response["error"]; exists {
|
||||
return fmt.Errorf("script returned error: %v", errorData)
|
||||
}
|
||||
|
||||
// Handle action result if present
|
||||
if result, exists := response["result"]; exists {
|
||||
if resultMap, ok := result.(map[string]interface{}); ok {
|
||||
sp.handleActionResult(ctx, resultMap)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleActionResult handles the result from an action execution
|
||||
func (sp *ScriptPlugin) handleActionResult(ctx context.Context, result map[string]interface{}) {
|
||||
actionType := getStringFromMap(result, "action")
|
||||
|
||||
switch actionType {
|
||||
case "open-url":
|
||||
url := getStringFromMap(result, "url")
|
||||
if url != "" {
|
||||
// TODO: Implement URL opening functionality
|
||||
util.GetLogger().Info(ctx, fmt.Sprintf("Script plugin %s requested to open URL: %s", sp.metadata.Name, url))
|
||||
}
|
||||
case "notify":
|
||||
message := getStringFromMap(result, "message")
|
||||
if message != "" {
|
||||
// TODO: Implement notification functionality
|
||||
util.GetLogger().Info(ctx, fmt.Sprintf("Script plugin %s notification: %s", sp.metadata.Name, message))
|
||||
}
|
||||
case "copy-to-clipboard":
|
||||
text := getStringFromMap(result, "text")
|
||||
if text != "" {
|
||||
// TODO: Implement clipboard functionality
|
||||
util.GetLogger().Info(ctx, fmt.Sprintf("Script plugin %s requested to copy to clipboard: %s", sp.metadata.Name, text))
|
||||
}
|
||||
default:
|
||||
util.GetLogger().Warn(ctx, fmt.Sprintf("Script plugin %s returned unknown action type: %s", sp.metadata.Name, actionType))
|
||||
}
|
||||
}
|
||||
|
||||
// getInterpreter determines the appropriate interpreter for the script based on file extension
|
||||
func (sp *ScriptPlugin) getInterpreter() (string, error) {
|
||||
ext := strings.ToLower(filepath.Ext(sp.scriptPath))
|
||||
|
||||
switch ext {
|
||||
case ".py":
|
||||
return "python3", nil
|
||||
case ".js":
|
||||
return "node", nil
|
||||
case ".sh":
|
||||
return "bash", nil
|
||||
case ".rb":
|
||||
return "ruby", nil
|
||||
case ".pl":
|
||||
return "perl", nil
|
||||
case "": // No extension, assume it's executable
|
||||
return "", nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported script type: %s", ext)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions to safely extract values from maps
|
||||
func getStringFromMap(m map[string]interface{}, key string) string {
|
||||
if value, exists := m[key]; exists {
|
||||
if str, ok := value.(string); ok {
|
||||
return str
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getFloatFromMap(m map[string]interface{}, key string) float64 {
|
||||
if value, exists := m[key]; exists {
|
||||
if num, ok := value.(float64); ok {
|
||||
return num
|
||||
}
|
||||
if num, ok := value.(int); ok {
|
||||
return float64(num)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
|
@ -8,6 +8,7 @@ import (
|
|||
"math"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
@ -24,6 +25,7 @@ import (
|
|||
"wox/util/selection"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/samber/lo"
|
||||
|
@ -47,6 +49,10 @@ type Manager struct {
|
|||
aiProviders *util.HashMap[common.ProviderName, ai.Provider]
|
||||
|
||||
activeBrowserUrl string //active browser url before wox is activated
|
||||
|
||||
// Script plugin monitoring
|
||||
scriptPluginWatcher *fsnotify.Watcher
|
||||
scriptReloadTimers *util.HashMap[string, *time.Timer]
|
||||
}
|
||||
|
||||
func GetPluginManager() *Manager {
|
||||
|
@ -55,6 +61,7 @@ func GetPluginManager() *Manager {
|
|||
resultCache: util.NewHashMap[string, *QueryResultCache](),
|
||||
debounceQueryTimer: util.NewHashMap[string, *debounceTimer](),
|
||||
aiProviders: util.NewHashMap[common.ProviderName, ai.Provider](),
|
||||
scriptReloadTimers: util.NewHashMap[string, *time.Timer](),
|
||||
}
|
||||
logger = util.GetLogger()
|
||||
})
|
||||
|
@ -69,6 +76,11 @@ func (m *Manager) Start(ctx context.Context, ui common.UI) error {
|
|||
return fmt.Errorf("failed to load plugins: %w", loadErr)
|
||||
}
|
||||
|
||||
// Start script plugin monitoring
|
||||
util.Go(ctx, "start script plugin monitoring", func() {
|
||||
m.startScriptPluginMonitoring(util.NewTraceContext())
|
||||
})
|
||||
|
||||
util.Go(ctx, "start store manager", func() {
|
||||
GetStoreManager().Start(util.NewTraceContext())
|
||||
})
|
||||
|
@ -77,6 +89,11 @@ func (m *Manager) Start(ctx context.Context, ui common.UI) error {
|
|||
}
|
||||
|
||||
func (m *Manager) Stop(ctx context.Context) {
|
||||
// Stop script plugin monitoring
|
||||
if m.scriptPluginWatcher != nil {
|
||||
m.scriptPluginWatcher.Close()
|
||||
}
|
||||
|
||||
for _, host := range AllHosts {
|
||||
host.Stop(ctx)
|
||||
}
|
||||
|
@ -141,6 +158,15 @@ func (m *Manager) loadPlugins(ctx context.Context) error {
|
|||
}
|
||||
metaDataList = append(metaDataList, MetadataWithDirectory{Metadata: metadata, Directory: pluginDirectory})
|
||||
}
|
||||
|
||||
// Load script plugins
|
||||
scriptMetaDataList, err := m.loadScriptPlugins(ctx)
|
||||
if err != nil {
|
||||
logger.Error(ctx, fmt.Sprintf("failed to load script plugins: %s", err.Error()))
|
||||
} else {
|
||||
metaDataList = append(metaDataList, scriptMetaDataList...)
|
||||
}
|
||||
|
||||
logger.Info(ctx, fmt.Sprintf("start loading user plugins, found %d user plugins", len(metaDataList)))
|
||||
|
||||
for _, host := range AllHosts {
|
||||
|
@ -169,6 +195,44 @@ func (m *Manager) loadPlugins(ctx context.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// loadScriptPlugins loads script plugins from the user script plugins directory
|
||||
func (m *Manager) loadScriptPlugins(ctx context.Context) ([]MetadataWithDirectory, error) {
|
||||
logger.Debug(ctx, "start loading script plugin metadata")
|
||||
|
||||
userScriptPluginDirectory := util.GetLocation().GetUserScriptPluginsDirectory()
|
||||
scriptFiles, readErr := os.ReadDir(userScriptPluginDirectory)
|
||||
if readErr != nil {
|
||||
return nil, fmt.Errorf("failed to read user script plugin directory: %w", readErr)
|
||||
}
|
||||
|
||||
var metaDataList []MetadataWithDirectory
|
||||
for _, entry := range scriptFiles {
|
||||
if entry.Name() == ".DS_Store" || entry.Name() == "README.md" {
|
||||
continue
|
||||
}
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
scriptPath := path.Join(userScriptPluginDirectory, entry.Name())
|
||||
metadata, metadataErr := m.ParseScriptMetadata(ctx, scriptPath)
|
||||
if metadataErr != nil {
|
||||
logger.Error(ctx, fmt.Sprintf("failed to parse script plugin metadata for %s: %s", entry.Name(), metadataErr.Error()))
|
||||
continue
|
||||
}
|
||||
|
||||
// Create a virtual directory for the script plugin
|
||||
virtualDirectory := path.Join(userScriptPluginDirectory, metadata.Id)
|
||||
metaDataList = append(metaDataList, MetadataWithDirectory{
|
||||
Metadata: metadata,
|
||||
Directory: virtualDirectory,
|
||||
})
|
||||
}
|
||||
|
||||
logger.Debug(ctx, fmt.Sprintf("found %d script plugins", len(metaDataList)))
|
||||
return metaDataList, nil
|
||||
}
|
||||
|
||||
func (m *Manager) ReloadPlugin(ctx context.Context, metadata MetadataWithDirectory) error {
|
||||
logger.Info(ctx, fmt.Sprintf("start reloading dev plugin: %s", metadata.Metadata.Name))
|
||||
|
||||
|
@ -357,6 +421,275 @@ func (m *Manager) ParseMetadata(ctx context.Context, pluginDirectory string) (Me
|
|||
return metadata, nil
|
||||
}
|
||||
|
||||
// ParseScriptMetadata parses metadata from script plugin file comments
|
||||
func (m *Manager) ParseScriptMetadata(ctx context.Context, scriptPath string) (Metadata, error) {
|
||||
content, err := os.ReadFile(scriptPath)
|
||||
if err != nil {
|
||||
return Metadata{}, fmt.Errorf("failed to read script file: %w", err)
|
||||
}
|
||||
|
||||
lines := strings.Split(string(content), "\n")
|
||||
metadata := Metadata{
|
||||
Runtime: string(PLUGIN_RUNTIME_SCRIPT),
|
||||
Entry: filepath.Base(scriptPath),
|
||||
Version: "1.0.0", // Default version
|
||||
}
|
||||
|
||||
// Parse metadata from comments
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
// Stop parsing when we reach non-comment lines (except shebang)
|
||||
if !strings.HasPrefix(line, "#") && !strings.HasPrefix(line, "//") && !strings.HasPrefix(line, "#!/") {
|
||||
if line != "" {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Remove comment markers
|
||||
line = strings.TrimPrefix(line, "#")
|
||||
line = strings.TrimPrefix(line, "//")
|
||||
line = strings.TrimPrefix(line, "#!/usr/bin/env")
|
||||
line = strings.TrimPrefix(line, "#!/bin/")
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
// Parse @wox.xxx metadata
|
||||
if strings.HasPrefix(line, "@wox.") {
|
||||
parts := strings.SplitN(line, " ", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
key := strings.TrimPrefix(parts[0], "@wox.")
|
||||
value := strings.TrimSpace(parts[1])
|
||||
|
||||
switch key {
|
||||
case "id":
|
||||
metadata.Id = value
|
||||
case "name":
|
||||
metadata.Name = value
|
||||
case "author":
|
||||
metadata.Author = value
|
||||
case "version":
|
||||
metadata.Version = value
|
||||
case "description":
|
||||
metadata.Description = value
|
||||
case "icon":
|
||||
metadata.Icon = value
|
||||
case "keywords":
|
||||
// Split keywords by comma or space
|
||||
keywords := strings.FieldsFunc(value, func(c rune) bool {
|
||||
return c == ',' || c == ' '
|
||||
})
|
||||
for i, keyword := range keywords {
|
||||
keywords[i] = strings.TrimSpace(keyword)
|
||||
}
|
||||
metadata.TriggerKeywords = keywords
|
||||
case "minWoxVersion":
|
||||
metadata.MinWoxVersion = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if metadata.Id == "" {
|
||||
return Metadata{}, fmt.Errorf("missing required field: @wox.id")
|
||||
}
|
||||
if metadata.Name == "" {
|
||||
return Metadata{}, fmt.Errorf("missing required field: @wox.name")
|
||||
}
|
||||
if len(metadata.TriggerKeywords) == 0 {
|
||||
return Metadata{}, fmt.Errorf("missing required field: @wox.keywords")
|
||||
}
|
||||
|
||||
// Set default values
|
||||
if metadata.Author == "" {
|
||||
metadata.Author = "Unknown"
|
||||
}
|
||||
if metadata.Description == "" {
|
||||
metadata.Description = "A script plugin"
|
||||
}
|
||||
if metadata.Icon == "" {
|
||||
metadata.Icon = "📝"
|
||||
}
|
||||
if metadata.MinWoxVersion == "" {
|
||||
metadata.MinWoxVersion = "2.0.0"
|
||||
}
|
||||
|
||||
// Set supported OS to all platforms by default
|
||||
metadata.SupportedOS = []string{"Windows", "Linux", "Macos"}
|
||||
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
// startScriptPluginMonitoring starts monitoring the user script plugins directory for changes
|
||||
func (m *Manager) startScriptPluginMonitoring(ctx context.Context) {
|
||||
userScriptPluginDirectory := util.GetLocation().GetUserScriptPluginsDirectory()
|
||||
|
||||
// Create file system watcher
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
logger.Error(ctx, fmt.Sprintf("Failed to create script plugin watcher: %s", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
m.scriptPluginWatcher = watcher
|
||||
|
||||
// Add the script plugins directory to the watcher
|
||||
err = watcher.Add(userScriptPluginDirectory)
|
||||
if err != nil {
|
||||
logger.Error(ctx, fmt.Sprintf("Failed to watch script plugin directory: %s", err.Error()))
|
||||
watcher.Close()
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info(ctx, fmt.Sprintf("Started monitoring script plugins directory: %s", userScriptPluginDirectory))
|
||||
|
||||
// Start watching for events
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
logger.Info(ctx, "Script plugin watcher closed")
|
||||
return
|
||||
}
|
||||
m.handleScriptPluginEvent(ctx, event)
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
logger.Info(ctx, "Script plugin watcher error channel closed")
|
||||
return
|
||||
}
|
||||
logger.Error(ctx, fmt.Sprintf("Script plugin watcher error: %s", err.Error()))
|
||||
case <-ctx.Done():
|
||||
logger.Info(ctx, "Script plugin monitoring stopped due to context cancellation")
|
||||
watcher.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleScriptPluginEvent handles file system events for script plugins
|
||||
func (m *Manager) handleScriptPluginEvent(ctx context.Context, event fsnotify.Event) {
|
||||
// Skip non-script files and temporary files
|
||||
fileName := filepath.Base(event.Name)
|
||||
if strings.HasPrefix(fileName, ".") || strings.HasSuffix(fileName, "~") || strings.HasSuffix(fileName, ".tmp") {
|
||||
return
|
||||
}
|
||||
|
||||
// Skip directories
|
||||
if info, err := os.Stat(event.Name); err == nil && info.IsDir() {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info(ctx, fmt.Sprintf("Script plugin file event: %s (%s)", event.Name, event.Op))
|
||||
|
||||
switch event.Op {
|
||||
case fsnotify.Create, fsnotify.Write:
|
||||
// File created or modified - reload the plugin
|
||||
m.debounceScriptPluginReload(ctx, event.Name, "file changed")
|
||||
case fsnotify.Remove, fsnotify.Rename:
|
||||
// File deleted or renamed - unload the plugin
|
||||
m.unloadScriptPluginByPath(ctx, event.Name)
|
||||
case fsnotify.Chmod:
|
||||
// File permissions changed - ignore for now
|
||||
logger.Debug(ctx, fmt.Sprintf("Script plugin file permissions changed: %s", event.Name))
|
||||
}
|
||||
}
|
||||
|
||||
// debounceScriptPluginReload debounces script plugin reload to avoid multiple reloads in a short time
|
||||
func (m *Manager) debounceScriptPluginReload(ctx context.Context, scriptPath, reason string) {
|
||||
fileName := filepath.Base(scriptPath)
|
||||
|
||||
// Cancel existing timer if any
|
||||
if timer, exists := m.scriptReloadTimers.Load(fileName); exists {
|
||||
timer.Stop()
|
||||
}
|
||||
|
||||
// Create new timer
|
||||
timer := time.AfterFunc(2*time.Second, func() {
|
||||
m.reloadScriptPlugin(util.NewTraceContext(), scriptPath, reason)
|
||||
m.scriptReloadTimers.Delete(fileName)
|
||||
})
|
||||
|
||||
m.scriptReloadTimers.Store(fileName, timer)
|
||||
}
|
||||
|
||||
// reloadScriptPlugin reloads a script plugin
|
||||
func (m *Manager) reloadScriptPlugin(ctx context.Context, scriptPath, reason string) {
|
||||
logger.Info(ctx, fmt.Sprintf("Reloading script plugin: %s, reason: %s", scriptPath, reason))
|
||||
|
||||
// Check if file still exists
|
||||
if _, err := os.Stat(scriptPath); os.IsNotExist(err) {
|
||||
logger.Warn(ctx, fmt.Sprintf("Script plugin file no longer exists: %s", scriptPath))
|
||||
return
|
||||
}
|
||||
|
||||
// Parse metadata from the script file
|
||||
metadata, err := m.ParseScriptMetadata(ctx, scriptPath)
|
||||
if err != nil {
|
||||
logger.Error(ctx, fmt.Sprintf("Failed to parse script plugin metadata: %s", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// Find and unload existing plugin instance if any
|
||||
existingInstance, exists := lo.Find(m.instances, func(instance *Instance) bool {
|
||||
return instance.Metadata.Id == metadata.Id
|
||||
})
|
||||
if exists {
|
||||
logger.Info(ctx, fmt.Sprintf("Unloading existing script plugin instance: %s", metadata.Name))
|
||||
m.UnloadPlugin(ctx, existingInstance)
|
||||
}
|
||||
|
||||
// Create metadata with directory for loading
|
||||
userScriptPluginDirectory := util.GetLocation().GetUserScriptPluginsDirectory()
|
||||
virtualDirectory := path.Join(userScriptPluginDirectory, metadata.Id)
|
||||
metadataWithDirectory := MetadataWithDirectory{
|
||||
Metadata: metadata,
|
||||
Directory: virtualDirectory,
|
||||
}
|
||||
|
||||
// Find script plugin host
|
||||
scriptHost, hostExists := lo.Find(AllHosts, func(host Host) bool {
|
||||
return host.GetRuntime(ctx) == PLUGIN_RUNTIME_SCRIPT
|
||||
})
|
||||
if !hostExists {
|
||||
logger.Error(ctx, "Script plugin host not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Load the plugin
|
||||
err = m.loadHostPlugin(ctx, scriptHost, metadataWithDirectory)
|
||||
if err != nil {
|
||||
logger.Error(ctx, fmt.Sprintf("Failed to reload script plugin: %s", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info(ctx, fmt.Sprintf("Successfully reloaded script plugin: %s", metadata.Name))
|
||||
}
|
||||
|
||||
// unloadScriptPluginByPath unloads a script plugin by its file path
|
||||
func (m *Manager) unloadScriptPluginByPath(ctx context.Context, scriptPath string) {
|
||||
fileName := filepath.Base(scriptPath)
|
||||
logger.Info(ctx, fmt.Sprintf("Unloading script plugin: %s", fileName))
|
||||
|
||||
// Find plugin instance by script file name
|
||||
var pluginToUnload *Instance
|
||||
for _, instance := range m.instances {
|
||||
if instance.Metadata.Runtime == string(PLUGIN_RUNTIME_SCRIPT) && instance.Metadata.Entry == fileName {
|
||||
pluginToUnload = instance
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if pluginToUnload != nil {
|
||||
logger.Info(ctx, fmt.Sprintf("Found script plugin to unload: %s", pluginToUnload.Metadata.Name))
|
||||
m.UnloadPlugin(ctx, pluginToUnload)
|
||||
} else {
|
||||
logger.Debug(ctx, fmt.Sprintf("No script plugin found for file: %s", fileName))
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) GetPluginInstances() []*Instance {
|
||||
return m.instances
|
||||
}
|
||||
|
|
|
@ -8,11 +8,12 @@ const (
|
|||
PLUGIN_RUNTIME_GO Runtime = "GO"
|
||||
PLUGIN_RUNTIME_PYTHON Runtime = "PYTHON"
|
||||
PLUGIN_RUNTIME_NODEJS Runtime = "NODEJS"
|
||||
PLUGIN_RUNTIME_SCRIPT Runtime = "SCRIPT"
|
||||
)
|
||||
|
||||
func IsSupportedRuntime(runtime string) bool {
|
||||
runtimeUpper := strings.ToUpper(runtime)
|
||||
return runtimeUpper == string(PLUGIN_RUNTIME_PYTHON) || runtimeUpper == string(PLUGIN_RUNTIME_NODEJS) || runtimeUpper == string(PLUGIN_RUNTIME_GO)
|
||||
return runtimeUpper == string(PLUGIN_RUNTIME_PYTHON) || runtimeUpper == string(PLUGIN_RUNTIME_NODEJS) || runtimeUpper == string(PLUGIN_RUNTIME_GO) || runtimeUpper == string(PLUGIN_RUNTIME_SCRIPT)
|
||||
}
|
||||
|
||||
func ConvertToRuntime(runtime string) Runtime {
|
||||
|
|
|
@ -31,6 +31,24 @@ var pluginTemplates = []pluginTemplate{
|
|||
},
|
||||
}
|
||||
|
||||
var scriptPluginTemplates = []pluginTemplate{
|
||||
{
|
||||
Runtime: plugin.PLUGIN_RUNTIME_SCRIPT,
|
||||
Name: "JavaScript Script Plugin",
|
||||
Url: "template.js",
|
||||
},
|
||||
{
|
||||
Runtime: plugin.PLUGIN_RUNTIME_SCRIPT,
|
||||
Name: "Python Script Plugin",
|
||||
Url: "template.py",
|
||||
},
|
||||
{
|
||||
Runtime: plugin.PLUGIN_RUNTIME_SCRIPT,
|
||||
Name: "Bash Script Plugin",
|
||||
Url: "template.sh",
|
||||
},
|
||||
}
|
||||
|
||||
type LocalPlugin struct {
|
||||
Path string
|
||||
}
|
||||
|
@ -297,12 +315,16 @@ func (w *WPMPlugin) createCommand(ctx context.Context, query plugin.Query) []plu
|
|||
}
|
||||
|
||||
var results []plugin.QueryResult
|
||||
|
||||
// Add regular plugin templates with group
|
||||
for _, template := range pluginTemplates {
|
||||
templateCopy := template // Create a copy for the closure
|
||||
results = append(results, plugin.QueryResult{
|
||||
Id: uuid.NewString(),
|
||||
Title: fmt.Sprintf(i18n.GetI18nManager().TranslateWox(ctx, "plugin_wpm_create_plugin"), string(template.Runtime)),
|
||||
SubTitle: fmt.Sprintf(i18n.GetI18nManager().TranslateWox(ctx, "plugin_wpm_plugin_name"), query.Search),
|
||||
Icon: wpmIcon,
|
||||
Group: "Regular Plugins",
|
||||
Actions: []plugin.QueryResultAction{
|
||||
{
|
||||
Name: "i18n:plugin_wpm_create",
|
||||
|
@ -310,7 +332,34 @@ func (w *WPMPlugin) createCommand(ctx context.Context, query plugin.Query) []plu
|
|||
Action: func(ctx context.Context, actionContext plugin.ActionContext) {
|
||||
pluginName := query.Search
|
||||
util.Go(ctx, "create plugin", func() {
|
||||
w.createPlugin(ctx, template, pluginName, query)
|
||||
w.createPlugin(ctx, templateCopy, pluginName, query)
|
||||
})
|
||||
w.api.ChangeQuery(ctx, common.PlainQuery{
|
||||
QueryType: plugin.QueryTypeInput,
|
||||
QueryText: fmt.Sprintf("%s create ", query.TriggerKeyword),
|
||||
})
|
||||
},
|
||||
},
|
||||
}})
|
||||
}
|
||||
|
||||
// Add script plugin templates with group
|
||||
for _, template := range scriptPluginTemplates {
|
||||
templateCopy := template // Create a copy for the closure
|
||||
results = append(results, plugin.QueryResult{
|
||||
Id: uuid.NewString(),
|
||||
Title: fmt.Sprintf("Create %s", template.Name),
|
||||
SubTitle: fmt.Sprintf(i18n.GetI18nManager().TranslateWox(ctx, "plugin_wpm_plugin_name"), query.Search),
|
||||
Icon: wpmIcon,
|
||||
Group: "Script Plugins",
|
||||
Actions: []plugin.QueryResultAction{
|
||||
{
|
||||
Name: "i18n:plugin_wpm_create",
|
||||
PreventHideAfterAction: true,
|
||||
Action: func(ctx context.Context, actionContext plugin.ActionContext) {
|
||||
pluginName := query.Search
|
||||
util.Go(ctx, "create script plugin", func() {
|
||||
w.createScriptPluginWithTemplate(ctx, templateCopy, pluginName, query)
|
||||
})
|
||||
w.api.ChangeQuery(ctx, common.PlainQuery{
|
||||
QueryType: plugin.QueryTypeInput,
|
||||
|
@ -425,7 +474,7 @@ func (w *WPMPlugin) listDevCommand(ctx context.Context) []plugin.QueryResult {
|
|||
PreviewType: plugin.WoxPreviewTypeMarkdown,
|
||||
PreviewData: fmt.Sprintf(`
|
||||
- **Directory**: %s
|
||||
- **Name**: %s
|
||||
- **Name**: %s
|
||||
- **Description**: %s
|
||||
- **Author**: %s
|
||||
- **Website**: %s
|
||||
|
@ -693,3 +742,102 @@ func (w *WPMPlugin) reloadLocalDistPlugin(ctx context.Context, localPlugin plugi
|
|||
w.api.Notify(ctx, fmt.Sprintf(i18n.GetI18nManager().TranslateWox(ctx, "plugin_wpm_reload_success"), localPlugin.Metadata.Name, reason))
|
||||
return nil
|
||||
}
|
||||
|
||||
// createScriptPluginWithTemplate creates a script plugin from a specific template
|
||||
func (w *WPMPlugin) createScriptPluginWithTemplate(ctx context.Context, template pluginTemplate, pluginName string, query plugin.Query) {
|
||||
w.creatingProcess = "Creating script plugin..."
|
||||
|
||||
// Get user script plugins directory
|
||||
userScriptPluginDirectory := util.GetLocation().GetUserScriptPluginsDirectory()
|
||||
|
||||
// Ensure the directory exists
|
||||
if err := util.GetLocation().EnsureDirectoryExist(userScriptPluginDirectory); err != nil {
|
||||
w.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("Failed to create user script plugin directory: %s", err.Error()))
|
||||
w.creatingProcess = fmt.Sprintf("Failed to create script plugin directory: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Use the template file specified in the template
|
||||
templateFile := template.Url // We store the template filename in the Url field
|
||||
var fileExtension string
|
||||
|
||||
switch templateFile {
|
||||
case "template.js":
|
||||
fileExtension = ".js"
|
||||
case "template.py":
|
||||
fileExtension = ".py"
|
||||
case "template.sh":
|
||||
fileExtension = ".sh"
|
||||
default:
|
||||
fileExtension = ".js" // Default fallback
|
||||
}
|
||||
|
||||
w.creatingProcess = "Copying template..."
|
||||
|
||||
// Read template from embedded resources
|
||||
scriptTemplateDirectory := util.GetLocation().GetScriptPluginTemplatesDirectory()
|
||||
templatePath := path.Join(scriptTemplateDirectory, templateFile)
|
||||
|
||||
templateContent, err := os.ReadFile(templatePath)
|
||||
if err != nil {
|
||||
w.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("Failed to read template file: %s", err.Error()))
|
||||
w.creatingProcess = fmt.Sprintf("Failed to read template: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Generate script file name
|
||||
cleanPluginName := strings.TrimSpace(pluginName)
|
||||
if cleanPluginName == "" {
|
||||
cleanPluginName = "my-plugin"
|
||||
}
|
||||
|
||||
scriptFileName := strings.ReplaceAll(strings.ToLower(cleanPluginName), " ", "-") + fileExtension
|
||||
scriptFilePath := path.Join(userScriptPluginDirectory, scriptFileName)
|
||||
|
||||
// Check if file already exists
|
||||
if _, err := os.Stat(scriptFilePath); err == nil {
|
||||
w.api.Notify(ctx, fmt.Sprintf("Script plugin file already exists: %s", scriptFileName))
|
||||
w.creatingProcess = ""
|
||||
return
|
||||
}
|
||||
|
||||
// Replace template variables
|
||||
templateString := string(templateContent)
|
||||
pluginId := strings.ReplaceAll(strings.ToLower(cleanPluginName), " ", "-")
|
||||
triggerKeyword := strings.ToLower(strings.ReplaceAll(cleanPluginName, " ", ""))
|
||||
if len(triggerKeyword) > 10 {
|
||||
triggerKeyword = triggerKeyword[:10]
|
||||
}
|
||||
|
||||
// Replace template placeholders
|
||||
templateString = strings.ReplaceAll(templateString, "script-plugin-template", pluginId)
|
||||
templateString = strings.ReplaceAll(templateString, "python-script-template", pluginId)
|
||||
templateString = strings.ReplaceAll(templateString, "bash-script-template", pluginId)
|
||||
templateString = strings.ReplaceAll(templateString, "Script Plugin Template", cleanPluginName)
|
||||
templateString = strings.ReplaceAll(templateString, "Python Script Template", cleanPluginName)
|
||||
templateString = strings.ReplaceAll(templateString, "Bash Script Template", cleanPluginName)
|
||||
templateString = strings.ReplaceAll(templateString, "spt", triggerKeyword)
|
||||
templateString = strings.ReplaceAll(templateString, "pst", triggerKeyword)
|
||||
templateString = strings.ReplaceAll(templateString, "bst", triggerKeyword)
|
||||
templateString = strings.ReplaceAll(templateString, "A template for Wox script plugins", fmt.Sprintf("A script plugin for %s", cleanPluginName))
|
||||
templateString = strings.ReplaceAll(templateString, "A Python template for Wox script plugins", fmt.Sprintf("A script plugin for %s", cleanPluginName))
|
||||
templateString = strings.ReplaceAll(templateString, "A Bash template for Wox script plugins", fmt.Sprintf("A script plugin for %s", cleanPluginName))
|
||||
|
||||
// Write the script file
|
||||
err = os.WriteFile(scriptFilePath, []byte(templateString), 0755)
|
||||
if err != nil {
|
||||
w.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("Failed to write script file: %s", err.Error()))
|
||||
w.creatingProcess = fmt.Sprintf("Failed to create script file: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
w.creatingProcess = ""
|
||||
w.api.Notify(ctx, fmt.Sprintf("Script plugin created successfully: %s", scriptFileName))
|
||||
w.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("Created script plugin: %s", scriptFilePath))
|
||||
|
||||
// Change query to show script plugins or open the file
|
||||
w.api.ChangeQuery(ctx, common.PlainQuery{
|
||||
QueryType: plugin.QueryTypeInput,
|
||||
QueryText: fmt.Sprintf("%s ", triggerKeyword),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -28,6 +28,9 @@ var appIconWindows []byte
|
|||
//go:embed others
|
||||
var OthersFS embed.FS
|
||||
|
||||
//go:embed script_plugin_templates
|
||||
var ScriptPluginTemplatesFS embed.FS
|
||||
|
||||
var embedThemes = []string{}
|
||||
|
||||
func Extract(ctx context.Context) error {
|
||||
|
@ -47,6 +50,11 @@ func Extract(ctx context.Context) error {
|
|||
return othersErr
|
||||
}
|
||||
|
||||
scriptPluginTemplatesErr := extractFiles(ctx, ScriptPluginTemplatesFS, util.GetLocation().GetScriptPluginTemplatesDirectory(), "script_plugin_templates", false)
|
||||
if scriptPluginTemplatesErr != nil {
|
||||
return scriptPluginTemplatesErr
|
||||
}
|
||||
|
||||
themeErr := parseThemes(ctx)
|
||||
if themeErr != nil {
|
||||
return themeErr
|
||||
|
|
|
@ -0,0 +1,171 @@
|
|||
#!/usr/bin/env node
|
||||
// Required parameters:
|
||||
// @wox.id script-plugin-template
|
||||
// @wox.name Script Plugin Template
|
||||
// @wox.keywords spt
|
||||
|
||||
// Optional parameters:
|
||||
// @wox.icon 📝
|
||||
// @wox.version 1.0.0
|
||||
// @wox.author Wox Team
|
||||
// @wox.description A template for Wox script plugins
|
||||
// @wox.minWoxVersion 2.0.0
|
||||
|
||||
/**
|
||||
* Wox Script Plugin Template
|
||||
*
|
||||
* This is a template for creating Wox script plugins.
|
||||
* Script plugins are single-file plugins that are executed once per query.
|
||||
*
|
||||
* Communication with Wox is done via JSON-RPC over stdin/stdout.
|
||||
*
|
||||
* Available methods:
|
||||
* - query: Process user queries and return results
|
||||
* - action: Handle user selection of a result
|
||||
*/
|
||||
|
||||
// Parse input from command line or stdin
|
||||
let request;
|
||||
try {
|
||||
if (process.argv.length > 2) {
|
||||
// From command line arguments
|
||||
request = JSON.parse(process.argv[2]);
|
||||
} else {
|
||||
// From stdin
|
||||
const data = require('fs').readFileSync(0, 'utf-8');
|
||||
request = JSON.parse(data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
error: {
|
||||
code: -32700,
|
||||
message: "Parse error",
|
||||
data: e.message
|
||||
},
|
||||
id: null
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate JSON-RPC request
|
||||
if (request.jsonrpc !== "2.0") {
|
||||
console.log(JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
error: {
|
||||
code: -32600,
|
||||
message: "Invalid Request",
|
||||
data: "Expected JSON-RPC 2.0"
|
||||
},
|
||||
id: request.id || null
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Handle different methods
|
||||
switch (request.method) {
|
||||
case "query":
|
||||
handleQuery(request);
|
||||
break;
|
||||
case "action":
|
||||
handleAction(request);
|
||||
break;
|
||||
default:
|
||||
// Method not found
|
||||
console.log(JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
error: {
|
||||
code: -32601,
|
||||
message: "Method not found",
|
||||
data: `Method '${request.method}' not supported`
|
||||
},
|
||||
id: request.id
|
||||
}));
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle query requests
|
||||
* @param {Object} request - The JSON-RPC request
|
||||
*/
|
||||
function handleQuery(request) {
|
||||
const query = request.params.search || "";
|
||||
|
||||
// Generate results based on the query
|
||||
const results = [
|
||||
{
|
||||
title: `You searched for: ${query}`,
|
||||
subtitle: "This is a template result",
|
||||
score: 100,
|
||||
action: {
|
||||
id: "example-action",
|
||||
data: query
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Another result",
|
||||
subtitle: "With a different action",
|
||||
score: 90,
|
||||
action: {
|
||||
id: "open-url",
|
||||
data: "https://github.com/Wox-launcher/Wox"
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// Return results
|
||||
console.log(JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
result: {
|
||||
items: results
|
||||
},
|
||||
id: request.id
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle action requests
|
||||
* @param {Object} request - The JSON-RPC request
|
||||
*/
|
||||
function handleAction(request) {
|
||||
const actionId = request.params.id;
|
||||
const actionData = request.params.data;
|
||||
|
||||
// Handle different action types
|
||||
switch (actionId) {
|
||||
case "example-action":
|
||||
// Example action that returns a message
|
||||
console.log(JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
result: {
|
||||
action: "notify",
|
||||
message: `You selected: ${actionData}`
|
||||
},
|
||||
id: request.id
|
||||
}));
|
||||
break;
|
||||
case "open-url":
|
||||
// Open URL action
|
||||
console.log(JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
result: {
|
||||
action: "open-url",
|
||||
url: actionData
|
||||
},
|
||||
id: request.id
|
||||
}));
|
||||
break;
|
||||
default:
|
||||
// Unknown action
|
||||
console.log(JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
error: {
|
||||
code: -32000,
|
||||
message: "Unknown action",
|
||||
data: `Action '${actionId}' not supported`
|
||||
},
|
||||
id: request.id
|
||||
}));
|
||||
break;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
#!/usr/bin/env python3
|
||||
# Required parameters:
|
||||
# @wox.id python-script-template
|
||||
# @wox.name Python Script Template
|
||||
# @wox.keywords pst
|
||||
|
||||
# Optional parameters:
|
||||
# @wox.icon 🐍
|
||||
# @wox.version 1.0.0
|
||||
# @wox.author Wox Team
|
||||
# @wox.description A Python template for Wox script plugins
|
||||
# @wox.minWoxVersion 2.0.0
|
||||
|
||||
"""
|
||||
Wox Python Script Plugin Template
|
||||
|
||||
This is a template for creating Wox script plugins in Python.
|
||||
Script plugins are single-file plugins that are executed once per query.
|
||||
|
||||
Communication with Wox is done via JSON-RPC over stdin/stdout.
|
||||
|
||||
Available methods:
|
||||
- query: Process user queries and return results
|
||||
- action: Handle user selection of a result
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
|
||||
|
||||
def handle_query(params, request_id):
|
||||
"""
|
||||
Handle query requests
|
||||
|
||||
Args:
|
||||
params: The parameters from the JSON-RPC request
|
||||
request_id: The ID of the JSON-RPC request
|
||||
|
||||
Returns:
|
||||
A JSON-RPC response with the query results
|
||||
"""
|
||||
query = params.get("search", "")
|
||||
|
||||
# Generate results based on the query
|
||||
results = [
|
||||
{
|
||||
"title": f"You searched for: {query}",
|
||||
"subtitle": "This is a template result",
|
||||
"score": 100,
|
||||
"action": {
|
||||
"id": "example-action",
|
||||
"data": query
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Another result",
|
||||
"subtitle": "With a different action",
|
||||
"score": 90,
|
||||
"action": {
|
||||
"id": "open-url",
|
||||
"data": "https://github.com/Wox-launcher/Wox"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
# Return results
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"items": results
|
||||
},
|
||||
"id": request_id
|
||||
}
|
||||
|
||||
|
||||
def handle_action(params, request_id):
|
||||
"""
|
||||
Handle action requests
|
||||
|
||||
Args:
|
||||
params: The parameters from the JSON-RPC request
|
||||
request_id: The ID of the JSON-RPC request
|
||||
|
||||
Returns:
|
||||
A JSON-RPC response with the action result
|
||||
"""
|
||||
action_id = params.get("id", "")
|
||||
action_data = params.get("data", "")
|
||||
|
||||
# Handle different action types
|
||||
if action_id == "example-action":
|
||||
# Example action that returns a message
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"action": "notify",
|
||||
"message": f"You selected: {action_data}"
|
||||
},
|
||||
"id": request_id
|
||||
}
|
||||
elif action_id == "open-url":
|
||||
# Open URL action
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"action": "open-url",
|
||||
"url": action_data
|
||||
},
|
||||
"id": request_id
|
||||
}
|
||||
else:
|
||||
# Unknown action
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": -32000,
|
||||
"message": "Unknown action",
|
||||
"data": f"Action '{action_id}' not supported"
|
||||
},
|
||||
"id": request_id
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for the script plugin"""
|
||||
# Parse input
|
||||
try:
|
||||
if len(sys.argv) > 1:
|
||||
# From command line arguments
|
||||
request = json.loads(sys.argv[1])
|
||||
else:
|
||||
# From stdin
|
||||
request = json.loads(sys.stdin.read())
|
||||
except json.JSONDecodeError as e:
|
||||
# Return parse error
|
||||
print(json.dumps({
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": -32700,
|
||||
"message": "Parse error",
|
||||
"data": str(e)
|
||||
},
|
||||
"id": None
|
||||
}))
|
||||
return 1
|
||||
|
||||
# Validate JSON-RPC request
|
||||
if request.get("jsonrpc") != "2.0":
|
||||
print(json.dumps({
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": -32600,
|
||||
"message": "Invalid Request",
|
||||
"data": "Expected JSON-RPC 2.0"
|
||||
},
|
||||
"id": request.get("id")
|
||||
}))
|
||||
return 1
|
||||
|
||||
# Handle different methods
|
||||
method = request.get("method")
|
||||
params = request.get("params", {})
|
||||
request_id = request.get("id")
|
||||
|
||||
if method == "query":
|
||||
response = handle_query(params, request_id)
|
||||
elif method == "action":
|
||||
response = handle_action(params, request_id)
|
||||
else:
|
||||
# Method not found
|
||||
response = {
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": -32601,
|
||||
"message": "Method not found",
|
||||
"data": f"Method '{method}' not supported"
|
||||
},
|
||||
"id": request_id
|
||||
}
|
||||
|
||||
# Output response
|
||||
print(json.dumps(response))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
|
@ -0,0 +1,152 @@
|
|||
#!/bin/bash
|
||||
# Required parameters:
|
||||
# @wox.id bash-script-template
|
||||
# @wox.name Bash Script Template
|
||||
# @wox.keywords bst
|
||||
|
||||
# Optional parameters:
|
||||
# @wox.icon 🐚
|
||||
# @wox.version 1.0.0
|
||||
# @wox.author Wox Team
|
||||
# @wox.description A Bash template for Wox script plugins
|
||||
# @wox.minWoxVersion 2.0.0
|
||||
|
||||
# Wox Bash Script Plugin Template
|
||||
#
|
||||
# This is a template for creating Wox script plugins in Bash.
|
||||
# Script plugins are single-file plugins that are executed once per query.
|
||||
#
|
||||
# Communication with Wox is done via JSON-RPC over stdin/stdout.
|
||||
#
|
||||
# Available methods:
|
||||
# - query: Process user queries and return results
|
||||
# - action: Handle user selection of a result
|
||||
|
||||
# Read input from command line or stdin
|
||||
if [ $# -gt 0 ]; then
|
||||
# From command line arguments
|
||||
REQUEST="$1"
|
||||
else
|
||||
# From stdin
|
||||
REQUEST=$(cat)
|
||||
fi
|
||||
|
||||
# Parse JSON-RPC request
|
||||
# Note: This is a simple JSON parser for Bash
|
||||
# For more complex JSON parsing, consider using jq if available
|
||||
METHOD=$(echo "$REQUEST" | grep -o '"method"[^,}]*' | sed 's/"method"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
|
||||
ID=$(echo "$REQUEST" | grep -o '"id"[^,}]*' | sed 's/"id"[[:space:]]*:[[:space:]]*\([^,}]*\).*/\1/')
|
||||
JSONRPC=$(echo "$REQUEST" | grep -o '"jsonrpc"[^,}]*' | sed 's/"jsonrpc"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
|
||||
|
||||
# Extract params based on method
|
||||
if [ "$METHOD" = "query" ]; then
|
||||
SEARCH=$(echo "$REQUEST" | grep -o '"search"[^,}]*' | sed 's/"search"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
|
||||
elif [ "$METHOD" = "action" ]; then
|
||||
ACTION_ID=$(echo "$REQUEST" | grep -o '"id"[^,}]*' | sed 's/"id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
|
||||
ACTION_DATA=$(echo "$REQUEST" | grep -o '"data"[^,}]*' | sed 's/"data"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
|
||||
fi
|
||||
|
||||
# Validate JSON-RPC version
|
||||
if [ "$JSONRPC" != "2.0" ]; then
|
||||
echo '{"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request","data":"Expected JSON-RPC 2.0"},"id":null}'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Handle different methods
|
||||
case "$METHOD" in
|
||||
"query")
|
||||
# Handle query request
|
||||
# Generate results based on the query
|
||||
cat << EOF
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"items": [
|
||||
{
|
||||
"title": "You searched for: $SEARCH",
|
||||
"subtitle": "This is a template result",
|
||||
"score": 100,
|
||||
"action": {
|
||||
"id": "example-action",
|
||||
"data": "$SEARCH"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "System Information",
|
||||
"subtitle": "Show system information",
|
||||
"score": 90,
|
||||
"action": {
|
||||
"id": "system-info",
|
||||
"data": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"id": $ID
|
||||
}
|
||||
EOF
|
||||
;;
|
||||
"action")
|
||||
# Handle action request
|
||||
case "$ACTION_ID" in
|
||||
"example-action")
|
||||
# Example action that returns a message
|
||||
cat << EOF
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"action": "notify",
|
||||
"message": "You selected: $ACTION_DATA"
|
||||
},
|
||||
"id": $ID
|
||||
}
|
||||
EOF
|
||||
;;
|
||||
"system-info")
|
||||
# Get system information
|
||||
OS=$(uname -s)
|
||||
HOSTNAME=$(hostname)
|
||||
UPTIME=$(uptime)
|
||||
|
||||
cat << EOF
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"action": "notify",
|
||||
"message": "System: $OS\nHostname: $HOSTNAME\nUptime: $UPTIME"
|
||||
},
|
||||
"id": $ID
|
||||
}
|
||||
EOF
|
||||
;;
|
||||
*)
|
||||
# Unknown action
|
||||
cat << EOF
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": -32000,
|
||||
"message": "Unknown action",
|
||||
"data": "Action '$ACTION_ID' not supported"
|
||||
},
|
||||
"id": $ID
|
||||
}
|
||||
EOF
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
# Method not found
|
||||
cat << EOF
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": -32601,
|
||||
"message": "Method not found",
|
||||
"data": "Method '$METHOD' not supported"
|
||||
},
|
||||
"id": $ID
|
||||
}
|
||||
EOF
|
||||
;;
|
||||
esac
|
|
@ -103,6 +103,12 @@ func (l *Location) Init() error {
|
|||
if directoryErr := l.EnsureDirectoryExist(l.GetOthersDirectory()); directoryErr != nil {
|
||||
return directoryErr
|
||||
}
|
||||
if directoryErr := l.EnsureDirectoryExist(l.GetScriptPluginTemplatesDirectory()); directoryErr != nil {
|
||||
return directoryErr
|
||||
}
|
||||
if directoryErr := l.EnsureDirectoryExist(l.GetUserScriptPluginsDirectory()); directoryErr != nil {
|
||||
return directoryErr
|
||||
}
|
||||
if directoryErr := l.EnsureDirectoryExist(l.GetCacheDirectory()); directoryErr != nil {
|
||||
return directoryErr
|
||||
}
|
||||
|
@ -147,6 +153,10 @@ func (l *Location) GetPluginDirectory() string {
|
|||
return path.Join(l.userDataDirectory, "plugins")
|
||||
}
|
||||
|
||||
func (l *Location) GetUserScriptPluginsDirectory() string {
|
||||
return path.Join(l.GetPluginDirectory(), "scripts")
|
||||
}
|
||||
|
||||
func (l *Location) GetThemeDirectory() string {
|
||||
return path.Join(l.userDataDirectory, "themes")
|
||||
}
|
||||
|
@ -183,6 +193,10 @@ func (l *Location) GetOthersDirectory() string {
|
|||
return path.Join(l.woxDataDirectory, "others")
|
||||
}
|
||||
|
||||
func (l *Location) GetScriptPluginTemplatesDirectory() string {
|
||||
return path.Join(l.woxDataDirectory, "script_plugin_templates")
|
||||
}
|
||||
|
||||
func (l *Location) GetCacheDirectory() string {
|
||||
return path.Join(l.woxDataDirectory, "cache")
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue