Wox/wox.core/plugin/system/wpm.go

946 lines
33 KiB
Go

package system
import (
"context"
"encoding/json"
"fmt"
"os"
"path"
"strings"
"time"
"wox/common"
"wox/i18n"
"wox/plugin"
"wox/setting/definition"
"wox/util"
"wox/util/shell"
"github.com/fsnotify/fsnotify"
"github.com/google/uuid"
cp "github.com/otiai10/copy"
"github.com/samber/lo"
)
var wpmIcon = plugin.PluginWPMIcon
var localPluginDirectoriesKey = "local_plugin_directories"
var pluginTemplates = []pluginTemplate{
{
Runtime: plugin.PLUGIN_RUNTIME_NODEJS,
Name: "Wox.Plugin.Template.Nodejs",
Url: "https://codeload.github.com/Wox-launcher/Wox.Plugin.Template.Nodejs/zip/refs/heads/main",
},
}
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
}
func init() {
plugin.AllSystemPlugin = append(plugin.AllSystemPlugin, &WPMPlugin{
reloadPluginTimers: util.NewHashMap[string, *time.Timer](),
})
}
type WPMPlugin struct {
api plugin.API
localPluginDirectories []string
localPlugins []localPlugin
reloadPluginTimers *util.HashMap[string, *time.Timer]
}
type pluginTemplate struct {
Runtime plugin.Runtime
Name string
Url string
}
type localPlugin struct {
metadata plugin.MetadataWithDirectory
watcher *fsnotify.Watcher
}
func (w *WPMPlugin) GetMetadata() plugin.Metadata {
return plugin.Metadata{
Id: "e2c5f005-6c73-43c8-bc53-ab04def265b2",
Name: "Wox Plugin Manager",
Author: "Wox Launcher",
Website: "https://github.com/Wox-launcher/Wox",
Version: "1.0.0",
MinWoxVersion: "2.0.0",
Runtime: "Go",
Description: "Plugin manager for Wox",
Icon: wpmIcon.String(),
Entry: "",
TriggerKeywords: []string{
"wpm",
},
Features: []plugin.MetadataFeature{
{
Name: plugin.MetadataFeatureIgnoreAutoScore,
},
},
Commands: []plugin.MetadataCommand{
{
Command: "install",
Description: "i18n:plugin_wpm_command_install",
},
{
Command: "uninstall",
Description: "i18n:plugin_wpm_command_uninstall",
},
{
Command: "create",
Description: "i18n:plugin_wpm_command_create",
},
{
Command: "dev.list",
Description: "i18n:plugin_wpm_command_dev_list",
},
{
Command: "dev.add",
Description: "i18n:plugin_wpm_command_dev_add",
},
{
Command: "dev.remove",
Description: "i18n:plugin_wpm_command_dev_remove",
},
{
Command: "dev.reload",
Description: "i18n:plugin_wpm_command_dev_reload",
},
},
SupportedOS: []string{
"Windows",
"Macos",
"Linux",
},
SettingDefinitions: definition.PluginSettingDefinitions{
{
Type: definition.PluginSettingDefinitionTypeTable,
Value: &definition.PluginSettingValueTable{
Key: localPluginDirectoriesKey,
Title: "i18n:plugin_wpm_local_plugin_directories",
Tooltip: "i18n:plugin_wpm_local_plugin_directories_tooltip",
Columns: []definition.PluginSettingValueTableColumn{
{
Key: "path",
Label: "i18n:plugin_wpm_path",
Type: definition.PluginSettingValueTableColumnTypeDirPath,
},
},
},
},
},
}
}
func (w *WPMPlugin) Init(ctx context.Context, initParams plugin.InitParams) {
w.api = initParams.API
w.reloadAllDevPlugins(ctx)
util.Go(ctx, "reload dev plugins in dist", func() {
// must delay reload, because host env is not ready when system plugin init
time.Sleep(time.Second * 5)
newCtx := util.NewTraceContext()
for _, lp := range w.localPlugins {
w.reloadLocalDistPlugin(newCtx, lp.metadata, "reload after startup")
}
})
}
func (w *WPMPlugin) reloadAllDevPlugins(ctx context.Context) {
var localPluginDirs []LocalPlugin
unmarshalErr := json.Unmarshal([]byte(w.api.GetSetting(ctx, localPluginDirectoriesKey)), &localPluginDirs)
if unmarshalErr != nil {
w.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("Failed to unmarshal local plugin directories: %s", unmarshalErr.Error()))
return
}
// remove invalid and duplicate directories
var pluginDirs []string
for _, pluginDir := range localPluginDirs {
if _, statErr := os.Stat(pluginDir.Path); statErr != nil {
w.api.Log(ctx, plugin.LogLevelWarning, fmt.Sprintf("Failed to stat local plugin directory, remove it: %s", statErr.Error()))
os.RemoveAll(pluginDir.Path)
continue
}
if !lo.Contains(pluginDirs, pluginDir.Path) {
pluginDirs = append(pluginDirs, pluginDir.Path)
}
}
w.localPluginDirectories = pluginDirs
for _, directory := range w.localPluginDirectories {
w.loadDevPlugin(ctx, directory)
}
}
func (w *WPMPlugin) loadDevPlugin(ctx context.Context, pluginDirectory string) {
w.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("start to load dev plugin: %s", pluginDirectory))
metadata, err := w.parseMetadata(ctx, pluginDirectory)
if err != nil {
w.api.Log(ctx, plugin.LogLevelError, err.Error())
return
}
lp := localPlugin{
metadata: metadata,
}
// check if plugin is already loaded
existingLocalPlugin, exist := lo.Find(w.localPlugins, func(lp localPlugin) bool {
return lp.metadata.Metadata.Id == metadata.Metadata.Id
})
if exist {
w.api.Log(ctx, plugin.LogLevelInfo, "plugin already loaded, unload first")
if existingLocalPlugin.watcher != nil {
closeWatcherErr := existingLocalPlugin.watcher.Close()
if closeWatcherErr != nil {
w.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("Failed to close watcher: %s", closeWatcherErr.Error()))
}
}
w.localPlugins = lo.Filter(w.localPlugins, func(lp localPlugin, _ int) bool {
return lp.metadata.Metadata.Id != metadata.Metadata.Id
})
}
// watch dist directory changes and auto reload plugin
distDirectory := path.Join(pluginDirectory, "dist")
if _, statErr := os.Stat(distDirectory); statErr == nil {
watcher, watchErr := util.WatchDirectoryChanges(ctx, distDirectory, func(e fsnotify.Event) {
if e.Op != fsnotify.Chmod {
// debounce reload plugin to avoid reload multiple times in a short time
if t, ok := w.reloadPluginTimers.Load(metadata.Metadata.Id); ok {
t.Stop()
}
w.reloadPluginTimers.Store(metadata.Metadata.Id, time.AfterFunc(time.Second*2, func() {
w.reloadLocalDistPlugin(ctx, metadata, "dist directory changed")
}))
}
})
if watchErr != nil {
w.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("Failed to watch dist directory: %s", watchErr.Error()))
} else {
w.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("Watching dist directory: %s", distDirectory))
lp.watcher = watcher
}
}
w.localPlugins = append(w.localPlugins, lp)
}
func (w *WPMPlugin) parseMetadata(ctx context.Context, directory string) (plugin.MetadataWithDirectory, error) {
// parse plugin.json in directory
metadata, metadataErr := plugin.GetPluginManager().ParseMetadata(ctx, directory)
if metadataErr != nil {
return plugin.MetadataWithDirectory{}, fmt.Errorf("failed to parse plugin.json in %s: %s", directory, metadataErr.Error())
}
return plugin.MetadataWithDirectory{
Metadata: metadata,
Directory: directory,
}, nil
}
func (w *WPMPlugin) Query(ctx context.Context, query plugin.Query) []plugin.QueryResult {
if query.Command == "create" {
return w.createCommand(ctx, query)
}
if query.Command == "install" {
return w.installCommand(ctx, query)
}
if query.Command == "uninstall" {
return w.uninstallCommand(ctx, query)
}
if query.Command == "dev.add" {
return w.addDevCommand(ctx, query)
}
if query.Command == "dev.remove" {
return w.removeDevCommand(ctx, query)
}
if query.Command == "dev.reload" {
return w.reloadDevCommand(ctx)
}
if query.Command == "dev.list" {
return w.listDevCommand(ctx)
}
return []plugin.QueryResult{}
}
func (w *WPMPlugin) createCommand(ctx context.Context, query plugin.Query) []plugin.QueryResult {
// Check if user has entered a plugin name
pluginName := strings.TrimSpace(query.Search)
if pluginName == "" {
return []plugin.QueryResult{
{
Id: uuid.NewString(),
Title: "i18n:plugin_wpm_enter_plugin_name",
SubTitle: "i18n:plugin_wpm_enter_plugin_name_subtitle",
Icon: wpmIcon,
},
}
}
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",
PreventHideAfterAction: true,
Action: func(ctx context.Context, actionContext plugin.ActionContext) {
pluginName := query.Search
util.Go(ctx, "create plugin", func() {
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
// 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,
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,
QueryText: fmt.Sprintf("%s create ", query.TriggerKeyword),
})
},
},
}
}
results = append(results, plugin.QueryResult{
Id: uuid.NewString(),
Title: title,
SubTitle: subtitle,
Icon: wpmIcon,
Group: "Script Plugins",
Actions: actions,
})
}
return results
}
func (w *WPMPlugin) uninstallCommand(ctx context.Context, query plugin.Query) []plugin.QueryResult {
var results []plugin.QueryResult
plugins := plugin.GetPluginManager().GetPluginInstances()
plugins = lo.Filter(plugins, func(pluginInstance *plugin.Instance, _ int) bool {
return pluginInstance.IsSystemPlugin == false
})
if query.Search != "" {
plugins = lo.Filter(plugins, func(pluginInstance *plugin.Instance, _ int) bool {
return IsStringMatchNoPinYin(ctx, pluginInstance.Metadata.Name, query.Search)
})
}
results = lo.Map(plugins, func(pluginInstanceShadow *plugin.Instance, _ int) plugin.QueryResult {
// action will be executed in another go routine, so we need to copy the variable
pluginInstance := pluginInstanceShadow
icon := common.ParseWoxImageOrDefault(pluginInstance.Metadata.Icon, wpmIcon)
icon = common.ConvertRelativePathToAbsolutePath(ctx, icon, pluginInstance.PluginDirectory)
return plugin.QueryResult{
Id: uuid.NewString(),
Title: pluginInstance.Metadata.Name,
SubTitle: pluginInstance.Metadata.Description,
Icon: icon,
Actions: []plugin.QueryResultAction{
{
Name: "i18n:plugin_wpm_uninstall",
Action: func(ctx context.Context, actionContext plugin.ActionContext) {
plugin.GetStoreManager().Uninstall(ctx, pluginInstance)
},
},
},
}
})
return results
}
func (w *WPMPlugin) installCommand(ctx context.Context, query plugin.Query) []plugin.QueryResult {
var results []plugin.QueryResult
pluginManifests := plugin.GetStoreManager().Search(ctx, query.Search)
for _, pluginManifest := range pluginManifests {
screenShotsMarkdown := lo.Map(pluginManifest.ScreenshotUrls, func(screenshot string, _ int) string {
return fmt.Sprintf("![screenshot](%s)", screenshot)
})
results = append(results, plugin.QueryResult{
Id: uuid.NewString(),
Title: pluginManifest.Name,
SubTitle: pluginManifest.Description,
Icon: common.NewWoxImageUrl(pluginManifest.IconUrl),
Preview: plugin.WoxPreview{
PreviewType: plugin.WoxPreviewTypeMarkdown,
PreviewData: fmt.Sprintf(`
### Description
%s
### Website
%s
### Screenshots
%s
`, pluginManifest.Description, pluginManifest.Website, strings.Join(screenShotsMarkdown, "\n")),
PreviewProperties: map[string]string{
"Author": pluginManifest.Author,
"Version": pluginManifest.Version,
"Website": pluginManifest.Website,
},
},
Actions: []plugin.QueryResultAction{
{
Name: "i18n:plugin_wpm_install",
Action: func(ctx context.Context, actionContext plugin.ActionContext) {
installErr := plugin.GetStoreManager().Install(ctx, pluginManifest)
if installErr != nil {
w.api.Notify(ctx, "i18n:plugin_wpm_install_failed")
}
},
},
}})
}
return results
}
func (w *WPMPlugin) listDevCommand(ctx context.Context) []plugin.QueryResult {
//list all local plugins
return lo.Map(w.localPlugins, func(lp localPlugin, _ int) plugin.QueryResult {
iconImage := common.ParseWoxImageOrDefault(lp.metadata.Metadata.Icon, wpmIcon)
iconImage = common.ConvertIcon(ctx, iconImage, lp.metadata.Directory)
return plugin.QueryResult{
Id: uuid.NewString(),
Title: lp.metadata.Metadata.Name,
SubTitle: lp.metadata.Metadata.Description,
Icon: iconImage,
Preview: plugin.WoxPreview{
PreviewType: plugin.WoxPreviewTypeMarkdown,
PreviewData: fmt.Sprintf(`
- **Directory**: %s
- **Name**: %s
- **Description**: %s
- **Author**: %s
- **Website**: %s
- **Version**: %s
- **MinWoxVersion**: %s
- **Runtime**: %s
- **Entry**: %s
- **TriggerKeywords**: %s
- **Commands**: %s
- **SupportedOS**: %s
- **Features**: %s
`, lp.metadata.Directory, lp.metadata.Metadata.Name, lp.metadata.Metadata.Description, lp.metadata.Metadata.Author,
lp.metadata.Metadata.Website, lp.metadata.Metadata.Version, lp.metadata.Metadata.MinWoxVersion,
lp.metadata.Metadata.Runtime, lp.metadata.Metadata.Entry, lp.metadata.Metadata.TriggerKeywords,
lp.metadata.Metadata.Commands, lp.metadata.Metadata.SupportedOS, lp.metadata.Metadata.Features),
},
Actions: []plugin.QueryResultAction{
{
Name: "i18n:plugin_wpm_reload",
IsDefault: true,
Action: func(ctx context.Context, actionContext plugin.ActionContext) {
w.reloadLocalDistPlugin(ctx, lp.metadata, "reload by user")
},
},
{
Name: "i18n:plugin_wpm_open_directory",
Action: func(ctx context.Context, actionContext plugin.ActionContext) {
openErr := shell.Open(lp.metadata.Directory)
if openErr != nil {
w.api.Notify(ctx, fmt.Sprintf(i18n.GetI18nManager().TranslateWox(ctx, "plugin_wpm_open_directory_failed"), openErr.Error()))
}
},
},
{
Name: "i18n:plugin_wpm_remove",
Action: func(ctx context.Context, actionContext plugin.ActionContext) {
w.localPluginDirectories = lo.Filter(w.localPluginDirectories, func(directory string, _ int) bool {
return directory != lp.metadata.Directory
})
w.saveLocalPluginDirectories(ctx)
},
},
{
Name: "i18n:plugin_wpm_remove_and_delete",
Action: func(ctx context.Context, actionContext plugin.ActionContext) {
deleteErr := os.RemoveAll(lp.metadata.Directory)
if deleteErr != nil {
w.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("Failed to delete plugin directory: %s", deleteErr.Error()))
return
}
w.localPluginDirectories = lo.Filter(w.localPluginDirectories, func(directory string, _ int) bool {
return directory != lp.metadata.Directory
})
w.saveLocalPluginDirectories(ctx)
},
},
},
}
})
}
func (w *WPMPlugin) reloadDevCommand(ctx context.Context) []plugin.QueryResult {
return []plugin.QueryResult{
{
Title: "i18n:plugin_wpm_reload_all_plugins",
Icon: wpmIcon,
Actions: []plugin.QueryResultAction{
{
Name: "i18n:plugin_wpm_reload",
Action: func(ctx context.Context, actionContext plugin.ActionContext) {
w.reloadAllDevPlugins(ctx)
util.Go(ctx, "reload dev plugins in dist", func() {
newCtx := util.NewTraceContext()
for _, lp := range w.localPlugins {
w.reloadLocalDistPlugin(newCtx, lp.metadata, "reload after user action")
}
})
},
},
},
},
}
}
func (w *WPMPlugin) addDevCommand(ctx context.Context, query plugin.Query) []plugin.QueryResult {
w.api.Log(ctx, plugin.LogLevelInfo, "Please choose a directory to add local plugin")
pluginDirectories := plugin.GetPluginManager().GetUI().PickFiles(ctx, common.PickFilesParams{IsDirectory: true})
if len(pluginDirectories) == 0 {
w.api.Notify(ctx, "i18n:plugin_wpm_choose_directory")
return []plugin.QueryResult{}
}
pluginDirectory := pluginDirectories[0]
if lo.Contains(w.localPluginDirectories, pluginDirectory) {
w.api.Notify(ctx, "i18n:plugin_wpm_directory_already_added")
return []plugin.QueryResult{}
}
w.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("Add local plugin: %s", pluginDirectory))
w.localPluginDirectories = append(w.localPluginDirectories, pluginDirectory)
w.saveLocalPluginDirectories(ctx)
w.loadDevPlugin(ctx, pluginDirectory)
return []plugin.QueryResult{}
}
func (w *WPMPlugin) removeDevCommand(ctx context.Context, query plugin.Query) []plugin.QueryResult {
if len(query.Search) == 0 {
w.api.Notify(ctx, "i18n:plugin_wpm_input_directory")
return []plugin.QueryResult{}
}
pluginDirectory := query.Search
if !lo.Contains(w.localPluginDirectories, pluginDirectory) {
w.api.Notify(ctx, "i18n:plugin_wpm_directory_not_found")
return []plugin.QueryResult{}
}
w.localPluginDirectories = lo.Filter(w.localPluginDirectories, func(directory string, _ int) bool {
return directory != pluginDirectory
})
w.saveLocalPluginDirectories(ctx)
return []plugin.QueryResult{}
}
func (w *WPMPlugin) createPlugin(ctx context.Context, template pluginTemplate, pluginName string, query plugin.Query) {
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.api.Notify(ctx, fmt.Sprintf(i18n.GetI18nManager().TranslateWox(ctx, "plugin_wpm_create_temp_dir_failed"), err.Error()))
return
}
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.api.Notify(ctx, fmt.Sprintf(i18n.GetI18nManager().TranslateWox(ctx, "plugin_wpm_download_template_failed"), err.Error()))
return
}
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.api.Notify(ctx, fmt.Sprintf(i18n.GetI18nManager().TranslateWox(ctx, "plugin_wpm_extract_template_failed"), err.Error()))
return
}
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")
return
}
pluginDirectory := path.Join(pluginDirectories[0], pluginName)
w.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("Creating plugin in directory: %s", pluginDirectory))
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.api.Notify(ctx, fmt.Sprintf("Failed to copy template: %s", cpErr.Error()))
return
}
// replace variables in plugin.json
pluginJsonPath := path.Join(pluginDirectory, "plugin.json")
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.api.Notify(ctx, fmt.Sprintf("Failed to read plugin.json: %s", readErr.Error()))
return
}
pluginJsonString := string(pluginJson)
pluginJsonString = strings.ReplaceAll(pluginJsonString, "[Id]", uuid.NewString())
pluginJsonString = strings.ReplaceAll(pluginJsonString, "[Name]", pluginName)
pluginJsonString = strings.ReplaceAll(pluginJsonString, "[Runtime]", strings.ToLower(string(template.Runtime)))
pluginJsonString = strings.ReplaceAll(pluginJsonString, "[Trigger Keyword]", "np")
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.api.Notify(ctx, fmt.Sprintf("Failed to write plugin.json: %s", writeErr.Error()))
return
}
// replace variables in package.json
if template.Runtime == plugin.PLUGIN_RUNTIME_NODEJS {
packageJsonPath := path.Join(pluginDirectory, "package.json")
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.api.Notify(ctx, fmt.Sprintf("Failed to read package.json: %s", readPackageErr.Error()))
return
}
packageJsonString := string(packageJson)
packageName := strings.ReplaceAll(strings.ToLower(pluginName), ".", "_")
packageJsonString = strings.ReplaceAll(packageJsonString, "replace_me_with_name", packageName)
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.api.Notify(ctx, fmt.Sprintf("Failed to write package.json: %s", writePackageErr.Error()))
return
}
}
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),
})
}
func (w *WPMPlugin) saveLocalPluginDirectories(ctx context.Context) {
var localPluginDirs []LocalPlugin
for _, directory := range w.localPluginDirectories {
localPluginDirs = append(localPluginDirs, LocalPlugin{Path: directory})
}
data, marshalErr := json.Marshal(localPluginDirs)
if marshalErr != nil {
w.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("Failed to marshal local plugin directories: %s", marshalErr.Error()))
return
}
w.api.SaveSetting(ctx, localPluginDirectoriesKey, string(data), false)
}
func (w *WPMPlugin) reloadLocalDistPlugin(ctx context.Context, localPlugin plugin.MetadataWithDirectory, reason string) error {
w.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("Reloading plugin: %s, reason: %s", localPlugin.Metadata.Name, reason))
// find dist directory, if not exist, prompt user to build it
distDirectory := path.Join(localPlugin.Directory, "dist")
_, statErr := os.Stat(distDirectory)
if statErr != nil {
w.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("Failed to stat dist directory: %s", statErr.Error()))
return statErr
}
distPluginMetadata, err := w.parseMetadata(ctx, distDirectory)
if err != nil {
w.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("Failed to load local plugin: %s", err.Error()))
return err
}
distPluginMetadata.IsDev = true
distPluginMetadata.DevPluginDirectory = localPlugin.Directory
reloadErr := plugin.GetPluginManager().ReloadPlugin(ctx, distPluginMetadata)
if reloadErr != nil {
w.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("Failed to reload plugin: %s", reloadErr.Error()))
return reloadErr
} else {
w.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("Reloaded plugin: %s", localPlugin.Metadata.Name))
}
w.api.Notify(ctx, fmt.Sprintf(i18n.GetI18nManager().TranslateWox(ctx, "plugin_wpm_reload_success"), localPlugin.Metadata.Name, reason))
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.api.Notify(ctx, "i18n:plugin_wpm_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.api.Notify(ctx, fmt.Sprintf("i18n:plugin_wpm_create_script_dir_failed: %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.api.Notify(ctx, "i18n:plugin_wpm_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.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 == "" {
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 and notify user
if _, err := os.Stat(scriptFilePath); err == nil {
w.api.Notify(ctx, fmt.Sprintf("i18n:plugin_wpm_overwriting_script_plugin: %s", scriptFileName))
}
// 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.api.Notify(ctx, fmt.Sprintf("i18n:plugin_wpm_create_script_file_failed: %s", err.Error()))
return
}
// 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))
// 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),
})
})
}