diff --git a/wox.core/plugin/api.go b/wox.core/plugin/api.go index dad8858d..702984c7 100644 --- a/wox.core/plugin/api.go +++ b/wox.core/plugin/api.go @@ -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, }) } diff --git a/wox.core/plugin/host/host_script.go b/wox.core/plugin/host/host_script.go index cd45403d..9821b956 100644 --- a/wox.core/plugin/host/host_script.go +++ b/wox.core/plugin/host/host_script.go @@ -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 != "" { diff --git a/wox.core/plugin/system/wpm.go b/wox.core/plugin/system/wpm.go index a761868f..c1f0e5d0 100644 --- a/wox.core/plugin/system/wpm.go +++ b/wox.core/plugin/system/wpm.go @@ -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), + }) }) } diff --git a/wox.core/resource/lang/en_US.json b/wox.core/resource/lang/en_US.json index 8dd85e56..beba9b9d 100644 --- a/wox.core/resource/lang/en_US.json +++ b/wox.core/resource/lang/en_US.json @@ -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", diff --git a/wox.core/resource/lang/pt_BR.json b/wox.core/resource/lang/pt_BR.json index 3fe9e1ae..e94d5b50 100644 --- a/wox.core/resource/lang/pt_BR.json +++ b/wox.core/resource/lang/pt_BR.json @@ -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", diff --git a/wox.core/resource/lang/ru_RU.json b/wox.core/resource/lang/ru_RU.json index 068a3f0b..9ca5cdc8 100644 --- a/wox.core/resource/lang/ru_RU.json +++ b/wox.core/resource/lang/ru_RU.json @@ -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, указав каталог", diff --git a/wox.core/resource/lang/zh_CN.json b/wox.core/resource/lang/zh_CN.json index 257106ed..b7f250df 100644 --- a/wox.core/resource/lang/zh_CN.json +++ b/wox.core/resource/lang/zh_CN.json @@ -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 插件,后跟目录", diff --git a/wox.core/resource/script_plugin_templates/template.js b/wox.core/resource/script_plugin_templates/template.js index 8db36706..473c8aaa 100644 --- a/wox.core/resource/script_plugin_templates/template.js +++ b/wox.core/resource/script_plugin_templates/template.js @@ -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 })); diff --git a/wox.core/resource/script_plugin_templates/template.py b/wox.core/resource/script_plugin_templates/template.py index fd393ae1..078156a0 100644 --- a/wox.core/resource/script_plugin_templates/template.py +++ b/wox.core/resource/script_plugin_templates/template.py @@ -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 diff --git a/wox.core/resource/script_plugin_templates/template.sh b/wox.core/resource/script_plugin_templates/template.sh index e96a5909..f8780923 100644 --- a/wox.core/resource/script_plugin_templates/template.sh +++ b/wox.core/resource/script_plugin_templates/template.sh @@ -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 } diff --git a/wox.core/ui/ui_impl.go b/wox.core/ui/ui_impl.go index 3cfa51c7..3ca2b67a 100644 --- a/wox.core/ui/ui_impl.go +++ b/wox.core/ui/ui_impl.go @@ -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 { diff --git a/wox.ui.flutter/wox/lib/components/wox_list_view.dart b/wox.ui.flutter/wox/lib/components/wox_list_view.dart index 291d1172..91401c8a 100644 --- a/wox.ui.flutter/wox/lib/components/wox_list_view.dart +++ b/wox.ui.flutter/wox/lib/components/wox_list_view.dart @@ -48,57 +48,64 @@ class WoxListView 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, + ), ), ), - ), - ); - }, + ); + }, + ), ), ), ), diff --git a/wox.ui.flutter/wox/lib/controllers/wox_list_controller.dart b/wox.ui.flutter/wox/lib/controllers/wox_list_controller.dart index 3eec75dd..c4468ab1 100644 --- a/wox.ui.flutter/wox/lib/controllers/wox_list_controller.dart +++ b/wox.ui.flutter/wox/lib/controllers/wox_list_controller.dart @@ -119,6 +119,13 @@ class WoxListController 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