feat(script_plugin): enhance script plugin functionality

- Increased notification display duration from 3 to 8 seconds in API implementation.
- Refactored script execution logic in host_script.go to improve error handling and streamline execution.
- Added checks for existing script plugins in WPM plugin creation process, providing options to open or overwrite.
- Enhanced user feedback with localized messages for plugin creation and actions.
- Updated script templates (JavaScript, Python, Bash) to include environment variable documentation and improved action handling.
- Improved UI responsiveness in Flutter by implementing AnimatedSwitcher for list view updates.
- Added validation for viewport height in WoxListController to prevent scrolling issues during initialization.
This commit is contained in:
qianlifeng 2025-05-25 21:48:15 +08:00
parent d488d98eec
commit 21d8d83a1f
No known key found for this signature in database
13 changed files with 334 additions and 268 deletions

View File

@ -65,7 +65,7 @@ func (a *APIImpl) Notify(ctx context.Context, message string) {
GetPluginManager().GetUI().Notify(ctx, common.NotifyMsg{
PluginId: a.pluginInstance.Metadata.Id,
Text: a.GetTranslation(ctx, message),
DisplaySeconds: 3,
DisplaySeconds: 8,
})
}

View File

@ -11,6 +11,7 @@ import (
"time"
"wox/plugin"
"wox/util"
"wox/util/shell"
)
func init() {
@ -110,52 +111,12 @@ func (sp *ScriptPlugin) Query(ctx context.Context, query plugin.Query) []plugin.
// 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()
// Execute script and get raw response
response, err := sp.executeScriptRaw(ctx, request)
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 {
@ -231,52 +192,70 @@ func (sp *ScriptPlugin) executeAction(ctx context.Context, actionData map[string
}
}
// executeScriptAction executes the script for action requests
func (sp *ScriptPlugin) executeScriptAction(ctx context.Context, request map[string]interface{}) error {
// executeScriptRaw executes the script with the given JSON-RPC request and returns the raw response
func (sp *ScriptPlugin) executeScriptRaw(ctx context.Context, request map[string]interface{}) (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)
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 err
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:]...)
// Prepare command
var cmd *exec.Cmd
if interpreter != "" {
cmd = exec.CommandContext(timeoutCtx, interpreter, sp.scriptPath)
} else {
cmd = exec.CommandContext(timeoutCtx, sp.scriptPath)
}
// Set up environment variables for script plugins
cmd.Env = append(os.Environ(),
"WOX_DIRECTORY_USER_SCRIPT_PLUGINS="+util.GetLocation().GetUserScriptPluginsDirectory(),
"WOX_DIRECTORY_USER_DATA="+util.GetLocation().GetUserDataDirectory(),
"WOX_DIRECTORY_WOX_DATA="+util.GetLocation().GetWoxDataDirectory(),
"WOX_DIRECTORY_PLUGINS="+util.GetLocation().GetPluginDirectory(),
"WOX_DIRECTORY_THEMES="+util.GetLocation().GetThemeDirectory(),
)
// Set up stdin with the JSON-RPC request
cmd.Stdin = strings.NewReader(string(requestJSON))
// Execute script
output, err := cmd.Output()
if err != nil {
return fmt.Errorf("script execution failed: %w", err)
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 fmt.Errorf("failed to parse script response: %w", err)
return nil, 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)
return nil, fmt.Errorf("script returned error: %v", errorData)
}
return response, nil
}
// executeScriptAction executes the script for action requests
func (sp *ScriptPlugin) executeScriptAction(ctx context.Context, request map[string]interface{}) error {
// Execute script and get raw response
response, err := sp.executeScriptRaw(ctx, request)
if err != nil {
return err
}
// Handle action result if present
@ -300,6 +279,16 @@ func (sp *ScriptPlugin) handleActionResult(ctx context.Context, result map[strin
// TODO: Implement URL opening functionality
util.GetLogger().Info(ctx, fmt.Sprintf("Script plugin %s requested to open URL: %s", sp.metadata.Name, url))
}
case "open-directory":
path := getStringFromMap(result, "path")
if path != "" {
// Open directory using shell.Open
if err := shell.Open(path); err != nil {
util.GetLogger().Error(ctx, fmt.Sprintf("Script plugin %s failed to open directory %s: %s", sp.metadata.Name, path, err.Error()))
} else {
util.GetLogger().Info(ctx, fmt.Sprintf("Script plugin %s opened directory: %s", sp.metadata.Name, path))
}
}
case "notify":
message := getStringFromMap(result, "message")
if message != "" {

View File

@ -61,7 +61,6 @@ func init() {
type WPMPlugin struct {
api plugin.API
creatingProcess string
localPluginDirectories []string
localPlugins []localPlugin
reloadPluginTimers *util.HashMap[string, *time.Timer]
@ -298,18 +297,15 @@ func (w *WPMPlugin) Query(ctx context.Context, query plugin.Query) []plugin.Quer
}
func (w *WPMPlugin) createCommand(ctx context.Context, query plugin.Query) []plugin.QueryResult {
if w.creatingProcess != "" {
// Check if user has entered a plugin name
pluginName := strings.TrimSpace(query.Search)
if pluginName == "" {
return []plugin.QueryResult{
{
Id: uuid.NewString(),
Title: w.creatingProcess,
SubTitle: "i18n:plugin_wpm_please_wait",
Icon: wpmIcon,
RefreshInterval: 300,
OnRefresh: func(ctx context.Context, current plugin.RefreshableResult) plugin.RefreshableResult {
current.Title = w.creatingProcess
return current
},
Id: uuid.NewString(),
Title: "i18n:plugin_wpm_enter_plugin_name",
SubTitle: "i18n:plugin_wpm_enter_plugin_name_subtitle",
Icon: wpmIcon,
},
}
}
@ -346,13 +342,48 @@ func (w *WPMPlugin) createCommand(ctx context.Context, query plugin.Query) []plu
// 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{
// Check if script plugin already exists
exists, fileName := w.checkScriptPluginExists(pluginName, template.Url)
var title, subtitle string
var actions []plugin.QueryResultAction
if exists {
title = fmt.Sprintf("⚠️ %s (Already exists)", template.Name)
subtitle = fmt.Sprintf("File '%s' already exists. Choose an action below.", fileName)
// When file exists, provide actions to open or overwrite the existing file
actions = []plugin.QueryResultAction{
{
Name: "Open existing file",
Action: func(ctx context.Context, actionContext plugin.ActionContext) {
userScriptPluginDirectory := util.GetLocation().GetUserScriptPluginsDirectory()
scriptFilePath := path.Join(userScriptPluginDirectory, fileName)
openErr := shell.Open(scriptFilePath)
if openErr != nil {
w.api.Notify(ctx, fmt.Sprintf("Failed to open file: %s", openErr.Error()))
}
},
},
{
Name: "Overwrite existing file",
PreventHideAfterAction: true,
Action: func(ctx context.Context, actionContext plugin.ActionContext) {
pluginName := query.Search
util.Go(ctx, "overwrite script plugin", func() {
w.createScriptPluginWithTemplate(ctx, templateCopy, pluginName, query)
})
w.api.ChangeQuery(ctx, common.PlainQuery{
QueryType: plugin.QueryTypeInput,
QueryText: fmt.Sprintf("%s create ", query.TriggerKeyword),
})
},
},
}
} else {
title = fmt.Sprintf("Create %s", template.Name)
subtitle = fmt.Sprintf(i18n.GetI18nManager().TranslateWox(ctx, "plugin_wpm_plugin_name"), query.Search)
actions = []plugin.QueryResultAction{
{
Name: "i18n:plugin_wpm_create",
PreventHideAfterAction: true,
@ -367,7 +398,17 @@ func (w *WPMPlugin) createCommand(ctx context.Context, query plugin.Query) []plu
})
},
},
}})
}
}
results = append(results, plugin.QueryResult{
Id: uuid.NewString(),
Title: title,
SubTitle: subtitle,
Icon: wpmIcon,
Group: "Script Plugins",
Actions: actions,
})
}
return results
@ -602,33 +643,33 @@ func (w *WPMPlugin) removeDevCommand(ctx context.Context, query plugin.Query) []
}
func (w *WPMPlugin) createPlugin(ctx context.Context, template pluginTemplate, pluginName string, query plugin.Query) {
w.creatingProcess = "i18n:plugin_wpm_downloading_template"
w.api.Notify(ctx, "i18n:plugin_wpm_downloading_template")
tempPluginDirectory := path.Join(os.TempDir(), uuid.NewString())
if err := util.GetLocation().EnsureDirectoryExist(tempPluginDirectory); err != nil {
w.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("Failed to create temp plugin directory: %s", err.Error()))
w.creatingProcess = fmt.Sprintf(i18n.GetI18nManager().TranslateWox(ctx, "plugin_wpm_create_temp_dir_failed"), err.Error())
w.api.Notify(ctx, fmt.Sprintf(i18n.GetI18nManager().TranslateWox(ctx, "plugin_wpm_create_temp_dir_failed"), err.Error()))
return
}
w.creatingProcess = fmt.Sprintf(i18n.GetI18nManager().TranslateWox(ctx, "plugin_wpm_downloading_template_to"), template.Runtime, tempPluginDirectory)
w.api.Notify(ctx, fmt.Sprintf(i18n.GetI18nManager().TranslateWox(ctx, "plugin_wpm_downloading_template_to"), template.Runtime, tempPluginDirectory))
tempZipPath := path.Join(tempPluginDirectory, "template.zip")
err := util.HttpDownload(ctx, template.Url, tempZipPath)
if err != nil {
w.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("Failed to download template: %s", err.Error()))
w.creatingProcess = fmt.Sprintf(i18n.GetI18nManager().TranslateWox(ctx, "plugin_wpm_download_template_failed"), err.Error())
w.api.Notify(ctx, fmt.Sprintf(i18n.GetI18nManager().TranslateWox(ctx, "plugin_wpm_download_template_failed"), err.Error()))
return
}
w.creatingProcess = "i18n:plugin_wpm_extracting_template"
w.api.Notify(ctx, "i18n:plugin_wpm_extracting_template")
err = util.Unzip(tempZipPath, tempPluginDirectory)
if err != nil {
w.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("Failed to extract template: %s", err.Error()))
w.creatingProcess = fmt.Sprintf(i18n.GetI18nManager().TranslateWox(ctx, "plugin_wpm_extract_template_failed"), err.Error())
w.api.Notify(ctx, fmt.Sprintf(i18n.GetI18nManager().TranslateWox(ctx, "plugin_wpm_extract_template_failed"), err.Error()))
return
}
w.creatingProcess = "i18n:plugin_wpm_choose_directory_prompt"
w.api.Notify(ctx, "i18n:plugin_wpm_choose_directory_prompt")
pluginDirectories := plugin.GetPluginManager().GetUI().PickFiles(ctx, common.PickFilesParams{IsDirectory: true})
if len(pluginDirectories) == 0 {
w.api.Notify(ctx, "You need to choose a directory to create the plugin")
@ -640,7 +681,7 @@ func (w *WPMPlugin) createPlugin(ctx context.Context, template pluginTemplate, p
cpErr := cp.Copy(path.Join(tempPluginDirectory, template.Name+"-main"), pluginDirectory)
if cpErr != nil {
w.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("Failed to copy template: %s", cpErr.Error()))
w.creatingProcess = fmt.Sprintf("Failed to copy template: %s", cpErr.Error())
w.api.Notify(ctx, fmt.Sprintf("Failed to copy template: %s", cpErr.Error()))
return
}
@ -649,7 +690,7 @@ func (w *WPMPlugin) createPlugin(ctx context.Context, template pluginTemplate, p
pluginJson, readErr := os.ReadFile(pluginJsonPath)
if readErr != nil {
w.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("Failed to read plugin.json: %s", readErr.Error()))
w.creatingProcess = fmt.Sprintf("Failed to read plugin.json: %s", readErr.Error())
w.api.Notify(ctx, fmt.Sprintf("Failed to read plugin.json: %s", readErr.Error()))
return
}
@ -662,7 +703,7 @@ func (w *WPMPlugin) createPlugin(ctx context.Context, template pluginTemplate, p
writeErr := os.WriteFile(pluginJsonPath, []byte(pluginJsonString), 0644)
if writeErr != nil {
w.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("Failed to write plugin.json: %s", writeErr.Error()))
w.creatingProcess = fmt.Sprintf("Failed to write plugin.json: %s", writeErr.Error())
w.api.Notify(ctx, fmt.Sprintf("Failed to write plugin.json: %s", writeErr.Error()))
return
}
@ -672,7 +713,7 @@ func (w *WPMPlugin) createPlugin(ctx context.Context, template pluginTemplate, p
packageJson, readPackageErr := os.ReadFile(packageJsonPath)
if readPackageErr != nil {
w.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("Failed to read package.json: %s", readPackageErr.Error()))
w.creatingProcess = fmt.Sprintf("Failed to read package.json: %s", readPackageErr.Error())
w.api.Notify(ctx, fmt.Sprintf("Failed to read package.json: %s", readPackageErr.Error()))
return
}
@ -683,15 +724,15 @@ func (w *WPMPlugin) createPlugin(ctx context.Context, template pluginTemplate, p
writePackageErr := os.WriteFile(packageJsonPath, []byte(packageJsonString), 0644)
if writePackageErr != nil {
w.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("Failed to write package.json: %s", writePackageErr.Error()))
w.creatingProcess = fmt.Sprintf("Failed to write package.json: %s", writePackageErr.Error())
w.api.Notify(ctx, fmt.Sprintf("Failed to write package.json: %s", writePackageErr.Error()))
return
}
}
w.creatingProcess = ""
w.localPluginDirectories = append(w.localPluginDirectories, pluginDirectory)
w.saveLocalPluginDirectories(ctx)
w.loadDevPlugin(ctx, pluginDirectory)
w.api.Notify(ctx, fmt.Sprintf("Plugin created successfully: %s", pluginName))
w.api.ChangeQuery(ctx, common.PlainQuery{
QueryType: plugin.QueryTypeInput,
QueryText: fmt.Sprintf("%s dev ", query.TriggerKeyword),
@ -743,9 +784,38 @@ func (w *WPMPlugin) reloadLocalDistPlugin(ctx context.Context, localPlugin plugi
return nil
}
// checkScriptPluginExists checks if a script plugin with the given name already exists
func (w *WPMPlugin) checkScriptPluginExists(pluginName string, templateFile string) (bool, string) {
cleanPluginName := strings.TrimSpace(pluginName)
if cleanPluginName == "" {
return false, ""
}
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
}
userScriptPluginDirectory := util.GetLocation().GetUserScriptPluginsDirectory()
scriptFileName := strings.ReplaceAll(strings.ToLower(cleanPluginName), " ", "-") + fileExtension
scriptFilePath := path.Join(userScriptPluginDirectory, scriptFileName)
if _, err := os.Stat(scriptFilePath); err == nil {
return true, scriptFileName
}
return false, scriptFileName
}
// 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..."
w.api.Notify(ctx, "i18n:plugin_wpm_creating_script_plugin")
// Get user script plugins directory
userScriptPluginDirectory := util.GetLocation().GetUserScriptPluginsDirectory()
@ -753,7 +823,7 @@ func (w *WPMPlugin) createScriptPluginWithTemplate(ctx context.Context, template
// 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())
w.api.Notify(ctx, fmt.Sprintf("i18n:plugin_wpm_create_script_dir_failed: %s", err.Error()))
return
}
@ -772,7 +842,7 @@ func (w *WPMPlugin) createScriptPluginWithTemplate(ctx context.Context, template
fileExtension = ".js" // Default fallback
}
w.creatingProcess = "Copying template..."
w.api.Notify(ctx, "i18n:plugin_wpm_copying_template")
// Read template from embedded resources
scriptTemplateDirectory := util.GetLocation().GetScriptPluginTemplatesDirectory()
@ -781,24 +851,25 @@ func (w *WPMPlugin) createScriptPluginWithTemplate(ctx context.Context, template
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())
w.api.Notify(ctx, fmt.Sprintf("i18n:plugin_wpm_read_template_failed: %s", err.Error()))
return
}
// Generate script file name
cleanPluginName := strings.TrimSpace(pluginName)
// Plugin name should not be empty at this point due to validation in createCommand
if cleanPluginName == "" {
cleanPluginName = "my-plugin"
w.api.Log(ctx, plugin.LogLevelError, "Plugin name is empty")
w.api.Notify(ctx, "i18n:plugin_wpm_plugin_name_empty")
return
}
scriptFileName := strings.ReplaceAll(strings.ToLower(cleanPluginName), " ", "-") + fileExtension
scriptFilePath := path.Join(userScriptPluginDirectory, scriptFileName)
// Check if file already exists
// Check if file already exists and notify user
if _, err := os.Stat(scriptFilePath); err == nil {
w.api.Notify(ctx, fmt.Sprintf("Script plugin file already exists: %s", scriptFileName))
w.creatingProcess = ""
return
w.api.Notify(ctx, fmt.Sprintf("i18n:plugin_wpm_overwriting_script_plugin: %s", scriptFileName))
}
// Replace template variables
@ -827,17 +898,48 @@ func (w *WPMPlugin) createScriptPluginWithTemplate(ctx context.Context, template
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())
w.api.Notify(ctx, fmt.Sprintf("i18n:plugin_wpm_create_script_file_failed: %s", err.Error()))
return
}
w.creatingProcess = ""
w.api.Notify(ctx, fmt.Sprintf("Script plugin created successfully: %s", scriptFileName))
// Show success notification
w.api.Notify(ctx, fmt.Sprintf("i18n:plugin_wpm_script_plugin_created_success: %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),
// Actively trigger script plugin loading instead of waiting
util.Go(ctx, "load script plugin immediately", func() {
// Trigger immediate reload of script plugins
pluginManager := plugin.GetPluginManager()
// Parse the script metadata to get plugin ID
metadata, parseErr := pluginManager.ParseScriptMetadata(ctx, scriptFilePath)
if parseErr != nil {
w.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("Failed to parse script metadata: %s", parseErr.Error()))
w.api.Notify(ctx, fmt.Sprintf("i18n:plugin_wpm_script_plugin_manual_try: %s", triggerKeyword))
return
}
// Create metadata with directory for loading
virtualDirectory := path.Join(userScriptPluginDirectory, metadata.Id)
metadataWithDirectory := plugin.MetadataWithDirectory{
Metadata: metadata,
Directory: virtualDirectory,
}
// Use ReloadPlugin to load the plugin immediately
loadErr := pluginManager.ReloadPlugin(ctx, metadataWithDirectory)
if loadErr != nil {
w.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("Failed to load script plugin: %s", loadErr.Error()))
w.api.Notify(ctx, fmt.Sprintf("i18n:plugin_wpm_script_plugin_manual_try: %s", triggerKeyword))
return
}
w.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("Successfully loaded script plugin: %s", metadata.Name))
// Change query to the new plugin
w.api.ChangeQuery(ctx, common.PlainQuery{
QueryType: plugin.QueryTypeInput,
QueryText: fmt.Sprintf("%s ", triggerKeyword),
})
})
}

View File

@ -267,6 +267,8 @@
"plugin_wpm_command_install": "Install Wox plugins",
"plugin_wpm_command_uninstall": "Uninstall Wox plugins",
"plugin_wpm_command_create": "Create Wox plugin",
"plugin_wpm_enter_plugin_name": "Please enter a plugin name",
"plugin_wpm_enter_plugin_name_subtitle": "Type the name of the plugin you want to create",
"plugin_wpm_command_dev_list": "List local Wox plugins",
"plugin_wpm_command_dev_add": "Add existing Wox plugin directory",
"plugin_wpm_command_dev_remove": "Remove local Wox plugin, followed by a directory",

View File

@ -265,6 +265,8 @@
"plugin_wpm_command_install": "Instalar plugins do Wox",
"plugin_wpm_command_uninstall": "Desinstalar plugins do Wox",
"plugin_wpm_command_create": "Criar plugin do Wox",
"plugin_wpm_enter_plugin_name": "Por favor, digite o nome do plugin",
"plugin_wpm_enter_plugin_name_subtitle": "Digite o nome do plugin que você deseja criar",
"plugin_wpm_command_dev_list": "Listar plugins locais do Wox",
"plugin_wpm_command_dev_add": "Adicionar diretório de plugin existente do Wox",
"plugin_wpm_command_dev_remove": "Remover plugin local do Wox, seguido de um diretório",

View File

@ -265,6 +265,8 @@
"plugin_wpm_command_install": "Установить плагины Wox",
"plugin_wpm_command_uninstall": "Удалить плагины Wox",
"plugin_wpm_command_create": "Создать плагин Wox",
"plugin_wpm_enter_plugin_name": "Пожалуйста, введите имя плагина",
"plugin_wpm_enter_plugin_name_subtitle": "Введите имя плагина, который вы хотите создать",
"plugin_wpm_command_dev_list": "Список локальных плагинов Wox",
"plugin_wpm_command_dev_add": "Добавить существующий каталог плагинов Wox",
"plugin_wpm_command_dev_remove": "Удалить локальный плагин Wox, указав каталог",

View File

@ -267,6 +267,8 @@
"plugin_wpm_command_install": "安装 Wox 插件",
"plugin_wpm_command_uninstall": "卸载 Wox 插件",
"plugin_wpm_command_create": "创建 Wox 插件",
"plugin_wpm_enter_plugin_name": "请输入插件名称",
"plugin_wpm_enter_plugin_name_subtitle": "输入您要创建的插件名称",
"plugin_wpm_command_dev_list": "列出本地 Wox 插件",
"plugin_wpm_command_dev_add": "添加现有的 Wox 插件目录",
"plugin_wpm_command_dev_remove": "移除本地 Wox 插件,后跟目录",

View File

@ -13,15 +13,22 @@
/**
* 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
*
* Available environment variables:
* - WOX_DIRECTORY_USER_SCRIPT_PLUGINS: Directory where script plugins are stored
* - WOX_DIRECTORY_USER_DATA: User data directory
* - WOX_DIRECTORY_WOX_DATA: Wox application data directory
* - WOX_DIRECTORY_PLUGINS: Plugin directory
* - WOX_DIRECTORY_THEMES: Theme directory
*/
// Parse input from command line or stdin
@ -89,30 +96,19 @@ switch (request.method) {
* @param {Object} request - The JSON-RPC request
*/
function handleQuery(request) {
const query = request.params.search || "";
// Generate results based on the query
// Generate results
const results = [
{
title: `You searched for: ${query}`,
subtitle: "This is a template result",
title: "Open Plugin Directory",
subtitle: "Open the script plugins directory in file manager",
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"
id: "open-plugin-directory",
data: ""
}
}
];
// Return results
console.log(JSON.stringify({
jsonrpc: "2.0",
@ -129,28 +125,16 @@ function handleQuery(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
case "open-plugin-directory":
// Open plugin directory action
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
action: "open-directory",
path: process.env.WOX_DIRECTORY_USER_SCRIPT_PLUGINS
},
id: request.id
}));

View File

@ -22,47 +22,44 @@ 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
Available environment variables:
- WOX_DIRECTORY_USER_SCRIPT_PLUGINS: Directory where script plugins are stored
- WOX_DIRECTORY_USER_DATA: User data directory
- WOX_DIRECTORY_WOX_DATA: Wox application data directory
- WOX_DIRECTORY_PLUGINS: Plugin directory
- WOX_DIRECTORY_THEMES: Theme directory
"""
import sys
import json
import os
def handle_query(params, request_id):
"""
Handle query requests
Args:
params: The parameters from the JSON-RPC request
params: The parameters from the JSON-RPC request (unused in this template)
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
# Generate results
results = [
{
"title": f"You searched for: {query}",
"subtitle": "This is a template result",
"title": "Open Plugin Directory",
"subtitle": "Open the script plugins directory in file manager",
"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"
"id": "open-plugin-directory",
"data": ""
}
}
]
# Return results
return {
"jsonrpc": "2.0",
@ -76,35 +73,24 @@ def handle_query(params, 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
if action_id == "open-plugin-directory":
# Open plugin directory action
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
"action": "open-directory",
"path": os.environ.get("WOX_DIRECTORY_USER_SCRIPT_PLUGINS", "")
},
"id": request_id
}
@ -143,7 +129,7 @@ def main():
"id": None
}))
return 1
# Validate JSON-RPC request
if request.get("jsonrpc") != "2.0":
print(json.dumps({
@ -156,12 +142,12 @@ def main():
"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":
@ -177,7 +163,7 @@ def main():
},
"id": request_id
}
# Output response
print(json.dumps(response))
return 0

View File

@ -21,6 +21,13 @@
# Available methods:
# - query: Process user queries and return results
# - action: Handle user selection of a result
#
# Available environment variables:
# - WOX_DIRECTORY_USER_SCRIPT_PLUGINS: Directory where script plugins are stored
# - WOX_DIRECTORY_USER_DATA: User data directory
# - WOX_DIRECTORY_WOX_DATA: Wox application data directory
# - WOX_DIRECTORY_PLUGINS: Plugin directory
# - WOX_DIRECTORY_THEMES: Theme directory
# Read input from command line or stdin
if [ $# -gt 0 ]; then
@ -56,27 +63,18 @@ fi
case "$METHOD" in
"query")
# Handle query request
# Generate results based on the query
# Generate results
cat << EOF
{
"jsonrpc": "2.0",
"result": {
"items": [
{
"title": "You searched for: $SEARCH",
"subtitle": "This is a template result",
"title": "Open Plugin Directory",
"subtitle": "Open the script plugins directory in file manager",
"score": 100,
"action": {
"id": "example-action",
"data": "$SEARCH"
}
},
{
"title": "System Information",
"subtitle": "Show system information",
"score": 90,
"action": {
"id": "system-info",
"id": "open-plugin-directory",
"data": ""
}
}
@ -89,31 +87,14 @@ EOF
"action")
# Handle action request
case "$ACTION_ID" in
"example-action")
# Example action that returns a message
"open-plugin-directory")
# Open plugin directory action
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"
"action": "open-directory",
"path": "$WOX_DIRECTORY_USER_SCRIPT_PLUGINS"
},
"id": $ID
}

View File

@ -150,6 +150,8 @@ func (u *uiImpl) isNotifyInToolbar(ctx context.Context, pluginId string) bool {
}
return false
return true
}
func (u *uiImpl) PickFiles(ctx context.Context, params common.PickFilesParams) []string {

View File

@ -48,57 +48,64 @@ class WoxListView<T> extends StatelessWidget {
}
},
child: Obx(
() => ListView.builder(
shrinkWrap: true,
controller: controller.scrollController,
physics: const ClampingScrollPhysics(),
itemCount: controller.items.length,
itemExtent: WoxThemeUtil.instance.getResultListViewHeightByCount(1),
itemBuilder: (context, index) {
var item = controller.items[index];
return MouseRegion(
onEnter: (_) {
if (controller.isMouseMoved && !item.value.isGroup) {
Logger.instance.info(const UuidV4().generate(), "MOUSE: onenter, is mouse moved: ${controller.isMouseMoved}, is group: ${item.value.isGroup}");
controller.updateHoveredIndex(index);
}
},
onHover: (_) {
if (!controller.isMouseMoved && !item.value.isGroup) {
Logger.instance.info(const UuidV4().generate(), "MOUSE: onHover, is mouse moved: ${controller.isMouseMoved}, is group: ${item.value.isGroup}");
controller.isMouseMoved = true;
controller.updateHoveredIndex(index);
}
},
onExit: (_) {
if (!item.value.isGroup && controller.hoveredIndex.value == index) {
controller.clearHoveredResult();
}
},
child: GestureDetector(
onTap: () {
if (!item.value.isGroup) {
controller.updateActiveIndex(const UuidV4().generate(), index);
controller.onItemActive?.call(const UuidV4().generate(), item.value);
() => AnimatedSwitcher(
duration: Duration.zero,
child: ListView.builder(
key: ValueKey(controller.items.length),
shrinkWrap: true,
controller: controller.scrollController,
physics: const NeverScrollableScrollPhysics(),
itemCount: controller.items.length,
itemExtent: WoxThemeUtil.instance.getResultListViewHeightByCount(1),
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
addSemanticIndexes: false,
itemBuilder: (context, index) {
var item = controller.items[index];
return MouseRegion(
onEnter: (_) {
if (controller.isMouseMoved && !item.value.isGroup) {
Logger.instance.info(const UuidV4().generate(), "MOUSE: onenter, is mouse moved: ${controller.isMouseMoved}, is group: ${item.value.isGroup}");
controller.updateHoveredIndex(index);
}
},
onDoubleTap: () {
if (!item.value.isGroup) {
controller.onItemExecuted?.call(const UuidV4().generate(), item.value);
onHover: (_) {
if (!controller.isMouseMoved && !item.value.isGroup) {
Logger.instance.info(const UuidV4().generate(), "MOUSE: onHover, is mouse moved: ${controller.isMouseMoved}, is group: ${item.value.isGroup}");
controller.isMouseMoved = true;
controller.updateHoveredIndex(index);
}
},
child: Obx(
() => WoxListItemView(
item: item.value,
woxTheme: WoxThemeUtil.instance.currentTheme.value,
isActive: controller.activeIndex.value == index,
isHovered: controller.hoveredIndex.value == index,
listViewType: listViewType,
onExit: (_) {
if (!item.value.isGroup && controller.hoveredIndex.value == index) {
controller.clearHoveredResult();
}
},
child: GestureDetector(
onTap: () {
if (!item.value.isGroup) {
controller.updateActiveIndex(const UuidV4().generate(), index);
controller.onItemActive?.call(const UuidV4().generate(), item.value);
}
},
onDoubleTap: () {
if (!item.value.isGroup) {
controller.onItemExecuted?.call(const UuidV4().generate(), item.value);
}
},
child: Obx(
() => WoxListItemView(
item: item.value,
woxTheme: WoxThemeUtil.instance.currentTheme.value,
isActive: controller.activeIndex.value == index,
isHovered: controller.hoveredIndex.value == index,
listViewType: listViewType,
),
),
),
),
);
},
);
},
),
),
),
),

View File

@ -119,6 +119,13 @@ class WoxListController<T> extends GetxController {
// Calculate how many items can be displayed in the current viewport
final viewportHeight = scrollController.position.viewportDimension;
// If viewport height is 0 or invalid, skip scrolling (this can happen during initialization)
if (viewportHeight <= 0) {
Logger.instance.debug(traceId, "Invalid viewport height: $viewportHeight, skipping scroll sync");
return;
}
final visibleItemCount = (viewportHeight / itemHeight).floor();
// If all items can be displayed in the viewport, no need to scroll