Skip to content

Commit

Permalink
Add create and local command for wpm plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
qianlifeng committed Dec 26, 2023
1 parent 1a8fd5b commit 3f44565
Show file tree
Hide file tree
Showing 4 changed files with 246 additions and 12 deletions.
2 changes: 1 addition & 1 deletion Wox.Plugin.Nodejs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@wox-launcher/wox-plugin",
"version": "0.0.50",
"version": "0.0.52",
"description": "All nodejs plugin for Wox should use types in this package",
"repository": {
"type": "git",
Expand Down
14 changes: 7 additions & 7 deletions Wox.Plugin.Nodejs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,14 @@ export interface Result {
Icon: WoxImage
Preview?: WoxPreview
Score?: number
ContextData: string
Actions: ResultAction[]
ContextData?: string
Actions?: ResultAction[]
// refresh result after specified interval, in milliseconds. If this value is 0, Wox will not refresh this result
// interval can only divisible by 100, if not, Wox will use the nearest number which is divisible by 100
// E.g. if you set 123, Wox will use 200, if you set 1234, Wox will use 1300
RefreshInterval: number
RefreshInterval?: number
// refresh result by calling OnRefresh function
OnRefresh: (current: RefreshableResult) => Promise<RefreshableResult>
OnRefresh?: (current: RefreshableResult) => Promise<RefreshableResult>
}

export interface RefreshableResult {
Expand All @@ -83,7 +83,7 @@ export interface ResultAction {
*/
Id?: string
Name: string
Icon: WoxImage
Icon?: WoxImage
/**
* If true, Wox will use this action as default action. There can be only one default action in results
* This can be omitted, if you don't set it, Wox will use the first action as default action
Expand All @@ -107,8 +107,8 @@ export interface PluginInitContext {

export interface ChangeQueryParam {
QueryType: "input" | "selection"
QueryText: string
QuerySelection: Selection
QueryText?: string
QuerySelection?: Selection
}

export interface PublicAPI {
Expand Down
6 changes: 3 additions & 3 deletions Wox/plugin/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func (m *Manager) loadPlugins(ctx context.Context) error {
var metaDataList []MetadataWithDirectory
for _, entry := range pluginDirectories {
pluginDirectory := path.Join(basePluginDirectory, entry.Name())
metadata, metadataErr := m.parseMetadata(ctx, pluginDirectory)
metadata, metadataErr := m.ParseMetadata(ctx, pluginDirectory)
if metadataErr != nil {
logger.Error(ctx, metadataErr.Error())
continue
Expand Down Expand Up @@ -189,7 +189,7 @@ func (m *Manager) loadHostPlugin(ctx context.Context, host Host, metadata Metada
}

func (m *Manager) LoadPlugin(ctx context.Context, pluginDirectory string) error {
metadata, parseErr := m.parseMetadata(ctx, pluginDirectory)
metadata, parseErr := m.ParseMetadata(ctx, pluginDirectory)
if parseErr != nil {
return parseErr
}
Expand Down Expand Up @@ -268,7 +268,7 @@ func (m *Manager) initPlugin(ctx context.Context, instance *Instance) {
instance.API.Log(ctx, fmt.Sprintf("[SYS] init plugin finished, cost %d ms", instance.InitFinishedTimestamp-instance.InitStartTimestamp))
}

func (m *Manager) parseMetadata(ctx context.Context, pluginDirectory string) (Metadata, error) {
func (m *Manager) ParseMetadata(ctx context.Context, pluginDirectory string) (Metadata, error) {
configPath := path.Join(pluginDirectory, "plugin.json")
if _, statErr := os.Stat(configPath); statErr != nil {
return Metadata{}, fmt.Errorf("missing plugin.json file in %s", configPath)
Expand Down
236 changes: 235 additions & 1 deletion Wox/plugin/system/wpm.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,42 @@ package system

import (
"context"
"encoding/json"
"fmt"
"github.com/google/uuid"
cp "github.com/otiai10/copy"
"github.com/samber/lo"
"os"
"path"
"wox/plugin"
"wox/share"
"wox/util"
)

var wpmIcon = plugin.NewWoxImageSvg(`<svg t="1697178225584" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="16738" width="200" height="200"><path d="M842.99 884.364H181.01c-22.85 0-41.374-18.756-41.374-41.892V181.528c0-23.136 18.524-41.892 41.374-41.892h661.98c22.85 0 41.374 18.756 41.374 41.892v660.944c0 23.136-18.524 41.892-41.374 41.892z" fill="#9C34FE" p-id="16739" data-spm-anchor-id="a313x.search_index.0.i6.1f873a81xqBP8f"></path><path d="M387.88 307.2h-82.748v83.78c0 115.68 92.618 209.456 206.868 209.456s206.868-93.776 206.868-209.454V307.2h-82.746v83.78c0 69.408-55.572 125.674-124.122 125.674s-124.12-56.266-124.12-125.672V307.2z" fill="#FFFFFF" p-id="16740"></path></svg>`)
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",
},
}

func init() {
plugin.AllSystemPlugin = append(plugin.AllSystemPlugin, &WPMPlugin{})
}

type WPMPlugin struct {
api plugin.API
api plugin.API
creatingProcess string
localPluginDirectories []string
localPlugins []plugin.MetadataWithDirectory
}

type pluginTemplate struct {
Runtime plugin.Runtime
Name string
Url string
}

func (i *WPMPlugin) GetMetadata() plugin.Metadata {
Expand Down Expand Up @@ -46,6 +69,14 @@ func (i *WPMPlugin) GetMetadata() plugin.Metadata {
Command: "uninstall",
Description: "Uninstall Wox plugins",
},
{
Command: "create",
Description: "Create Wox plugin",
},
{
Command: "local",
Description: "List local Wox plugins",
},
},
SupportedOS: []string{
"Windows",
Expand All @@ -57,11 +88,159 @@ func (i *WPMPlugin) GetMetadata() plugin.Metadata {

func (i *WPMPlugin) Init(ctx context.Context, initParams plugin.InitParams) {
i.api = initParams.API

localPluginDirs := i.api.GetSetting(ctx, "localPluginDirectories")
if localPluginDirs != "" {
unmarshalErr := json.Unmarshal([]byte(localPluginDirs), &i.localPluginDirectories)
if unmarshalErr != nil {
i.api.Log(ctx, fmt.Sprintf("Failed to unmarshal local plugin directories: %s", unmarshalErr.Error()))
}
}
i.loadLocalPlugins(ctx)
}

func (i *WPMPlugin) loadLocalPlugins(ctx context.Context) {
i.localPlugins = nil
for _, localPluginDirectory := range i.localPluginDirectories {
p, err := i.loadLocalPluginsFromDirectory(ctx, localPluginDirectory)
if err != nil {
i.api.Log(ctx, err.Error())
continue
}

i.api.Log(ctx, fmt.Sprintf("Loaded local plugin: %s", p.Metadata.Name))
i.localPlugins = append(i.localPlugins, p)
}
}

func (i *WPMPlugin) loadLocalPluginsFromDirectory(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 (i *WPMPlugin) Query(ctx context.Context, query plugin.Query) []plugin.QueryResult {
var results []plugin.QueryResult

if query.Command == "create" {
if i.creatingProcess != "" {
results = append(results, plugin.QueryResult{
Id: uuid.NewString(),
Title: i.creatingProcess,
SubTitle: "Please wait...",
Icon: wpmIcon,
RefreshInterval: 300,
OnRefresh: func(current plugin.RefreshableResult) plugin.RefreshableResult {
current.Title = i.creatingProcess
return current
},
})
return results
}

for _, template := range pluginTemplates {
// action will be executed in another go routine, so we need to copy the variable
pluginTemplateDummy := template
results = append(results, plugin.QueryResult{
Id: uuid.NewString(),
Title: "Create " + string(pluginTemplateDummy.Runtime) + " plugin",
SubTitle: fmt.Sprintf("Name: %s", query.Search),
Icon: wpmIcon,
Actions: []plugin.QueryResultAction{
{
Name: "create",
PreventHideAfterAction: true,
Action: func(actionContext plugin.ActionContext) {
pluginName := query.Search
util.Go(ctx, "create plugin", func() {
i.createPlugin(ctx, pluginTemplateDummy, pluginName, query)
})
i.api.ChangeQuery(ctx, share.ChangedQuery{
QueryType: plugin.QueryTypeInput,
QueryText: fmt.Sprintf("%s create ", query.TriggerKeyword),
})
},
},
}})
}
}

if query.Command == "local" {
//list all local plugins
return lo.Map(i.localPlugins, func(metadataWithDirectory plugin.MetadataWithDirectory, _ int) plugin.QueryResult {
iconImage := plugin.ParseWoxImageOrDefault(metadataWithDirectory.Metadata.Icon, wpmIcon)
iconImage = plugin.ConvertIcon(ctx, iconImage, metadataWithDirectory.Directory)

return plugin.QueryResult{
Id: uuid.NewString(),
Title: metadataWithDirectory.Metadata.Name,
SubTitle: metadataWithDirectory.Metadata.Description,
Icon: iconImage,
Preview: plugin.WoxPreview{
PreviewType: plugin.WoxPreviewTypeMarkdown,
PreviewData: fmt.Sprintf(`
- **Name**: %s
- **Description**: %s
- **Author**: %s
- **Website**: %s
- **Version**: %s
- **MinWoxVersion**: %s
- **Runtime**: %s
- **Entry**: %s
- **TriggerKeywords**: %s
- **Commands**: %s
- **SupportedOS**: %s
- **Features**: %s
`, metadataWithDirectory.Metadata.Name, metadataWithDirectory.Metadata.Description, metadataWithDirectory.Metadata.Author,
metadataWithDirectory.Metadata.Website, metadataWithDirectory.Metadata.Version, metadataWithDirectory.Metadata.MinWoxVersion,
metadataWithDirectory.Metadata.Runtime, metadataWithDirectory.Metadata.Entry, metadataWithDirectory.Metadata.TriggerKeywords,
metadataWithDirectory.Metadata.Commands, metadataWithDirectory.Metadata.SupportedOS, metadataWithDirectory.Metadata.Features),
},
Actions: []plugin.QueryResultAction{
{
Name: "open",
Action: func(actionContext plugin.ActionContext) {
openErr := util.ShellOpen(metadataWithDirectory.Directory)
if openErr != nil {
i.api.ShowMsg(ctx, "Failed to open plugin directory", openErr.Error(), wpmIcon.String())
}
},
},
{
Name: "Remove",
Action: func(actionContext plugin.ActionContext) {
i.localPluginDirectories = lo.Filter(i.localPluginDirectories, func(directory string, _ int) bool {
return directory != metadataWithDirectory.Directory
})
i.saveLocalPluginDirectories(ctx)
},
},
{
Name: "Remove and delete plugin directory",
Action: func(actionContext plugin.ActionContext) {
deleteErr := os.RemoveAll(metadataWithDirectory.Directory)
if deleteErr != nil {
i.api.Log(ctx, fmt.Sprintf("Failed to delete plugin directory: %s", deleteErr.Error()))
return
}

i.localPluginDirectories = lo.Filter(i.localPluginDirectories, func(directory string, _ int) bool {
return directory != metadataWithDirectory.Directory
})
i.saveLocalPluginDirectories(ctx)
},
},
},
}
})
}

if query.Command == "install" {
if query.Search == "" {
//TODO: return featured plugins
Expand Down Expand Up @@ -118,3 +297,58 @@ func (i *WPMPlugin) Query(ctx context.Context, query plugin.Query) []plugin.Quer

return results
}

func (i *WPMPlugin) createPlugin(ctx context.Context, template pluginTemplate, pluginName string, query plugin.Query) {
i.creatingProcess = "Downloading template..."

tempPluginDirectory := path.Join(os.TempDir(), uuid.NewString())
if err := util.GetLocation().EnsureDirectoryExist(tempPluginDirectory); err != nil {
i.api.Log(ctx, fmt.Sprintf("Failed to create temp plugin directory: %s", err.Error()))
i.creatingProcess = fmt.Sprintf("Failed to create temp plugin directory: %s", err.Error())
return
}

i.creatingProcess = fmt.Sprintf("Downloading %s template to %s", template.Runtime, tempPluginDirectory)
tempZipPath := path.Join(tempPluginDirectory, "template.zip")
err := util.HttpDownload(ctx, template.Url, tempZipPath)
if err != nil {
i.api.Log(ctx, fmt.Sprintf("Failed to download template: %s", err.Error()))
i.creatingProcess = fmt.Sprintf("Failed to download template: %s", err.Error())
return
}

i.creatingProcess = "Extracting template..."
err = util.Unzip(tempZipPath, tempPluginDirectory)
if err != nil {
i.api.Log(ctx, fmt.Sprintf("Failed to extract template: %s", err.Error()))
i.creatingProcess = fmt.Sprintf("Failed to extract template: %s", err.Error())
return
}

// TODO: let user choose the directory
pluginDirectory := path.Join(util.GetLocation().GetPluginDirectory(), pluginName)
cpErr := cp.Copy(path.Join(tempPluginDirectory, template.Name+"-main"), pluginDirectory)
if cpErr != nil {
i.api.Log(ctx, fmt.Sprintf("Failed to copy template: %s", cpErr.Error()))
i.creatingProcess = fmt.Sprintf("Failed to copy template: %s", cpErr.Error())
return
}

i.creatingProcess = ""
i.localPluginDirectories = append(i.localPluginDirectories, pluginDirectory)
i.saveLocalPluginDirectories(ctx)
i.api.ChangeQuery(ctx, share.ChangedQuery{
QueryType: plugin.QueryTypeInput,
QueryText: fmt.Sprintf("%s local ", query.TriggerKeyword),
})
}

func (i *WPMPlugin) saveLocalPluginDirectories(ctx context.Context) {
data, marshalErr := json.Marshal(i.localPluginDirectories)
if marshalErr != nil {
i.api.Log(ctx, fmt.Sprintf("Failed to marshal local plugin directories: %s", marshalErr.Error()))
return
}
i.api.SaveSetting(ctx, "localPluginDirectories", string(data), false)
i.loadLocalPlugins(ctx)
}

0 comments on commit 3f44565

Please sign in to comment.