mirror of https://github.com/Wox-launcher/Wox
1217 lines
41 KiB
Go
1217 lines
41 KiB
Go
package plugin
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"path"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
"wox/ai"
|
|
"wox/i18n"
|
|
"wox/setting"
|
|
"wox/share"
|
|
"wox/util"
|
|
"wox/util/notifier"
|
|
|
|
"github.com/Masterminds/semver/v3"
|
|
"github.com/google/uuid"
|
|
"github.com/jinzhu/copier"
|
|
"github.com/samber/lo"
|
|
"github.com/wissance/stringFormatter"
|
|
)
|
|
|
|
var managerInstance *Manager
|
|
var managerOnce sync.Once
|
|
var logger *util.Log
|
|
|
|
type debounceTimer struct {
|
|
timer *time.Timer
|
|
onStop func()
|
|
}
|
|
|
|
type Manager struct {
|
|
instances []*Instance
|
|
ui share.UI
|
|
resultCache *util.HashMap[string, *QueryResultCache]
|
|
debounceQueryTimer *util.HashMap[string, *debounceTimer]
|
|
aiProviders *util.HashMap[ai.ProviderName, ai.Provider]
|
|
|
|
activeBrowserUrl string //active browser url before wox is activated
|
|
}
|
|
|
|
func GetPluginManager() *Manager {
|
|
managerOnce.Do(func() {
|
|
managerInstance = &Manager{
|
|
resultCache: util.NewHashMap[string, *QueryResultCache](),
|
|
debounceQueryTimer: util.NewHashMap[string, *debounceTimer](),
|
|
aiProviders: util.NewHashMap[ai.ProviderName, ai.Provider](),
|
|
}
|
|
logger = util.GetLogger()
|
|
})
|
|
return managerInstance
|
|
}
|
|
|
|
func (m *Manager) Start(ctx context.Context, ui share.UI) error {
|
|
m.ui = ui
|
|
|
|
loadErr := m.loadPlugins(ctx)
|
|
if loadErr != nil {
|
|
return fmt.Errorf("failed to load plugins: %w", loadErr)
|
|
}
|
|
|
|
util.Go(ctx, "start store manager", func() {
|
|
GetStoreManager().Start(util.NewTraceContext())
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) Stop(ctx context.Context) {
|
|
for _, host := range AllHosts {
|
|
host.Stop(ctx)
|
|
}
|
|
}
|
|
|
|
func (m *Manager) SetActiveBrowserUrl(url string) {
|
|
m.activeBrowserUrl = url
|
|
}
|
|
|
|
func (m *Manager) loadPlugins(ctx context.Context) error {
|
|
logger.Info(ctx, "start loading plugins")
|
|
|
|
// load system plugin first
|
|
m.loadSystemPlugins(ctx)
|
|
|
|
logger.Debug(ctx, "start loading user plugin metadata")
|
|
basePluginDirectory := util.GetLocation().GetPluginDirectory()
|
|
pluginDirectories, readErr := os.ReadDir(basePluginDirectory)
|
|
if readErr != nil {
|
|
return fmt.Errorf("failed to read plugin directory: %w", readErr)
|
|
}
|
|
|
|
var metaDataList []MetadataWithDirectory
|
|
for _, entry := range pluginDirectories {
|
|
if entry.Name() == ".DS_Store" {
|
|
continue
|
|
}
|
|
if !entry.IsDir() {
|
|
continue
|
|
}
|
|
|
|
pluginDirectory := path.Join(basePluginDirectory, entry.Name())
|
|
metadata, metadataErr := m.ParseMetadata(ctx, pluginDirectory)
|
|
if metadataErr != nil {
|
|
logger.Error(ctx, metadataErr.Error())
|
|
continue
|
|
}
|
|
|
|
//check if metadata already exist, only add newer version
|
|
existMetadata, exist := lo.Find(metaDataList, func(item MetadataWithDirectory) bool {
|
|
return item.Metadata.Id == metadata.Id
|
|
})
|
|
if exist {
|
|
existVersion, existVersionErr := semver.NewVersion(existMetadata.Metadata.Version)
|
|
currentVersion, currentVersionErr := semver.NewVersion(metadata.Version)
|
|
if existVersionErr == nil && currentVersionErr == nil {
|
|
if existVersion.GreaterThan(currentVersion) || existVersion.Equal(currentVersion) {
|
|
logger.Info(ctx, fmt.Sprintf("skip parse %s(%s) metadata, because it's already parsed(%s)", metadata.Name, metadata.Version, existMetadata.Metadata.Version))
|
|
continue
|
|
} else {
|
|
// remove older version
|
|
logger.Info(ctx, fmt.Sprintf("remove older metadata version %s(%s)", existMetadata.Metadata.Name, existMetadata.Metadata.Version))
|
|
var newMetaDataList []MetadataWithDirectory
|
|
for _, item := range metaDataList {
|
|
if item.Metadata.Id != existMetadata.Metadata.Id {
|
|
newMetaDataList = append(newMetaDataList, item)
|
|
}
|
|
}
|
|
metaDataList = newMetaDataList
|
|
}
|
|
}
|
|
}
|
|
metaDataList = append(metaDataList, MetadataWithDirectory{Metadata: metadata, Directory: pluginDirectory})
|
|
}
|
|
logger.Info(ctx, fmt.Sprintf("start loading user plugins, found %d user plugins", len(metaDataList)))
|
|
|
|
for _, host := range AllHosts {
|
|
util.Go(ctx, fmt.Sprintf("[%s] start host", host.GetRuntime(ctx)), func() {
|
|
newCtx := util.NewTraceContext()
|
|
hostErr := host.Start(newCtx)
|
|
if hostErr != nil {
|
|
logger.Error(newCtx, fmt.Errorf("[%s HOST] %w", host.GetRuntime(newCtx), hostErr).Error())
|
|
return
|
|
}
|
|
|
|
for _, metadata := range metaDataList {
|
|
if strings.ToUpper(metadata.Metadata.Runtime) != strings.ToUpper(string(host.GetRuntime(newCtx))) {
|
|
continue
|
|
}
|
|
|
|
loadErr := m.loadHostPlugin(newCtx, host, metadata)
|
|
if loadErr != nil {
|
|
logger.Error(newCtx, fmt.Errorf("[%s HOST] %w", host.GetRuntime(newCtx), loadErr).Error())
|
|
continue
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) ReloadPlugin(ctx context.Context, metadata MetadataWithDirectory) error {
|
|
logger.Info(ctx, fmt.Sprintf("start reloading dev plugin: %s", metadata.Metadata.Name))
|
|
|
|
pluginHost, exist := lo.Find(AllHosts, func(item Host) bool {
|
|
return strings.ToLower(string(item.GetRuntime(ctx))) == strings.ToLower(metadata.Metadata.Runtime)
|
|
})
|
|
if !exist {
|
|
return fmt.Errorf("unsupported runtime: %s", metadata.Metadata.Runtime)
|
|
}
|
|
|
|
pluginInstance, pluginInstanceExist := lo.Find(m.instances, func(item *Instance) bool {
|
|
return item.Metadata.Id == metadata.Metadata.Id
|
|
})
|
|
if pluginInstanceExist {
|
|
logger.Info(ctx, fmt.Sprintf("plugin(%s) is loaded, unload first", metadata.Metadata.Name))
|
|
m.UnloadPlugin(ctx, pluginInstance)
|
|
} else {
|
|
logger.Info(ctx, fmt.Sprintf("plugin(%s) is not loaded, skip unload", metadata.Metadata.Name))
|
|
}
|
|
|
|
loadErr := m.loadHostPlugin(ctx, pluginHost, metadata)
|
|
if loadErr != nil {
|
|
return loadErr
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) loadHostPlugin(ctx context.Context, host Host, metadata MetadataWithDirectory) error {
|
|
loadStartTimestamp := util.GetSystemTimestamp()
|
|
plugin, loadErr := host.LoadPlugin(ctx, metadata.Metadata, metadata.Directory)
|
|
if loadErr != nil {
|
|
logger.Error(ctx, fmt.Errorf("[%s HOST] failed to load plugin: %w", host.GetRuntime(ctx), loadErr).Error())
|
|
return loadErr
|
|
}
|
|
loadFinishTimestamp := util.GetSystemTimestamp()
|
|
|
|
instance := &Instance{
|
|
Metadata: metadata.Metadata,
|
|
PluginDirectory: metadata.Directory,
|
|
Plugin: plugin,
|
|
Host: host,
|
|
LoadStartTimestamp: loadStartTimestamp,
|
|
LoadFinishedTimestamp: loadFinishTimestamp,
|
|
IsDevPlugin: metadata.IsDev,
|
|
DevPluginDirectory: metadata.DevPluginDirectory,
|
|
}
|
|
instance.API = NewAPI(instance)
|
|
pluginSetting, settingErr := setting.GetSettingManager().LoadPluginSetting(ctx, metadata.Metadata.Id, metadata.Metadata.Name, metadata.Metadata.SettingDefinitions)
|
|
if settingErr != nil {
|
|
instance.API.Log(ctx, LogLevelError, fmt.Errorf("[SYS] failed to load plugin[%s] setting: %w", metadata.Metadata.Name, settingErr).Error())
|
|
return settingErr
|
|
}
|
|
instance.Setting = pluginSetting
|
|
|
|
m.instances = append(m.instances, instance)
|
|
|
|
if pluginSetting.Disabled {
|
|
logger.Info(ctx, fmt.Errorf("[%s HOST] plugin is disabled by user, skip init: %s", host.GetRuntime(ctx), metadata.Metadata.Name).Error())
|
|
instance.API.Log(ctx, LogLevelWarning, fmt.Sprintf("[SYS] plugin is disabled by user, skip init: %s", metadata.Metadata.Name))
|
|
return nil
|
|
}
|
|
|
|
util.Go(ctx, fmt.Sprintf("[%s] init plugin", metadata.Metadata.Name), func() {
|
|
m.initPlugin(ctx, instance)
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) LoadPlugin(ctx context.Context, pluginDirectory string) error {
|
|
metadata, parseErr := m.ParseMetadata(ctx, pluginDirectory)
|
|
if parseErr != nil {
|
|
return parseErr
|
|
}
|
|
|
|
pluginHost, exist := lo.Find(AllHosts, func(item Host) bool {
|
|
return strings.ToLower(string(item.GetRuntime(ctx))) == strings.ToLower(metadata.Runtime)
|
|
})
|
|
if !exist {
|
|
return fmt.Errorf("unsupported runtime: %s", metadata.Runtime)
|
|
}
|
|
|
|
loadErr := m.loadHostPlugin(ctx, pluginHost, MetadataWithDirectory{Metadata: metadata, Directory: pluginDirectory})
|
|
if loadErr != nil {
|
|
return loadErr
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) UnloadPlugin(ctx context.Context, pluginInstance *Instance) {
|
|
for _, callback := range pluginInstance.UnloadCallbacks {
|
|
callback()
|
|
}
|
|
pluginInstance.Host.UnloadPlugin(ctx, pluginInstance.Metadata)
|
|
|
|
var newInstances []*Instance
|
|
for _, instance := range m.instances {
|
|
if instance.Metadata.Id != pluginInstance.Metadata.Id {
|
|
newInstances = append(newInstances, instance)
|
|
}
|
|
}
|
|
m.instances = newInstances
|
|
}
|
|
|
|
func (m *Manager) loadSystemPlugins(ctx context.Context) {
|
|
start := util.GetSystemTimestamp()
|
|
logger.Info(ctx, fmt.Sprintf("start loading system plugins, found %d system plugins", len(AllSystemPlugin)))
|
|
|
|
for _, plugin := range AllSystemPlugin {
|
|
util.Go(ctx, fmt.Sprintf("load system plugin <%s>", plugin.GetMetadata().Name), func() {
|
|
metadata := plugin.GetMetadata()
|
|
instance := &Instance{
|
|
Metadata: metadata,
|
|
Plugin: plugin,
|
|
Host: nil,
|
|
IsSystemPlugin: true,
|
|
LoadStartTimestamp: util.GetSystemTimestamp(),
|
|
LoadFinishedTimestamp: util.GetSystemTimestamp(),
|
|
}
|
|
instance.API = NewAPI(instance)
|
|
|
|
startTimestamp := util.GetSystemTimestamp()
|
|
pluginSetting, settingErr := setting.GetSettingManager().LoadPluginSetting(ctx, metadata.Id, metadata.Name, metadata.SettingDefinitions)
|
|
if settingErr != nil {
|
|
errMsg := fmt.Sprintf("failed to load system plugin[%s] setting, use default plugin setting. err: %s", metadata.Name, settingErr.Error())
|
|
logger.Error(ctx, errMsg)
|
|
instance.API.Log(ctx, LogLevelError, fmt.Sprintf("[SYS] %s", errMsg))
|
|
pluginSetting = &setting.PluginSetting{
|
|
Settings: util.NewHashMap[string, string](),
|
|
}
|
|
}
|
|
instance.Setting = pluginSetting
|
|
if util.GetSystemTimestamp()-startTimestamp > 100 {
|
|
logger.Warn(ctx, fmt.Sprintf("load system plugin[%s] setting too slow, cost %d ms", metadata.Name, util.GetSystemTimestamp()-startTimestamp))
|
|
}
|
|
|
|
m.instances = append(m.instances, instance)
|
|
|
|
m.initPlugin(util.NewTraceContext(), instance)
|
|
})
|
|
}
|
|
|
|
logger.Debug(ctx, fmt.Sprintf("finish loading system plugins, cost %d ms", util.GetSystemTimestamp()-start))
|
|
}
|
|
|
|
func (m *Manager) initPlugin(ctx context.Context, instance *Instance) {
|
|
logger.Info(ctx, fmt.Sprintf("start init plugin: %s", instance.Metadata.Name))
|
|
instance.InitStartTimestamp = util.GetSystemTimestamp()
|
|
instance.Plugin.Init(ctx, InitParams{
|
|
API: instance.API,
|
|
PluginDirectory: instance.PluginDirectory,
|
|
})
|
|
instance.InitFinishedTimestamp = util.GetSystemTimestamp()
|
|
logger.Info(ctx, fmt.Sprintf("init plugin %s finished, cost %d ms", instance.Metadata.Name, instance.InitFinishedTimestamp-instance.InitStartTimestamp))
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
metadataJson, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
return Metadata{}, fmt.Errorf("failed to read plugin.json file: %w", err)
|
|
}
|
|
|
|
var metadata Metadata
|
|
unmarshalErr := json.Unmarshal(metadataJson, &metadata)
|
|
if unmarshalErr != nil {
|
|
return Metadata{}, fmt.Errorf("failed to unmarshal plugin.json file (%s): %w", pluginDirectory, unmarshalErr)
|
|
}
|
|
|
|
if len(metadata.TriggerKeywords) == 0 {
|
|
return Metadata{}, fmt.Errorf("missing trigger keywords in plugin.json file (%s)", pluginDirectory)
|
|
}
|
|
if !IsSupportedRuntime(metadata.Runtime) {
|
|
return Metadata{}, fmt.Errorf("unsupported runtime in plugin.json file (%s), runtime=%s", pluginDirectory, metadata.Runtime)
|
|
}
|
|
if !IsSupportedOSAny(metadata.SupportedOS) {
|
|
return Metadata{}, fmt.Errorf("unsupported os in plugin.json file (%s), os=%s", pluginDirectory, metadata.SupportedOS)
|
|
}
|
|
|
|
return metadata, nil
|
|
}
|
|
|
|
func (m *Manager) GetPluginInstances() []*Instance {
|
|
return m.instances
|
|
}
|
|
|
|
func (m *Manager) canOperateQuery(ctx context.Context, pluginInstance *Instance, query Query) bool {
|
|
if pluginInstance.Setting.Disabled {
|
|
return false
|
|
}
|
|
|
|
if query.Type == QueryTypeSelection {
|
|
isPluginSupportSelection := pluginInstance.Metadata.IsSupportFeature(MetadataFeatureQuerySelection)
|
|
return isPluginSupportSelection
|
|
}
|
|
|
|
var validGlobalQuery = lo.Contains(pluginInstance.GetTriggerKeywords(), "*") && query.TriggerKeyword == ""
|
|
var validNonGlobalQuery = lo.Contains(pluginInstance.GetTriggerKeywords(), query.TriggerKeyword)
|
|
if !validGlobalQuery && !validNonGlobalQuery {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (m *Manager) queryForPlugin(ctx context.Context, pluginInstance *Instance, query Query) (results []QueryResult) {
|
|
defer util.GoRecover(ctx, fmt.Sprintf("<%s> query panic", pluginInstance.Metadata.Name), func(err error) {
|
|
// if plugin query panic, return error result
|
|
failedResult := m.GetResultForFailedQuery(ctx, pluginInstance.Metadata, query, err)
|
|
results = []QueryResult{
|
|
m.PolishResult(ctx, pluginInstance, query, failedResult),
|
|
}
|
|
})
|
|
|
|
logger.Info(ctx, fmt.Sprintf("<%s> start query: %s", pluginInstance.Metadata.Name, query.RawQuery))
|
|
start := util.GetSystemTimestamp()
|
|
|
|
// set query env base on plugin's feature
|
|
currentEnv := query.Env
|
|
newEnv := QueryEnv{}
|
|
if pluginInstance.Metadata.IsSupportFeature(MetadataFeatureQueryEnv) {
|
|
queryEnvParams, err := pluginInstance.Metadata.GetFeatureParamsForQueryEnv()
|
|
if err != nil {
|
|
logger.Error(ctx, fmt.Sprintf("<%s> invalid query env config: %s", pluginInstance.Metadata.Name, err))
|
|
} else {
|
|
if queryEnvParams.RequireActiveWindowName {
|
|
newEnv.ActiveWindowTitle = currentEnv.ActiveWindowTitle
|
|
}
|
|
if queryEnvParams.RequireActiveWindowPid {
|
|
newEnv.ActiveWindowPid = currentEnv.ActiveWindowPid
|
|
}
|
|
if queryEnvParams.RequireActiveBrowserUrl {
|
|
newEnv.ActiveBrowserUrl = currentEnv.ActiveBrowserUrl
|
|
}
|
|
}
|
|
}
|
|
query.Env = newEnv
|
|
|
|
results = pluginInstance.Plugin.Query(ctx, query)
|
|
logger.Debug(ctx, fmt.Sprintf("<%s> finish query, result count: %d, cost: %dms", pluginInstance.Metadata.Name, len(results), util.GetSystemTimestamp()-start))
|
|
|
|
for i := range results {
|
|
results[i] = m.addDefaultActions(ctx, pluginInstance, query, results[i])
|
|
results[i] = m.PolishResult(ctx, pluginInstance, query, results[i])
|
|
}
|
|
|
|
if query.Type == QueryTypeSelection && query.Search != "" {
|
|
woxSetting := setting.GetSettingManager().GetWoxSetting(ctx)
|
|
results = lo.Filter(results, func(item QueryResult, _ int) bool {
|
|
match, _ := util.IsStringMatchScore(item.Title, query.Search, woxSetting.UsePinYin)
|
|
return match
|
|
})
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
func (m *Manager) GetResultForFailedQuery(ctx context.Context, pluginMetadata Metadata, query Query, err error) QueryResult {
|
|
overlayIcon := NewWoxImageEmoji("🚫")
|
|
pluginIcon := ParseWoxImageOrDefault(pluginMetadata.Icon, overlayIcon)
|
|
icon := pluginIcon.OverlayFullPercentage(overlayIcon, 0.6)
|
|
|
|
return QueryResult{
|
|
Title: fmt.Sprintf(i18n.GetI18nManager().TranslateWox(ctx, "plugin_manager_query_failed"), pluginMetadata.Name),
|
|
SubTitle: util.EllipsisEnd(err.Error(), 20),
|
|
Icon: icon,
|
|
Preview: WoxPreview{
|
|
PreviewType: WoxPreviewTypeText,
|
|
PreviewData: err.Error(),
|
|
},
|
|
}
|
|
}
|
|
|
|
func (m *Manager) addDefaultActions(ctx context.Context, pluginInstance *Instance, query Query, result QueryResult) QueryResult {
|
|
if query.Type == QueryTypeInput {
|
|
if result.Group == "" {
|
|
if setting.GetSettingManager().IsFavoriteResult(ctx, pluginInstance.Metadata.Id, result.Title, result.SubTitle) {
|
|
result.Actions = append(result.Actions, QueryResultAction{
|
|
Name: "i18n:plugin_manager_remove_from_favorite",
|
|
Icon: RemoveFromFavIcon,
|
|
Action: func(ctx context.Context, actionContext ActionContext) {
|
|
setting.GetSettingManager().RemoveFavoriteResult(ctx, pluginInstance.Metadata.Id, result.Title, result.SubTitle)
|
|
},
|
|
})
|
|
} else {
|
|
result.Actions = append(result.Actions, QueryResultAction{
|
|
Name: "i18n:plugin_manager_add_to_favorite",
|
|
Icon: AddToFavIcon,
|
|
Action: func(ctx context.Context, actionContext ActionContext) {
|
|
setting.GetSettingManager().AddFavoriteResult(ctx, pluginInstance.Metadata.Id, result.Title, result.SubTitle)
|
|
},
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (m *Manager) PolishResult(ctx context.Context, pluginInstance *Instance, query Query, result QueryResult) QueryResult {
|
|
// set default id
|
|
if result.Id == "" {
|
|
result.Id = uuid.NewString()
|
|
}
|
|
for actionIndex := range result.Actions {
|
|
if result.Actions[actionIndex].Id == "" {
|
|
result.Actions[actionIndex].Id = uuid.NewString()
|
|
}
|
|
if result.Actions[actionIndex].Icon.ImageType == "" {
|
|
// set default action icon if not present
|
|
result.Actions[actionIndex].Icon = DefaultActionIcon
|
|
}
|
|
}
|
|
|
|
// convert icon
|
|
result.Icon = ConvertIcon(ctx, result.Icon, pluginInstance.PluginDirectory)
|
|
for i := range result.Tails {
|
|
if result.Tails[i].Type == QueryResultTailTypeImage {
|
|
result.Tails[i].Image = ConvertIcon(ctx, result.Tails[i].Image, pluginInstance.PluginDirectory)
|
|
}
|
|
}
|
|
|
|
// add default preview for selection query if no preview is set
|
|
if query.Type == QueryTypeSelection && result.Preview.PreviewType == "" {
|
|
if query.Selection.Type == util.SelectionTypeText {
|
|
result.Preview = WoxPreview{
|
|
PreviewType: WoxPreviewTypeText,
|
|
PreviewData: query.Selection.Text,
|
|
}
|
|
}
|
|
if query.Selection.Type == util.SelectionTypeFile {
|
|
result.Preview = WoxPreview{
|
|
PreviewType: WoxPreviewTypeMarkdown,
|
|
PreviewData: m.formatFileListPreview(ctx, query.Selection.FilePaths),
|
|
}
|
|
}
|
|
}
|
|
|
|
// translate title
|
|
result.Title = m.translatePlugin(ctx, pluginInstance, result.Title)
|
|
// translate subtitle
|
|
result.SubTitle = m.translatePlugin(ctx, pluginInstance, result.SubTitle)
|
|
// translate preview properties
|
|
var previewProperties = make(map[string]string)
|
|
for key, value := range result.Preview.PreviewProperties {
|
|
translatedKey := m.translatePlugin(ctx, pluginInstance, key)
|
|
previewProperties[translatedKey] = value
|
|
}
|
|
result.Preview.PreviewProperties = previewProperties
|
|
// translate action names
|
|
for actionIndex := range result.Actions {
|
|
result.Actions[actionIndex].Name = m.translatePlugin(ctx, pluginInstance, result.Actions[actionIndex].Name)
|
|
}
|
|
// translate preview data if preview type is text
|
|
if result.Preview.PreviewType == WoxPreviewTypeText {
|
|
result.Preview.PreviewData = m.translatePlugin(ctx, pluginInstance, result.Preview.PreviewData)
|
|
}
|
|
|
|
// set first action as default if no default action is set
|
|
defaultActionCount := lo.CountBy(result.Actions, func(item QueryResultAction) bool {
|
|
return item.IsDefault
|
|
})
|
|
if defaultActionCount == 0 && len(result.Actions) > 0 {
|
|
result.Actions[0].IsDefault = true
|
|
result.Actions[0].Hotkey = "Enter"
|
|
}
|
|
|
|
//move default action to first one of the actions
|
|
sort.Slice(result.Actions, func(i, j int) bool {
|
|
return result.Actions[i].IsDefault
|
|
})
|
|
|
|
var resultCache = &QueryResultCache{
|
|
ResultId: result.Id,
|
|
ResultTitle: result.Title,
|
|
ResultSubTitle: result.SubTitle,
|
|
ContextData: result.ContextData,
|
|
PluginInstance: pluginInstance,
|
|
Query: query,
|
|
Actions: util.NewHashMap[string, func(ctx context.Context, actionContext ActionContext)](),
|
|
}
|
|
|
|
// store actions for ui invoke later
|
|
for actionIndex := range result.Actions {
|
|
var action = result.Actions[actionIndex]
|
|
|
|
// if default action's hotkey is empty, set it as Enter
|
|
if action.IsDefault && action.Hotkey == "" {
|
|
result.Actions[actionIndex].Hotkey = "Enter"
|
|
}
|
|
|
|
// replace hotkey modifiers for platform specific, E.g. replace win to cmd on macos, replace cmd to win on windows
|
|
if util.IsMacOS() {
|
|
result.Actions[actionIndex].Hotkey = strings.ReplaceAll(result.Actions[actionIndex].Hotkey, "win", "cmd")
|
|
result.Actions[actionIndex].Hotkey = strings.ReplaceAll(result.Actions[actionIndex].Hotkey, "windows", "cmd")
|
|
result.Actions[actionIndex].Hotkey = strings.ReplaceAll(result.Actions[actionIndex].Hotkey, "alt", "option")
|
|
}
|
|
if util.IsWindows() || util.IsLinux() {
|
|
result.Actions[actionIndex].Hotkey = strings.ReplaceAll(result.Actions[actionIndex].Hotkey, "cmd", "win")
|
|
result.Actions[actionIndex].Hotkey = strings.ReplaceAll(result.Actions[actionIndex].Hotkey, "command", "win")
|
|
result.Actions[actionIndex].Hotkey = strings.ReplaceAll(result.Actions[actionIndex].Hotkey, "option", "alt")
|
|
}
|
|
|
|
if action.Action != nil {
|
|
resultCache.Actions.Store(action.Id, action.Action)
|
|
}
|
|
}
|
|
|
|
// if query is input and trigger keyword is global, disable preview and group
|
|
if query.IsGlobalQuery() {
|
|
result.Preview = WoxPreview{}
|
|
result.Group = ""
|
|
result.GroupScore = 0
|
|
}
|
|
|
|
// store preview for ui invoke later
|
|
// because preview may contain some heavy data (E.g. image or large text), we will store preview in cache and only send preview to ui when user select the result
|
|
if result.Preview.PreviewType != "" && result.Preview.PreviewData != "" && result.Preview.PreviewType != WoxPreviewTypeRemote {
|
|
resultCache.Preview = result.Preview
|
|
result.Preview = WoxPreview{
|
|
PreviewType: WoxPreviewTypeRemote,
|
|
PreviewData: fmt.Sprintf("/preview?id=%s", result.Id),
|
|
}
|
|
}
|
|
|
|
if result.RefreshInterval > 0 && result.OnRefresh != nil {
|
|
newInterval := int(math.Floor(float64(result.RefreshInterval)/100) * 100)
|
|
if result.RefreshInterval != newInterval {
|
|
logger.Info(ctx, fmt.Sprintf("[%s] result(%s) refresh interval %d is not divisible by 100, use %d instead", pluginInstance.Metadata.Name, result.Id, result.RefreshInterval, newInterval))
|
|
result.RefreshInterval = newInterval
|
|
}
|
|
resultCache.Refresh = result.OnRefresh
|
|
}
|
|
|
|
ignoreAutoScore := pluginInstance.Metadata.IsSupportFeature(MetadataFeatureIgnoreAutoScore)
|
|
if !ignoreAutoScore {
|
|
score := m.calculateResultScore(ctx, pluginInstance.Metadata.Id, result.Title, result.SubTitle)
|
|
if score > 0 {
|
|
logger.Debug(ctx, fmt.Sprintf("<%s> result(%s) add score: %d", pluginInstance.Metadata.Name, result.Title, score))
|
|
result.Score += score
|
|
}
|
|
}
|
|
|
|
m.resultCache.Store(result.Id, resultCache)
|
|
|
|
return result
|
|
}
|
|
|
|
func (m *Manager) formatFileListPreview(ctx context.Context, filePaths []string) string {
|
|
totalFiles := len(filePaths)
|
|
if totalFiles == 0 {
|
|
return i18n.GetI18nManager().TranslateWox(ctx, "selection_no_files_selected")
|
|
}
|
|
|
|
var sb strings.Builder
|
|
sb.WriteString(fmt.Sprintf(i18n.GetI18nManager().TranslateWox(ctx, "selection_selected_files_count"), totalFiles))
|
|
sb.WriteString("\n\n")
|
|
|
|
maxDisplayFiles := 10
|
|
for i, filePath := range filePaths {
|
|
if i < maxDisplayFiles {
|
|
sb.WriteString(fmt.Sprintf("- `%s`\n", filePath))
|
|
} else {
|
|
remainingFiles := totalFiles - maxDisplayFiles
|
|
sb.WriteString("\n")
|
|
sb.WriteString(fmt.Sprintf(i18n.GetI18nManager().TranslateWox(ctx, "selection_remaining_files_not_shown"), remainingFiles))
|
|
break
|
|
}
|
|
}
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
func (m *Manager) calculateResultScore(ctx context.Context, pluginId, title, subTitle string) int64 {
|
|
var score int64 = 0
|
|
|
|
// check if result is favorite result
|
|
if setting.GetSettingManager().IsFavoriteResult(ctx, pluginId, title, subTitle) {
|
|
score += 100000
|
|
}
|
|
|
|
resultHash := setting.NewResultHash(pluginId, title, subTitle)
|
|
woxAppData := setting.GetSettingManager().GetWoxAppData(ctx)
|
|
actionResults, ok := woxAppData.ActionedResults.Load(resultHash)
|
|
if !ok {
|
|
return score
|
|
}
|
|
|
|
// actioned score are based on actioned counts, the more actioned, the more score
|
|
// also, action timestamp will be considered, the more recent actioned, the more score weight. If action is in recent 7 days, it will be considered as recent actioned and add score weight
|
|
// we will use fibonacci sequence to calculate score, the more recent actioned, the more score: 5, 8, 13, 21, 34, 55, 89
|
|
// that means, actions in day one, we will add weight 89, day two, we will add weight 55, day three, we will add weight 34, and so on
|
|
// E.g. if actioned 3 times in day one, 2 times in day two, 1 time in day three, the score will be: 89*3 + 55*2 + 34*1 = 450
|
|
|
|
for _, actionResult := range actionResults {
|
|
var weight int64 = 2
|
|
|
|
actionedTime := util.ParseTimeStamp(actionResult.Timestamp)
|
|
hours := util.GetSystemTime().Sub(actionedTime).Hours()
|
|
if hours < 24*7 {
|
|
fibonacciIndex := int(math.Ceil(hours / 24))
|
|
if fibonacciIndex > 7 {
|
|
fibonacciIndex = 7
|
|
}
|
|
if fibonacciIndex < 1 {
|
|
fibonacciIndex = 1
|
|
}
|
|
fibonacci := []int64{5, 8, 13, 21, 34, 55, 89}
|
|
score += fibonacci[7-fibonacciIndex]
|
|
}
|
|
|
|
score += weight
|
|
}
|
|
|
|
return score
|
|
}
|
|
|
|
func (m *Manager) polishRefreshableResult(ctx context.Context, resultCache *QueryResultCache, result RefreshableResult) RefreshableResult {
|
|
pluginInstance := resultCache.PluginInstance
|
|
|
|
for actionIndex := range result.Actions {
|
|
if result.Actions[actionIndex].Id == "" {
|
|
result.Actions[actionIndex].Id = uuid.NewString()
|
|
}
|
|
if result.Actions[actionIndex].Icon.ImageType == "" {
|
|
// set default action icon if not present
|
|
result.Actions[actionIndex].Icon = DefaultActionIcon
|
|
}
|
|
}
|
|
|
|
// set first action as default if no default action is set
|
|
defaultActionCount := lo.CountBy(result.Actions, func(item QueryResultAction) bool {
|
|
return item.IsDefault
|
|
})
|
|
if defaultActionCount == 0 && len(result.Actions) > 0 {
|
|
result.Actions[0].IsDefault = true
|
|
result.Actions[0].Hotkey = "Enter"
|
|
}
|
|
|
|
//move default action to first one of the actions
|
|
sort.Slice(result.Actions, func(i, j int) bool {
|
|
return result.Actions[i].IsDefault
|
|
})
|
|
|
|
// convert icon
|
|
result.Icon = ConvertIcon(ctx, result.Icon, pluginInstance.PluginDirectory)
|
|
for i := range result.Tails {
|
|
if result.Tails[i].Type == QueryResultTailTypeImage {
|
|
result.Tails[i].Image = ConvertIcon(ctx, result.Tails[i].Image, pluginInstance.PluginDirectory)
|
|
}
|
|
}
|
|
|
|
// translate title
|
|
result.Title = m.translatePlugin(ctx, pluginInstance, result.Title)
|
|
// translate subtitle
|
|
result.SubTitle = m.translatePlugin(ctx, pluginInstance, result.SubTitle)
|
|
// translate tail text
|
|
for i := range result.Tails {
|
|
if result.Tails[i].Type == QueryResultTailTypeText {
|
|
result.Tails[i].Text = m.translatePlugin(ctx, pluginInstance, result.Tails[i].Text)
|
|
}
|
|
}
|
|
// translate preview properties
|
|
var previewProperties = make(map[string]string)
|
|
for key, value := range result.Preview.PreviewProperties {
|
|
translatedKey := m.translatePlugin(ctx, pluginInstance, key)
|
|
previewProperties[translatedKey] = value
|
|
}
|
|
result.Preview.PreviewProperties = previewProperties
|
|
// translate action names
|
|
for actionIndex := range result.Actions {
|
|
result.Actions[actionIndex].Name = m.translatePlugin(ctx, pluginInstance, result.Actions[actionIndex].Name)
|
|
}
|
|
|
|
// update result cache
|
|
resultCache.ResultTitle = result.Title
|
|
resultCache.ResultSubTitle = result.SubTitle
|
|
resultCache.ContextData = result.ContextData
|
|
resultCache.Actions = util.NewHashMap[string, func(ctx context.Context, actionContext ActionContext)]()
|
|
for _, newAction := range result.Actions {
|
|
if newAction.Action != nil {
|
|
resultCache.Actions.Store(newAction.Id, newAction.Action)
|
|
}
|
|
}
|
|
|
|
// convert non-remote preview to remote preview
|
|
// because preview may contain some heavy data (E.g. image or large text),
|
|
// we will store preview in cache and only send preview to ui when user select the result
|
|
if result.Preview.PreviewType != "" && result.Preview.PreviewType != WoxPreviewTypeRemote {
|
|
resultCache.Preview = result.Preview
|
|
result.Preview = WoxPreview{
|
|
PreviewType: WoxPreviewTypeRemote,
|
|
PreviewData: fmt.Sprintf("/preview?id=%s", resultCache.ResultId),
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (m *Manager) Query(ctx context.Context, query Query) (results chan []QueryResultUI, done chan bool) {
|
|
results = make(chan []QueryResultUI, 10)
|
|
done = make(chan bool)
|
|
|
|
// clear old result cache
|
|
m.resultCache.Clear()
|
|
|
|
counter := &atomic.Int32{}
|
|
counter.Store(int32(len(m.instances)))
|
|
|
|
for _, pluginInstance := range m.instances {
|
|
if !m.canOperateQuery(ctx, pluginInstance, query) {
|
|
counter.Add(-1)
|
|
if counter.Load() == 0 {
|
|
done <- true
|
|
}
|
|
continue
|
|
}
|
|
|
|
if pluginInstance.Metadata.IsSupportFeature(MetadataFeatureDebounce) {
|
|
debounceParams, err := pluginInstance.Metadata.GetFeatureParamsForDebounce()
|
|
if err == nil {
|
|
logger.Debug(ctx, fmt.Sprintf("[%s] debounce query, will execute in %d ms", pluginInstance.Metadata.Name, debounceParams.intervalMs))
|
|
if v, ok := m.debounceQueryTimer.Load(pluginInstance.Metadata.Id); ok {
|
|
if v.timer.Stop() {
|
|
v.onStop()
|
|
}
|
|
}
|
|
|
|
timer := time.AfterFunc(time.Duration(debounceParams.intervalMs)*time.Millisecond, func() {
|
|
m.queryParallel(ctx, pluginInstance, query, results, done, counter)
|
|
})
|
|
onStop := func() {
|
|
logger.Debug(ctx, fmt.Sprintf("[%s] previous debounced query cancelled", pluginInstance.Metadata.Name))
|
|
counter.Add(-1)
|
|
if counter.Load() == 0 {
|
|
done <- true
|
|
}
|
|
}
|
|
m.debounceQueryTimer.Store(pluginInstance.Metadata.Id, &debounceTimer{
|
|
timer: timer,
|
|
onStop: onStop,
|
|
})
|
|
continue
|
|
} else {
|
|
logger.Error(ctx, fmt.Sprintf("[%s] %s, query directlly", pluginInstance.Metadata.Name, err))
|
|
}
|
|
}
|
|
|
|
m.queryParallel(ctx, pluginInstance, query, results, done, counter)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (m *Manager) QuerySilent(ctx context.Context, query Query) bool {
|
|
var startTimestamp = util.GetSystemTimestamp()
|
|
var results []QueryResultUI
|
|
resultChan, doneChan := m.Query(ctx, query)
|
|
for {
|
|
select {
|
|
case r := <-resultChan:
|
|
results = append(results, r...)
|
|
case <-doneChan:
|
|
logger.Info(ctx, fmt.Sprintf("silent query done, total results: %d, cost %d ms", len(results), util.GetSystemTimestamp()-startTimestamp))
|
|
|
|
// execute default action if only one result
|
|
if len(results) == 1 {
|
|
result := results[0]
|
|
for _, action := range result.Actions {
|
|
if action.IsDefault {
|
|
m.ExecuteAction(ctx, result.Id, action.Id)
|
|
return true
|
|
}
|
|
}
|
|
} else {
|
|
notifier.Notify(fmt.Sprintf("Silent query failed, there shouldbe only one result, but got %d", len(results)))
|
|
}
|
|
|
|
return false
|
|
case <-time.After(time.Minute):
|
|
logger.Error(ctx, "silent query timeout")
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *Manager) QueryFallback(ctx context.Context, query Query, queryPlugin *Instance) (results []QueryResultUI) {
|
|
var queryResults []QueryResult
|
|
if query.IsGlobalQuery() {
|
|
for _, instance := range m.instances {
|
|
pluginInstance := instance
|
|
if v, ok := pluginInstance.Plugin.(FallbackSearcher); ok {
|
|
queryResults = v.QueryFallback(ctx, query)
|
|
for i := range queryResults {
|
|
queryResults[i] = m.PolishResult(ctx, pluginInstance, query, queryResults[i])
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if query.Command != "" {
|
|
return results
|
|
}
|
|
|
|
// search query commands
|
|
commands := lo.Filter(queryPlugin.GetQueryCommands(), func(item MetadataCommand, index int) bool {
|
|
return strings.Contains(item.Command, query.Search) || query.Search == ""
|
|
})
|
|
queryResults = lo.Map(commands, func(item MetadataCommand, index int) QueryResult {
|
|
return QueryResult{
|
|
Title: item.Command,
|
|
SubTitle: item.Description,
|
|
Icon: ParseWoxImageOrDefault(queryPlugin.Metadata.Icon, NewWoxImageEmoji("🔍")),
|
|
Actions: []QueryResultAction{
|
|
{
|
|
Name: "Execute",
|
|
PreventHideAfterAction: true,
|
|
Action: func(ctx context.Context, actionContext ActionContext) {
|
|
m.ui.ChangeQuery(ctx, share.PlainQuery{
|
|
QueryType: QueryTypeInput,
|
|
QueryText: fmt.Sprintf("%s %s ", query.TriggerKeyword, item.Command),
|
|
})
|
|
},
|
|
},
|
|
},
|
|
}
|
|
})
|
|
for i := range queryResults {
|
|
queryResults[i] = m.PolishResult(ctx, queryPlugin, query, queryResults[i])
|
|
}
|
|
}
|
|
|
|
queryResultsUI := lo.Map(queryResults, func(item QueryResult, index int) QueryResultUI {
|
|
return item.ToUI()
|
|
})
|
|
results = append(results, queryResultsUI...)
|
|
return results
|
|
}
|
|
|
|
func (m *Manager) queryParallel(ctx context.Context, pluginInstance *Instance, query Query, results chan []QueryResultUI, done chan bool, counter *atomic.Int32) {
|
|
util.Go(ctx, fmt.Sprintf("[%s] parallel query", pluginInstance.Metadata.Name), func() {
|
|
queryResults := m.queryForPlugin(ctx, pluginInstance, query)
|
|
results <- lo.Map(queryResults, func(item QueryResult, index int) QueryResultUI {
|
|
return item.ToUI()
|
|
})
|
|
counter.Add(-1)
|
|
if counter.Load() == 0 {
|
|
done <- true
|
|
}
|
|
}, func() {
|
|
counter.Add(-1)
|
|
if counter.Load() == 0 {
|
|
done <- true
|
|
}
|
|
})
|
|
}
|
|
|
|
func (m *Manager) translatePlugin(ctx context.Context, pluginInstance *Instance, key string) string {
|
|
if !strings.HasPrefix(key, "i18n:") {
|
|
return key
|
|
}
|
|
|
|
if pluginInstance.IsSystemPlugin {
|
|
return i18n.GetI18nManager().TranslateWox(ctx, key)
|
|
} else {
|
|
return i18n.GetI18nManager().TranslatePlugin(ctx, key, pluginInstance.PluginDirectory)
|
|
}
|
|
}
|
|
|
|
func (m *Manager) GetUI() share.UI {
|
|
return m.ui
|
|
}
|
|
|
|
func (m *Manager) NewQuery(ctx context.Context, plainQuery share.PlainQuery) (Query, *Instance, error) {
|
|
if plainQuery.QueryType == QueryTypeInput {
|
|
newQuery := plainQuery.QueryText
|
|
woxSetting := setting.GetSettingManager().GetWoxSetting(ctx)
|
|
if len(woxSetting.QueryShortcuts) > 0 {
|
|
originQuery := plainQuery.QueryText
|
|
expandedQuery := m.expandQueryShortcut(ctx, plainQuery.QueryText, woxSetting.QueryShortcuts)
|
|
if originQuery != expandedQuery {
|
|
logger.Info(ctx, fmt.Sprintf("expand query shortcut: %s -> %s", originQuery, expandedQuery))
|
|
newQuery = expandedQuery
|
|
}
|
|
}
|
|
query, instance := newQueryInputWithPlugins(newQuery, GetPluginManager().GetPluginInstances())
|
|
query.Env.ActiveWindowTitle = m.GetUI().GetActiveWindowName()
|
|
query.Env.ActiveWindowPid = m.GetUI().GetActiveWindowPid()
|
|
query.Env.ActiveBrowserUrl = m.getActiveBrowserUrl(ctx)
|
|
return query, instance, nil
|
|
}
|
|
|
|
if plainQuery.QueryType == QueryTypeSelection {
|
|
query := Query{
|
|
Type: QueryTypeSelection,
|
|
RawQuery: plainQuery.QueryText,
|
|
Search: plainQuery.QueryText,
|
|
Selection: plainQuery.QuerySelection,
|
|
}
|
|
query.Env.ActiveWindowTitle = m.GetUI().GetActiveWindowName()
|
|
query.Env.ActiveWindowPid = m.GetUI().GetActiveWindowPid()
|
|
query.Env.ActiveBrowserUrl = m.getActiveBrowserUrl(ctx)
|
|
return query, nil, nil
|
|
}
|
|
|
|
return Query{}, nil, errors.New("invalid query type")
|
|
}
|
|
|
|
func (m *Manager) getActiveBrowserUrl(ctx context.Context) string {
|
|
activeWindowName := m.GetUI().GetActiveWindowName()
|
|
isGoogleChrome := strings.ToLower(activeWindowName) == "google chrome"
|
|
if !isGoogleChrome {
|
|
return ""
|
|
}
|
|
|
|
return m.activeBrowserUrl
|
|
}
|
|
|
|
func (m *Manager) expandQueryShortcut(ctx context.Context, query string, queryShorts []setting.QueryShortcut) (newQuery string) {
|
|
newQuery = query
|
|
|
|
//sort query shorts by shortcut length, we will expand the longest shortcut first
|
|
slices.SortFunc(queryShorts, func(i, j setting.QueryShortcut) int {
|
|
return len(j.Shortcut) - len(i.Shortcut)
|
|
})
|
|
|
|
for _, shortcut := range queryShorts {
|
|
if strings.HasPrefix(query, shortcut.Shortcut) {
|
|
if !shortcut.HasPlaceholder() {
|
|
newQuery = strings.Replace(query, shortcut.Shortcut, shortcut.Query, 1)
|
|
break
|
|
} else {
|
|
queryWithoutShortcut := strings.Replace(query, shortcut.Shortcut, "", 1)
|
|
queryWithoutShortcut = strings.TrimLeft(queryWithoutShortcut, " ")
|
|
parameters := strings.Split(queryWithoutShortcut, " ")
|
|
placeholderCount := shortcut.PlaceholderCount()
|
|
var paramsCount = 0
|
|
|
|
var params []any
|
|
var nonPrams string
|
|
for _, param := range parameters {
|
|
if paramsCount < placeholderCount {
|
|
paramsCount++
|
|
params = append(params, param)
|
|
} else {
|
|
nonPrams += " " + param
|
|
}
|
|
}
|
|
newQuery = stringFormatter.Format(shortcut.Query, params...) + nonPrams
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return newQuery
|
|
}
|
|
|
|
func (m *Manager) ExecuteAction(ctx context.Context, resultId string, actionId string) error {
|
|
resultCache, found := m.resultCache.Load(resultId)
|
|
if !found {
|
|
return fmt.Errorf("result cache not found for result id (execute action): %s", resultId)
|
|
}
|
|
action, exist := resultCache.Actions.Load(actionId)
|
|
if !exist {
|
|
return fmt.Errorf("action not found for result id: %s, action id: %s", resultId, actionId)
|
|
}
|
|
|
|
action(ctx, ActionContext{
|
|
ContextData: resultCache.ContextData,
|
|
})
|
|
|
|
util.Go(ctx, fmt.Sprintf("[%s] add actioned result", resultCache.PluginInstance.Metadata.Name), func() {
|
|
setting.GetSettingManager().AddActionedResult(ctx, resultCache.PluginInstance.Metadata.Id, resultCache.ResultTitle, resultCache.ResultSubTitle)
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) ExecuteRefresh(ctx context.Context, refreshableResultWithId RefreshableResultWithResultId) (RefreshableResultWithResultId, error) {
|
|
var refreshableResult RefreshableResult
|
|
copyErr := copier.Copy(&refreshableResult, &refreshableResultWithId)
|
|
if copyErr != nil {
|
|
return RefreshableResultWithResultId{}, fmt.Errorf("failed to copy refreshable result: %w", copyErr)
|
|
}
|
|
|
|
resultCache, found := m.resultCache.Load(refreshableResultWithId.ResultId)
|
|
if !found {
|
|
return refreshableResultWithId, fmt.Errorf("result cache not found for result id (execute refresh): %s", refreshableResultWithId.ResultId)
|
|
}
|
|
|
|
//restore actions in cache
|
|
refreshableResult.Actions = []QueryResultAction{}
|
|
for _, action := range refreshableResultWithId.Actions {
|
|
// get actual action from cache
|
|
actionFunc, exist := resultCache.Actions.Load(action.Id)
|
|
if !exist {
|
|
continue
|
|
}
|
|
refreshableResult.Actions = append(refreshableResult.Actions, QueryResultAction{
|
|
Id: action.Id,
|
|
Name: action.Name,
|
|
Icon: action.Icon,
|
|
IsDefault: action.IsDefault,
|
|
PreventHideAfterAction: action.PreventHideAfterAction,
|
|
Hotkey: action.Hotkey,
|
|
Action: actionFunc,
|
|
})
|
|
}
|
|
|
|
newResult := resultCache.Refresh(ctx, refreshableResult)
|
|
newResult = m.polishRefreshableResult(ctx, resultCache, newResult)
|
|
return RefreshableResultWithResultId{
|
|
ResultId: refreshableResultWithId.ResultId,
|
|
Title: newResult.Title,
|
|
SubTitle: newResult.SubTitle,
|
|
Icon: newResult.Icon,
|
|
Tails: newResult.Tails,
|
|
Preview: newResult.Preview,
|
|
ContextData: newResult.ContextData,
|
|
RefreshInterval: newResult.RefreshInterval,
|
|
Actions: lo.Map(newResult.Actions, func(action QueryResultAction, index int) QueryResultActionUI {
|
|
return QueryResultActionUI{
|
|
Id: action.Id,
|
|
Name: action.Name,
|
|
Icon: action.Icon,
|
|
IsDefault: action.IsDefault,
|
|
PreventHideAfterAction: action.PreventHideAfterAction,
|
|
Hotkey: action.Hotkey,
|
|
}
|
|
}),
|
|
}, nil
|
|
}
|
|
|
|
func (m *Manager) GetResultPreview(ctx context.Context, resultId string) (WoxPreview, error) {
|
|
resultCache, found := m.resultCache.Load(resultId)
|
|
if !found {
|
|
return WoxPreview{}, fmt.Errorf("result cache not found for result id (get preview): %s", resultId)
|
|
}
|
|
|
|
preview := m.polishPreview(ctx, resultCache.Preview)
|
|
|
|
// if preview text is too long, ellipsis it, otherwise UI maybe freeze when render
|
|
if preview.PreviewType == WoxPreviewTypeText {
|
|
preview.PreviewData = util.EllipsisMiddle(preview.PreviewData, 2000)
|
|
}
|
|
|
|
return preview, nil
|
|
}
|
|
|
|
func (m *Manager) polishPreview(ctx context.Context, preview WoxPreview) WoxPreview {
|
|
if preview.PreviewType == WoxPreviewTypeImage {
|
|
woxImage, err := ParseWoxImage(preview.PreviewData)
|
|
if err != nil {
|
|
logger.Error(ctx, fmt.Sprintf("failed to parse wox image for preview: %s", err.Error()))
|
|
return preview
|
|
}
|
|
|
|
if woxImage.ImageType == WoxImageTypeAbsolutePath {
|
|
newWoxImage := convertLocalImageToUrl(ctx, woxImage)
|
|
return WoxPreview{
|
|
PreviewType: WoxPreviewTypeImage,
|
|
PreviewData: newWoxImage.String(),
|
|
PreviewProperties: preview.PreviewProperties,
|
|
}
|
|
}
|
|
}
|
|
|
|
return preview
|
|
}
|
|
|
|
func (m *Manager) ReplaceQueryVariable(ctx context.Context, query string) string {
|
|
if strings.Contains(query, QueryVariableSelectedText) {
|
|
selection, selectedErr := util.GetSelected()
|
|
if selectedErr != nil {
|
|
logger.Error(ctx, fmt.Sprintf("failed to get selected text: %s", selectedErr.Error()))
|
|
} else {
|
|
if selection.Type == util.SelectionTypeText {
|
|
query = strings.ReplaceAll(query, QueryVariableSelectedText, selection.Text)
|
|
} else {
|
|
logger.Error(ctx, fmt.Sprintf("selected data is not text, type: %s", selection.Type))
|
|
}
|
|
}
|
|
}
|
|
|
|
if strings.Contains(query, QueryVariableActiveBrowserUrl) {
|
|
activeBrowserUrl := m.activeBrowserUrl
|
|
query = strings.ReplaceAll(query, QueryVariableActiveBrowserUrl, activeBrowserUrl)
|
|
}
|
|
|
|
return query
|
|
}
|
|
|
|
func (m *Manager) IsHostStarted(ctx context.Context, runtime Runtime) bool {
|
|
if runtime == PLUGIN_RUNTIME_GO {
|
|
return true
|
|
}
|
|
|
|
for _, host := range AllHosts {
|
|
if host.GetRuntime(ctx) == runtime {
|
|
return host.IsStarted(ctx)
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (m *Manager) GetAIProvider(ctx context.Context, provider ai.ProviderName) (ai.Provider, error) {
|
|
if v, exist := m.aiProviders.Load(provider); exist {
|
|
return v, nil
|
|
}
|
|
|
|
//check if provider has setting
|
|
aiProviderSettings := setting.GetSettingManager().GetWoxSetting(ctx).AIProviders
|
|
providerSetting, providerSettingExist := lo.Find(aiProviderSettings, func(item setting.AIProvider) bool {
|
|
return item.Name == string(provider)
|
|
})
|
|
if !providerSettingExist {
|
|
return nil, fmt.Errorf("ai provider setting not found: %s", provider)
|
|
}
|
|
|
|
newProvider, newProviderErr := ai.NewProvider(ctx, providerSetting)
|
|
if newProviderErr != nil {
|
|
return nil, newProviderErr
|
|
}
|
|
m.aiProviders.Store(provider, newProvider)
|
|
return newProvider, nil
|
|
}
|
|
|
|
func (m *Manager) ExecutePluginDeeplink(ctx context.Context, pluginId string, arguments map[string]string) {
|
|
pluginInstance, exist := lo.Find(m.instances, func(item *Instance) bool {
|
|
return item.Metadata.Id == pluginId
|
|
})
|
|
if !exist {
|
|
logger.Error(ctx, fmt.Sprintf("plugin not found: %s", pluginId))
|
|
return
|
|
}
|
|
|
|
for _, callback := range pluginInstance.DeepLinkCallbacks {
|
|
util.Go(ctx, fmt.Sprintf("[%s] execute deeplink callback", pluginInstance.Metadata.Name), func() {
|
|
callback(arguments)
|
|
})
|
|
}
|
|
}
|