feat(mru): Implement Most Recently Used (MRU) functionality across plugins

- Added MRU support to SysPlugin and UrlPlugin, allowing users to access recently used commands and URLs.
- Introduced new data structures for MRU items and context data.
- Updated the query methods to include MRU results based on user settings.
- Modified the settings management to accommodate the new query mode for MRU.
- Enhanced the UI to reflect the changes in query modes and added relevant translations.
- Created a dedicated MRU manager for handling MRU items in the database.
- Updated Flutter UI to support MRU queries and settings.
This commit is contained in:
qianlifeng 2025-07-30 19:54:31 +08:00
parent 9f9cf84ea8
commit 4b2018934d
No known key found for this signature in database
33 changed files with 938 additions and 94 deletions

View File

@ -36,6 +36,19 @@ type Oplog struct {
SyncedToCloud bool `gorm:"default:false"`
}
type MRURecord struct {
Hash string `gorm:"primaryKey"` // MD5 hash of pluginId+title+subTitle
PluginID string `gorm:"not null"`
Title string `gorm:"not null"`
SubTitle string
Icon string // JSON serialized WoxImage
ContextData string // Plugin context data for restoration
LastUsed int64 `gorm:"not null"`
UseCount int `gorm:"default:1"`
CreatedAt time.Time
UpdatedAt time.Time
}
func Init(ctx context.Context) error {
dbPath := filepath.Join(util.GetLocation().GetUserDataDirectory(), "wox.db")
@ -86,6 +99,7 @@ func Init(ctx context.Context) error {
&WoxSetting{},
&PluginSetting{},
&Oplog{},
&MRURecord{},
)
if err != nil {
return fmt.Errorf("failed to migrate database schema: %w", err)

View File

@ -163,6 +163,9 @@ func main() {
// Start auto backup if enabled
setting.GetSettingManager().StartAutoBackup(ctx)
// Start MRU cleanup
setting.GetSettingManager().StartMRUCleanup(ctx)
// Start auto update checker if enabled
updater.StartAutoUpdateChecker(ctx)

View File

@ -224,7 +224,7 @@ func Run(ctx context.Context) error {
"HideOnLostFocus": oldSettings.HideOnLostFocus,
"ShowTray": oldSettings.ShowTray,
"LangCode": oldSettings.LangCode,
"LastQueryMode": oldSettings.LastQueryMode,
"QueryMode": oldSettings.LastQueryMode, // Migrate LastQueryMode to QueryMode
"ShowPosition": oldSettings.ShowPosition,
"EnableAutoBackup": oldSettings.EnableAutoBackup,
"EnableAutoUpdate": oldSettings.EnableAutoUpdate,

View File

@ -37,6 +37,7 @@ type API interface {
OnGetDynamicSetting(ctx context.Context, callback func(key string) string)
OnDeepLink(ctx context.Context, callback func(arguments map[string]string))
OnUnload(ctx context.Context, callback func())
OnMRURestore(ctx context.Context, callback func(mruData MRUData) (*QueryResult, error))
RegisterQueryCommands(ctx context.Context, commands []MetadataCommand)
AIChatStream(ctx context.Context, model common.Model, conversations []common.Conversation, options common.ChatOptions, callback common.ChatStreamFunc) error
}
@ -305,6 +306,15 @@ func (a *APIImpl) applyStartTimeIfAbsent(streamResult *common.ChatStreamData) {
}
}
func (a *APIImpl) OnMRURestore(ctx context.Context, callback func(mruData MRUData) (*QueryResult, error)) {
if !a.pluginInstance.Metadata.IsSupportFeature(MetadataFeatureMRU) {
a.Log(ctx, LogLevelError, "plugin has no access to MRU feature")
return
}
a.pluginInstance.MRURestoreCallbacks = append(a.pluginInstance.MRURestoreCallbacks, callback)
}
func NewAPI(instance *Instance) API {
apiImpl := &APIImpl{pluginInstance: instance}
logFolder := path.Join(util.GetLocation().GetLogPluginDirectory(), instance.Metadata.Name)

View File

@ -19,6 +19,7 @@ type Instance struct {
SettingChangeCallbacks []func(key string, value string)
DeepLinkCallbacks []func(arguments map[string]string)
UnloadCallbacks []func()
MRURestoreCallbacks []func(mruData MRUData) (*QueryResult, error) // MRU restore callbacks
// for measure performance
LoadStartTimestamp int64

View File

@ -820,6 +820,8 @@ func (m *Manager) PolishResult(ctx context.Context, pluginInstance *Instance, qu
}
}
originalIcon := result.Icon
// convert icon
result.Icon = common.ConvertIcon(ctx, result.Icon, pluginInstance.PluginDirectory)
for i := range result.Tails {
@ -883,6 +885,7 @@ func (m *Manager) PolishResult(ctx context.Context, pluginInstance *Instance, qu
ResultTitle: result.Title,
ResultSubTitle: result.SubTitle,
ContextData: result.ContextData,
Icon: originalIcon,
PluginInstance: pluginInstance,
Query: query,
Actions: util.NewHashMap[string, func(ctx context.Context, actionContext ActionContext)](),
@ -1385,13 +1388,41 @@ func (m *Manager) ExecuteAction(ctx context.Context, resultId string, actionId s
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, resultCache.Query.RawQuery)
util.Go(ctx, fmt.Sprintf("[%s] post execute action", resultCache.PluginInstance.Metadata.Name), func() {
m.postExecuteAction(ctx, resultCache)
})
return nil
}
func (m *Manager) postExecuteAction(ctx context.Context, resultCache *QueryResultCache) {
// Add actioned result for statistics
setting.GetSettingManager().AddActionedResult(ctx, resultCache.PluginInstance.Metadata.Id, resultCache.ResultTitle, resultCache.ResultSubTitle, resultCache.Query.RawQuery)
// Add to MRU if plugin supports it
if resultCache.PluginInstance.Metadata.IsSupportFeature(MetadataFeatureMRU) {
mruItem := setting.MRUItem{
PluginID: resultCache.PluginInstance.Metadata.Id,
Title: resultCache.ResultTitle,
SubTitle: resultCache.ResultSubTitle,
Icon: resultCache.Icon,
ContextData: resultCache.ContextData,
}
if err := setting.GetSettingManager().AddMRUItem(ctx, mruItem); err != nil {
util.GetLogger().Error(ctx, fmt.Sprintf("failed to add MRU item: %s", err.Error()))
}
}
// Add to query history only if query is not empty (skip empty queries like MRU)
if resultCache.Query.RawQuery != "" {
plainQuery := common.PlainQuery{
QueryType: resultCache.Query.Type,
QueryText: resultCache.Query.RawQuery,
}
setting.GetSettingManager().AddQueryHistory(ctx, plainQuery)
}
}
func (m *Manager) ExecuteRefresh(ctx context.Context, refreshableResultWithId RefreshableResultWithResultId) (RefreshableResultWithResultId, error) {
var refreshableResult RefreshableResult
copyErr := copier.Copy(&refreshableResult, &refreshableResultWithId)
@ -1606,3 +1637,71 @@ func (m *Manager) ExecutePluginDeeplink(ctx context.Context, pluginId string, ar
})
}
}
func (m *Manager) QueryMRU(ctx context.Context) []QueryResultUI {
mruItems, err := setting.GetSettingManager().GetMRUItems(ctx, 10)
if err != nil {
util.GetLogger().Error(ctx, fmt.Sprintf("failed to get MRU items: %s", err.Error()))
return []QueryResultUI{}
}
var results []QueryResultUI
for _, item := range mruItems {
pluginInstance := m.getPluginInstance(item.PluginID)
if pluginInstance == nil {
continue
}
if !pluginInstance.Metadata.IsSupportFeature(MetadataFeatureMRU) {
continue
}
if restored := m.restoreFromMRU(ctx, pluginInstance, item); restored != nil {
polishedResult := m.PolishResult(ctx, pluginInstance, Query{}, *restored)
results = append(results, polishedResult.ToUI())
}
}
return results
}
// getPluginInstance finds a plugin instance by ID
func (m *Manager) getPluginInstance(pluginID string) *Instance {
pluginInstance, found := lo.Find(m.instances, func(item *Instance) bool {
return item.Metadata.Id == pluginID
})
if found {
return pluginInstance
}
return nil
}
// restoreFromMRU attempts to restore a QueryResult from MRU data
func (m *Manager) restoreFromMRU(ctx context.Context, pluginInstance *Instance, item setting.MRUItem) *QueryResult {
// For Go plugins, call MRU restore callbacks directly
if len(pluginInstance.MRURestoreCallbacks) > 0 {
mruData := MRUData{
PluginID: item.PluginID,
Title: item.Title,
SubTitle: item.SubTitle,
Icon: item.Icon,
ContextData: item.ContextData,
LastUsed: item.LastUsed,
UseCount: item.UseCount,
}
// Call the first (and typically only) MRU restore callback
if restored, err := pluginInstance.MRURestoreCallbacks[0](mruData); err == nil {
return restored
} else {
util.GetLogger().Debug(ctx, fmt.Sprintf("MRU restore failed for plugin %s: %s", pluginInstance.Metadata.Name, err.Error()))
}
}
// For external plugins (Python/Node.js), MRU support will be implemented later
// Currently only Go plugins support MRU functionality
if pluginInstance.Host != nil {
util.GetLogger().Debug(ctx, fmt.Sprintf("External plugin MRU restore not yet implemented for plugin %s", pluginInstance.Metadata.Name))
}
return nil
}

View File

@ -37,6 +37,10 @@ const (
// by default, the width ratio is 0.5, which means the result list and preview panel have the same width
// if the width ratio is 0.3, which means the result list takes 30% of the width and the preview panel takes 70% of the width
MetadataFeatureResultPreviewWidthRatio MetadataFeatureName = "resultPreviewWidthRatio"
// enable this feature to support MRU (Most Recently Used) functionality
// plugin must implement OnMRURestore callback to restore results from MRU data
MetadataFeatureMRU MetadataFeatureName = "mru"
)
// Metadata parsed from plugin.json, see `Plugin.json.md` for more detail

13
wox.core/plugin/mru.go Normal file
View File

@ -0,0 +1,13 @@
package plugin
import "wox/common"
type MRUData struct {
PluginID string `json:"pluginId"`
Title string `json:"title"`
SubTitle string `json:"subTitle"`
Icon common.WoxImage `json:"icon"`
ContextData string `json:"contextData"`
LastUsed int64 `json:"lastUsed"`
UseCount int `json:"useCount"`
}

View File

@ -210,6 +210,7 @@ type QueryResultCache struct {
ResultTitle string
ResultSubTitle string
ContextData string
Icon common.WoxImage
Refresh func(context.Context, RefreshableResult) RefreshableResult
PluginInstance *Instance
Query Query

View File

@ -43,6 +43,12 @@ type appInfo struct {
Pid int `json:"-"`
}
type appContextData struct {
Name string `json:"name"`
Path string `json:"path"`
Type string `json:"type"`
}
func (a *appInfo) GetDisplayPath() string {
if a.Type == AppTypeUWP {
return ""
@ -92,6 +98,11 @@ func (a *ApplicationPlugin) GetMetadata() plugin.Metadata {
"Macos",
"Linux",
},
Features: []plugin.MetadataFeature{
{
Name: plugin.MetadataFeatureMRU,
},
},
SettingDefinitions: []definition.PluginSettingDefinitionItem{
{
Type: definition.PluginSettingDefinitionTypeTable,
@ -140,6 +151,8 @@ func (a *ApplicationPlugin) Init(ctx context.Context, initParams plugin.InitPara
a.indexApps(ctx)
}
})
a.api.OnMRURestore(ctx, a.handleMRURestore)
}
func (a *ApplicationPlugin) Query(ctx context.Context, query plugin.Query) []plugin.QueryResult {
@ -149,12 +162,21 @@ func (a *ApplicationPlugin) Query(ctx context.Context, query plugin.Query) []plu
isPathNameMatch, pathNameScore := system.IsStringMatchScore(ctx, filepath.Base(info.Path), query.Search)
if isNameMatch || isPathNameMatch {
displayPath := info.GetDisplayPath()
contextData := appContextData{
Name: info.Name,
Path: info.Path,
Type: info.Type,
}
contextDataJson, _ := json.Marshal(contextData)
result := plugin.QueryResult{
Id: uuid.NewString(),
Title: info.Name,
SubTitle: displayPath,
Icon: info.Icon,
Score: util.MaxInt64(nameScore, pathNameScore),
ContextData: string(contextDataJson),
Actions: []plugin.QueryResultAction{
{
Name: "i18n:plugin_app_open",
@ -525,3 +547,87 @@ func (a *ApplicationPlugin) removeDuplicateApps(ctx context.Context, apps []appI
a.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("removed %d duplicate apps, %d apps remaining", len(apps)-len(result), len(result)))
return result
}
func (a *ApplicationPlugin) handleMRURestore(mruData plugin.MRUData) (*plugin.QueryResult, error) {
var contextData appContextData
if err := json.Unmarshal([]byte(mruData.ContextData), &contextData); err != nil {
return nil, fmt.Errorf("failed to parse context data: %w", err)
}
var appInfo *appInfo
for _, info := range a.apps {
if info.Name == contextData.Name && info.Path == contextData.Path {
appInfo = &info
break
}
}
if appInfo == nil {
return nil, fmt.Errorf("app not found: %s", contextData.Name)
}
displayPath := appInfo.GetDisplayPath()
result := &plugin.QueryResult{
Id: uuid.NewString(),
Title: appInfo.Name,
SubTitle: displayPath,
Icon: mruData.Icon,
ContextData: mruData.ContextData,
Actions: []plugin.QueryResultAction{
{
Name: "i18n:plugin_app_open",
Icon: plugin.OpenIcon,
Action: func(ctx context.Context, actionContext plugin.ActionContext) {
runErr := shell.Open(appInfo.Path)
if runErr != nil {
a.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("error opening app %s: %s", appInfo.Path, runErr.Error()))
a.api.Notify(ctx, fmt.Sprintf("i18n:plugin_app_open_failed_description: %s", runErr.Error()))
}
},
},
{
Name: "i18n:plugin_app_open_containing_folder",
Icon: plugin.OpenContainingFolderIcon,
Action: func(ctx context.Context, actionContext plugin.ActionContext) {
if err := a.retriever.OpenAppFolder(ctx, *appInfo); err != nil {
a.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("error opening folder: %s", err.Error()))
}
},
},
{
Name: "i18n:plugin_app_copy_path",
Icon: plugin.CopyIcon,
Action: func(ctx context.Context, actionContext plugin.ActionContext) {
clipboard.WriteText(appInfo.Path)
},
},
},
}
if appInfo.IsRunning() {
result.Actions = append(result.Actions, plugin.QueryResultAction{
Name: "i18n:plugin_app_terminate",
Icon: plugin.TerminateAppIcon,
Action: func(ctx context.Context, actionContext plugin.ActionContext) {
p, getErr := os.FindProcess(appInfo.Pid)
if getErr != nil {
a.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("error finding process %d: %s", appInfo.Pid, getErr.Error()))
return
}
killErr := p.Kill()
if killErr != nil {
a.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("error killing process %d: %s", appInfo.Pid, killErr.Error()))
}
},
})
result.RefreshInterval = 1000
result.OnRefresh = func(ctx context.Context, result plugin.RefreshableResult) plugin.RefreshableResult {
result.Tails = a.getRunningProcessResult(appInfo.Pid)
return result
}
}
return result, nil
}

View File

@ -58,6 +58,9 @@ func (e emptyAPIImpl) AIChatStream(ctx context.Context, model common.Model, conv
return nil
}
func (e emptyAPIImpl) OnMRURestore(ctx context.Context, callback func(mruData plugin.MRUData) (*plugin.QueryResult, error)) {
}
func TestMacRetriever_ParseAppInfo(t *testing.T) {
if util.IsMacOS() {
util.GetLocation().Init()

View File

@ -2,6 +2,7 @@ package system
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
@ -23,6 +24,11 @@ type Bookmark struct {
Url string
}
type bookmarkContextData struct {
Name string `json:"name"`
Url string `json:"url"`
}
type BrowserBookmarkPlugin struct {
api plugin.API
bookmarks []Bookmark
@ -49,6 +55,11 @@ func (c *BrowserBookmarkPlugin) GetMetadata() plugin.Metadata {
"Macos",
"Linux",
},
Features: []plugin.MetadataFeature{
{
Name: plugin.MetadataFeatureMRU,
},
},
}
}
@ -97,6 +108,8 @@ func (c *BrowserBookmarkPlugin) Init(ctx context.Context, initParams plugin.Init
// Remove duplicate bookmarks (same name and url)
c.bookmarks = c.removeDuplicateBookmarks(c.bookmarks)
c.api.OnMRURestore(ctx, c.handleMRURestore)
}
func (c *BrowserBookmarkPlugin) Query(ctx context.Context, query plugin.Query) (results []plugin.QueryResult) {
@ -122,11 +135,18 @@ func (c *BrowserBookmarkPlugin) Query(ctx context.Context, query plugin.Query) (
}
if isMatch {
contextData := bookmarkContextData{
Name: bookmark.Name,
Url: bookmark.Url,
}
contextDataJson, _ := json.Marshal(contextData)
results = append(results, plugin.QueryResult{
Title: bookmark.Name,
SubTitle: bookmark.Url,
Score: matchScore,
Icon: browserBookmarkIcon,
ContextData: string(contextDataJson),
Actions: []plugin.QueryResultAction{
{
Name: "i18n:plugin_browser_bookmark_open_in_browser",
@ -235,3 +255,40 @@ func (c *BrowserBookmarkPlugin) removeDuplicateBookmarks(bookmarks []Bookmark) [
return result
}
func (c *BrowserBookmarkPlugin) handleMRURestore(mruData plugin.MRUData) (*plugin.QueryResult, error) {
var contextData bookmarkContextData
if err := json.Unmarshal([]byte(mruData.ContextData), &contextData); err != nil {
return nil, fmt.Errorf("failed to parse context data: %w", err)
}
// Check if bookmark still exists in current bookmarks
found := false
for _, bookmark := range c.bookmarks {
if bookmark.Name == contextData.Name && bookmark.Url == contextData.Url {
found = true
break
}
}
if !found {
return nil, fmt.Errorf("bookmark no longer exists: %s", contextData.Name)
}
result := &plugin.QueryResult{
Title: contextData.Name,
SubTitle: contextData.Url,
Icon: mruData.Icon,
ContextData: mruData.ContextData,
Actions: []plugin.QueryResultAction{
{
Name: "i18n:plugin_browser_bookmark_open_in_browser",
Action: func(ctx context.Context, actionContext plugin.ActionContext) {
shell.Open(contextData.Url)
},
},
},
}
return result, nil
}

View File

@ -270,3 +270,5 @@ func (m *mockAPI) RegisterQueryCommands(ctx context.Context, commands []plugin.M
func (m *mockAPI) AIChatStream(ctx context.Context, model common.Model, conversations []common.Conversation, options common.ChatOptions, callback common.ChatStreamFunc) error {
return nil
}
func (m *mockAPI) OnMRURestore(ctx context.Context, callback func(mruData plugin.MRUData) (*plugin.QueryResult, error)) {
}

View File

@ -2,6 +2,7 @@ package system
import (
"context"
"encoding/json"
"fmt"
"wox/common"
"wox/i18n"
@ -21,6 +22,12 @@ type IndicatorPlugin struct {
api plugin.API
}
type indicatorContextData struct {
TriggerKeyword string `json:"triggerKeyword"`
PluginID string `json:"pluginId"`
Command string `json:"command,omitempty"`
}
func (i *IndicatorPlugin) GetMetadata() plugin.Metadata {
return plugin.Metadata{
Id: "38564bf0-75ad-4b3e-8afe-a0e0a287c42e",
@ -41,11 +48,17 @@ func (i *IndicatorPlugin) GetMetadata() plugin.Metadata {
"Macos",
"Linux",
},
Features: []plugin.MetadataFeature{
{
Name: plugin.MetadataFeatureMRU,
},
},
}
}
func (i *IndicatorPlugin) Init(ctx context.Context, initParams plugin.InitParams) {
i.api = initParams.API
i.api.OnMRURestore(ctx, i.handleMRURestore)
}
func (i *IndicatorPlugin) Query(ctx context.Context, query plugin.Query) []plugin.QueryResult {
@ -55,12 +68,19 @@ func (i *IndicatorPlugin) Query(ctx context.Context, query plugin.Query) []plugi
return triggerKeyword != "*" && IsStringMatchNoPinYin(ctx, triggerKeyword, query.Search)
})
if found {
contextData := indicatorContextData{
TriggerKeyword: triggerKeyword,
PluginID: pluginInstance.Metadata.Id,
}
contextDataJson, _ := json.Marshal(contextData)
results = append(results, plugin.QueryResult{
Id: uuid.NewString(),
Title: triggerKeyword,
SubTitle: fmt.Sprintf(i18n.GetI18nManager().TranslateWox(ctx, "plugin_indicator_activate_plugin"), pluginInstance.Metadata.Name),
Score: 10,
Icon: pluginInstance.Metadata.GetIconOrDefault(pluginInstance.PluginDirectory, indicatorIcon),
ContextData: string(contextDataJson),
Actions: []plugin.QueryResultAction{
{
Name: "i18n:plugin_indicator_activate",
@ -101,3 +121,59 @@ func (i *IndicatorPlugin) Query(ctx context.Context, query plugin.Query) []plugi
}
return results
}
func (i *IndicatorPlugin) handleMRURestore(mruData plugin.MRUData) (*plugin.QueryResult, error) {
var contextData indicatorContextData
if err := json.Unmarshal([]byte(mruData.ContextData), &contextData); err != nil {
return nil, fmt.Errorf("failed to parse context data: %w", err)
}
// Find the plugin instance by ID
var pluginInstance *plugin.Instance
for _, instance := range plugin.GetPluginManager().GetPluginInstances() {
if instance.Metadata.Id == contextData.PluginID {
pluginInstance = instance
break
}
}
if pluginInstance == nil {
return nil, fmt.Errorf("plugin no longer exists: %s", contextData.PluginID)
}
// Check if trigger keyword still exists
triggerKeywords := pluginInstance.GetTriggerKeywords()
found := false
for _, keyword := range triggerKeywords {
if keyword == contextData.TriggerKeyword {
found = true
break
}
}
if !found {
return nil, fmt.Errorf("trigger keyword no longer exists: %s", contextData.TriggerKeyword)
}
result := &plugin.QueryResult{
Id: uuid.NewString(),
Title: contextData.TriggerKeyword,
SubTitle: fmt.Sprintf(i18n.GetI18nManager().TranslateWox(context.Background(), "plugin_indicator_activate_plugin"), pluginInstance.Metadata.Name),
Icon: mruData.Icon,
ContextData: mruData.ContextData,
Actions: []plugin.QueryResultAction{
{
Name: "i18n:plugin_indicator_activate",
PreventHideAfterAction: true,
Action: func(ctx context.Context, actionContext plugin.ActionContext) {
i.api.ChangeQuery(ctx, common.PlainQuery{
QueryType: plugin.QueryTypeInput,
QueryText: fmt.Sprintf("%s ", contextData.TriggerKeyword),
})
},
},
},
}
return result, nil
}

View File

@ -2,6 +2,7 @@ package system
import (
"context"
"encoding/json"
"fmt"
"os"
"path"
@ -34,6 +35,10 @@ type SysCommand struct {
Action func(ctx context.Context, actionContext plugin.ActionContext)
}
type sysContextData struct {
CommandTitle string `json:"commandTitle"`
}
func (r *SysPlugin) GetMetadata() plugin.Metadata {
return plugin.Metadata{
Id: "227f7d64-df08-4e35-ad05-98a26d540d06",
@ -55,6 +60,11 @@ func (r *SysPlugin) GetMetadata() plugin.Metadata {
"Macos",
"Linux",
},
Features: []plugin.MetadataFeature{
{
Name: plugin.MetadataFeatureMRU,
},
},
}
}
@ -186,11 +196,17 @@ func (r *SysPlugin) Query(ctx context.Context, query plugin.Query) (results []pl
}
if isTitleMatch {
contextData := sysContextData{
CommandTitle: command.Title,
}
contextDataJson, _ := json.Marshal(contextData)
results = append(results, plugin.QueryResult{
Title: command.Title,
SubTitle: command.SubTitle,
Score: titleScore,
Icon: command.Icon,
ContextData: string(contextDataJson),
Actions: []plugin.QueryResultAction{
{
Name: "i18n:plugin_sys_execute",
@ -200,6 +216,8 @@ func (r *SysPlugin) Query(ctx context.Context, query plugin.Query) (results []pl
},
})
}
r.api.OnMRURestore(ctx, r.handleMRURestore)
}
for _, instance := range plugin.GetPluginManager().GetPluginInstances() {
@ -236,3 +254,39 @@ func (r *SysPlugin) Query(ctx context.Context, query plugin.Query) (results []pl
return
}
func (r *SysPlugin) handleMRURestore(mruData plugin.MRUData) (*plugin.QueryResult, error) {
var contextData sysContextData
if err := json.Unmarshal([]byte(mruData.ContextData), &contextData); err != nil {
return nil, fmt.Errorf("failed to parse context data: %w", err)
}
// Find the command by title
var foundCommand *SysCommand
for _, command := range r.commands {
if command.Title == contextData.CommandTitle {
foundCommand = &command
break
}
}
if foundCommand == nil {
return nil, fmt.Errorf("system command no longer exists: %s", contextData.CommandTitle)
}
result := &plugin.QueryResult{
Title: foundCommand.Title,
SubTitle: foundCommand.SubTitle,
Icon: mruData.Icon,
ContextData: mruData.ContextData,
Actions: []plugin.QueryResultAction{
{
Name: "i18n:plugin_sys_execute",
Action: foundCommand.Action,
PreventHideAfterAction: foundCommand.PreventHideAfterAction,
},
},
}
return result, nil
}

View File

@ -26,6 +26,12 @@ type UrlHistory struct {
Title string
}
type urlContextData struct {
Url string `json:"url"`
Title string `json:"title"`
Type string `json:"type"` // "history" or "direct"
}
type UrlPlugin struct {
api plugin.API
reg *regexp.Regexp
@ -53,6 +59,11 @@ func (r *UrlPlugin) GetMetadata() plugin.Metadata {
"Macos",
"Linux",
},
Features: []plugin.MetadataFeature{
{
Name: plugin.MetadataFeatureMRU,
},
},
}
}
@ -60,6 +71,8 @@ func (r *UrlPlugin) Init(ctx context.Context, initParams plugin.InitParams) {
r.api = initParams.API
r.reg = r.getReg()
r.recentUrls = r.loadRecentUrls(ctx)
r.api.OnMRURestore(ctx, r.handleMRURestore)
}
func (r *UrlPlugin) loadRecentUrls(ctx context.Context) []UrlHistory {
@ -90,11 +103,19 @@ func (r *UrlPlugin) Query(ctx context.Context, query plugin.Query) (results []pl
})
for _, history := range existingUrlHistory {
contextData := urlContextData{
Url: history.Url,
Title: history.Title,
Type: "history",
}
contextDataJson, _ := json.Marshal(contextData)
results = append(results, plugin.QueryResult{
Title: history.Url,
SubTitle: history.Title,
Score: 100,
Icon: history.Icon.Overlay(urlIcon, 0.4, 0.6, 0.6),
ContextData: string(contextDataJson),
Actions: []plugin.QueryResultAction{
{
Name: "i18n:plugin_url_open",
@ -119,11 +140,19 @@ func (r *UrlPlugin) Query(ctx context.Context, query plugin.Query) (results []pl
}
if len(r.reg.FindStringIndex(query.Search)) > 0 {
contextData := urlContextData{
Url: query.Search,
Title: "",
Type: "direct",
}
contextDataJson, _ := json.Marshal(contextData)
results = append(results, plugin.QueryResult{
Title: query.Search,
SubTitle: "i18n:plugin_url_open_in_browser",
Score: 100,
Icon: urlIcon,
ContextData: string(contextDataJson),
Actions: []plugin.QueryResultAction{
{
Name: "i18n:plugin_url_open",
@ -202,3 +231,77 @@ func (r *UrlPlugin) removeRecentUrl(ctx context.Context, url string) {
r.api.SaveSetting(ctx, "recentUrls", string(urlsJson), false)
}
func (r *UrlPlugin) handleMRURestore(mruData plugin.MRUData) (*plugin.QueryResult, error) {
var contextData urlContextData
if err := json.Unmarshal([]byte(mruData.ContextData), &contextData); err != nil {
return nil, fmt.Errorf("failed to parse context data: %w", err)
}
url := contextData.Url
if !strings.HasPrefix(url, "http") {
url = "https://" + url
}
result := &plugin.QueryResult{
Title: contextData.Url,
SubTitle: contextData.Title,
Icon: mruData.Icon,
ContextData: mruData.ContextData,
}
if contextData.Type == "history" {
found := false
for _, history := range r.recentUrls {
if history.Url == contextData.Url {
found = true
result.SubTitle = history.Title
break
}
}
if !found {
return nil, fmt.Errorf("URL no longer in history: %s", contextData.Url)
}
result.Actions = []plugin.QueryResultAction{
{
Name: "i18n:plugin_url_open",
Icon: plugin.OpenIcon,
Action: func(ctx context.Context, actionContext plugin.ActionContext) {
openErr := shell.Open(url)
if openErr != nil {
r.api.Log(ctx, "Error opening URL", openErr.Error())
}
},
},
{
Name: "i18n:plugin_url_remove",
Icon: plugin.TrashIcon,
Action: func(ctx context.Context, actionContext plugin.ActionContext) {
r.removeRecentUrl(ctx, contextData.Url)
},
},
}
} else {
result.SubTitle = "i18n:plugin_url_open_in_browser"
result.Actions = []plugin.QueryResultAction{
{
Name: "i18n:plugin_url_open",
Icon: urlIcon,
Action: func(ctx context.Context, actionContext plugin.ActionContext) {
openErr := shell.Open(url)
if openErr != nil {
r.api.Log(ctx, "Error opening URL", openErr.Error())
} else {
util.Go(ctx, "saveRecentUrl", func() {
r.saveRecentUrl(ctx, url)
})
}
},
},
}
}
return result, nil
}

View File

@ -9,10 +9,11 @@
"ui_selection_hotkey_tips": "Hotkeys to do actions on selected text or files",
"ui_use_pinyin": "Use Pinyin",
"ui_use_pinyin_tips": "When selected, Wox will convert Chinese into Pinyin",
"ui_last_query_mode": "Last Query Mode",
"ui_last_query_mode_tips": "Choose the default behavior when opening Wox",
"ui_last_query_mode_preserve": "Preserve last query",
"ui_last_query_mode_empty": "Always start with empty query",
"ui_query_mode": "Query Mode",
"ui_query_mode_tips": "Choose the default behavior when opening Wox",
"ui_query_mode_preserve": "Preserve last query",
"ui_query_mode_empty": "Always start with empty query",
"ui_query_mode_mru": "Show most recently used items",
"ui_hide_on_lost_focus": "Hide on lost focus",
"ui_hide_on_lost_focus_tips": "When selected, Wox will hide when it loses focus",
"ui_hide_on_start": "Hide on start",
@ -386,6 +387,7 @@
"plugin_ai_chat_default_model_tooltip": "The default model to use for this command",
"plugin_ai_chat_enable_fallback_search": "Fallback Search",
"plugin_ai_chat_enable_fallback_search_tooltip": "When enabled, if the query result is empty, Wox will return a result of 'use AI Chat for %s'",
"plugin_app_open_failed_description": "Failed to open: %s",
"plugin_ai_chat_fallback_search_chat_for": "Use AI Chat for %s",
"plugin_ai_chat_start_chat": "Start Chat",
"plugin_ai_chat_enable_auto_focus_to_chat_input": "Auto focus to chat input when open with query hotkey",

View File

@ -13,6 +13,9 @@
"ui_last_query_mode_tips": "Escolha o comportamento padrão ao abrir o Wox",
"ui_last_query_mode_preserve": "Preservar última consulta",
"ui_last_query_mode_empty": "Sempre começar com consulta vazia",
"ui_last_query_mode_mru": "Mostrar itens usados recentemente",
"ui_last_query_mode_preserve": "Preservar última consulta",
"ui_last_query_mode_empty": "Sempre começar com consulta vazia",
"ui_hide_on_lost_focus": "Ocultar na perda do foco",
"ui_hide_on_lost_focus_tips": "Quando selecionado, o Wox será ocultado na perda do foco",
"ui_hide_on_start": "Ocultar ao iniciar",

View File

@ -13,6 +13,9 @@
"ui_last_query_mode_tips": "Выберите поведение по умолчанию при открытии Wox",
"ui_last_query_mode_preserve": "Сохранить последний запрос",
"ui_last_query_mode_empty": "Всегда начинать с пустого запроса",
"ui_last_query_mode_mru": "Показать недавно использованные элементы",
"ui_last_query_mode_preserve": "Сохранить последний запрос",
"ui_last_query_mode_empty": "Всегда начинать с пустого запроса",
"ui_hide_on_lost_focus": "Скрывать при потере фокуса",
"ui_hide_on_lost_focus_tips": "При выборе Wox будет скрываться при потере фокуса",
"ui_hide_on_start": "Скрывать при запуске",

View File

@ -9,10 +9,11 @@
"ui_selection_hotkey_tips": "用于在选定的文本或文件上执行操作的快捷键",
"ui_use_pinyin": "使用拼音",
"ui_use_pinyin_tips": "搜索时,把中文转换为拼音",
"ui_last_query_mode": "上次查询模式",
"ui_last_query_mode_tips": "选择打开Wox时的默认行为",
"ui_last_query_mode_preserve": "保留上次查询",
"ui_last_query_mode_empty": "总是从空查询开始",
"ui_query_mode": "查询模式",
"ui_query_mode_tips": "选择打开Wox时的默认行为",
"ui_query_mode_preserve": "保留上次查询",
"ui_query_mode_empty": "总是从空查询开始",
"ui_query_mode_mru": "显示最近使用的项目",
"ui_hide_on_lost_focus": "失去焦点时隐藏",
"ui_hide_on_lost_focus_tips": "选中后Wox失去焦点时将隐藏",
"ui_hide_on_start": "启动时隐藏",
@ -386,6 +387,11 @@
"plugin_ai_chat_default_model_tooltip": "用于对话的默认模型",
"plugin_ai_chat_enable_fallback_search": "回退搜索",
"plugin_ai_chat_enable_fallback_search_tooltip": "当查询结果为空时Wox 将返回一个 '使用 AI Chat 查询 %s' 的结果",
"plugin_app_open": "打开",
"plugin_app_open_containing_folder": "打开所在文件夹",
"plugin_app_copy_path": "复制路径",
"plugin_app_terminate": "终止进程",
"plugin_app_open_failed_description": "打开失败:%s",
"plugin_ai_chat_fallback_search_chat_for": "使用 AI Chat 查询 %s",
"plugin_ai_chat_start_chat": "开始对话",
"plugin_ai_chat_enable_auto_focus_to_chat_input": "使用查询快捷键打开时自动聚焦到对话输入框",

View File

@ -4,10 +4,13 @@ import (
"context"
"fmt"
"sync"
"time"
"wox/common"
"wox/database"
"wox/util"
"wox/util/autostart"
"github.com/samber/lo"
)
var managerInstance *Manager
@ -16,6 +19,7 @@ var logger *util.Log
type Manager struct {
woxSetting *WoxSetting
mruManager *MRUManager
}
func GetSettingManager() *Manager {
@ -30,6 +34,7 @@ func GetSettingManager() *Manager {
store := NewWoxSettingStore(db)
managerInstance = &Manager{}
managerInstance.woxSetting = NewWoxSetting(store)
managerInstance.mruManager = NewMRUManager(db)
})
return managerInstance
}
@ -77,14 +82,14 @@ func (m *Manager) GetWoxSetting(ctx context.Context) *WoxSetting {
return m.woxSetting
}
func (m *Manager) GetLatestQueryHistory(ctx context.Context, limit int) []common.PlainQuery {
func (m *Manager) GetLatestQueryHistory(ctx context.Context, limit int) []QueryHistory {
histories := m.woxSetting.QueryHistories.Get()
// Sort by timestamp descending and limit results
var result []common.PlainQuery
var result []QueryHistory
count := 0
for i := len(histories) - 1; i >= 0 && count < limit; i-- {
result = append(result, histories[i].Query)
result = append(result, histories[i])
count++
}
@ -137,3 +142,64 @@ func (m *Manager) RemoveFavoriteResult(ctx context.Context, pluginId string, res
favoriteResults.Delete(resultHash)
m.woxSetting.FavoriteResults.Set(favoriteResults)
}
func (m *Manager) AddQueryHistory(ctx context.Context, query common.PlainQuery) {
histories := m.woxSetting.QueryHistories.Get()
newHistory := QueryHistory{
Query: query,
Timestamp: util.GetSystemTimestamp(),
}
// Remove duplicate if exists (same query text)
histories = lo.Filter(histories, func(item QueryHistory, index int) bool {
return !item.Query.IsEmpty() && item.Query.QueryText != query.QueryText
})
// Add new history at the end
histories = append(histories, newHistory)
// Keep only the most recent 1000 entries
if len(histories) > 1000 {
histories = histories[len(histories)-1000:]
}
m.woxSetting.QueryHistories.Set(histories)
}
// MRU related methods
func (m *Manager) AddMRUItem(ctx context.Context, item MRUItem) error {
return m.mruManager.AddMRUItem(ctx, item)
}
func (m *Manager) GetMRUItems(ctx context.Context, limit int) ([]MRUItem, error) {
return m.mruManager.GetMRUItems(ctx, limit)
}
func (m *Manager) RemoveMRUItem(ctx context.Context, pluginID, title, subTitle string) error {
return m.mruManager.RemoveMRUItem(ctx, pluginID, title, subTitle)
}
func (m *Manager) CleanupOldMRUItems(ctx context.Context, keepCount int) error {
return m.mruManager.CleanupOldMRUItems(ctx, keepCount)
}
// StartMRUCleanup starts a background goroutine to periodically clean up old MRU items
func (m *Manager) StartMRUCleanup(ctx context.Context) {
util.Go(ctx, "MRU cleanup", func() {
ticker := time.NewTicker(24 * time.Hour) // Clean up once per day
defer ticker.Stop()
for {
select {
case <-ticker.C:
// Keep only the most recent 100 MRU items
if err := m.CleanupOldMRUItems(ctx, 100); err != nil {
util.GetLogger().Error(ctx, fmt.Sprintf("failed to cleanup old MRU items: %s", err.Error()))
}
case <-ctx.Done():
return
}
}
})
}

153
wox.core/setting/mru.go Normal file
View File

@ -0,0 +1,153 @@
package setting
import (
"context"
"encoding/json"
"fmt"
"time"
"wox/common"
"wox/database"
"wox/util"
"gorm.io/gorm"
)
// MRUItem represents a Most Recently Used item
type MRUItem struct {
PluginID string `json:"pluginId"`
Title string `json:"title"`
SubTitle string `json:"subTitle"`
Icon common.WoxImage `json:"icon"`
ContextData string `json:"contextData"`
LastUsed int64 `json:"lastUsed"`
UseCount int `json:"useCount"`
}
// MRUManager manages Most Recently Used items
type MRUManager struct {
db *gorm.DB
}
// NewMRUManager creates a new MRU manager
func NewMRUManager(db *gorm.DB) *MRUManager {
return &MRUManager{db: db}
}
// AddMRUItem adds or updates an MRU item
func (m *MRUManager) AddMRUItem(ctx context.Context, item MRUItem) error {
hash := NewResultHash(item.PluginID, item.Title, item.SubTitle)
// Serialize icon to JSON
iconData, err := json.Marshal(item.Icon)
if err != nil {
return fmt.Errorf("failed to serialize icon: %w", err)
}
now := time.Now()
timestamp := util.GetSystemTimestamp()
// Check if record exists
var existingRecord database.MRURecord
err = m.db.Where("hash = ?", string(hash)).First(&existingRecord).Error
if err == gorm.ErrRecordNotFound {
// Create new record
record := database.MRURecord{
Hash: string(hash),
PluginID: item.PluginID,
Title: item.Title,
SubTitle: item.SubTitle,
Icon: string(iconData),
ContextData: item.ContextData,
LastUsed: timestamp,
UseCount: 1,
CreatedAt: now,
UpdatedAt: now,
}
return m.db.Create(&record).Error
} else if err != nil {
return fmt.Errorf("failed to query MRU record: %w", err)
} else {
// Update existing record
updates := map[string]interface{}{
"last_used": timestamp,
"use_count": existingRecord.UseCount + 1,
"context_data": item.ContextData, // Update context data in case it changed
"icon": string(iconData), // Update icon in case it changed
"updated_at": now,
}
return m.db.Model(&existingRecord).Updates(updates).Error
}
}
// GetMRUItems retrieves MRU items sorted by usage
func (m *MRUManager) GetMRUItems(ctx context.Context, limit int) ([]MRUItem, error) {
var records []database.MRURecord
// Order by last_used DESC, then by use_count DESC for items with same last_used time
err := m.db.Order("last_used DESC, use_count DESC").Limit(limit).Find(&records).Error
if err != nil {
return nil, fmt.Errorf("failed to query MRU records: %w", err)
}
items := make([]MRUItem, 0, len(records))
for _, record := range records {
// Deserialize icon
var icon common.WoxImage
if err := json.Unmarshal([]byte(record.Icon), &icon); err != nil {
util.GetLogger().Warn(ctx, fmt.Sprintf("failed to deserialize icon for MRU item %s: %s", record.Hash, err.Error()))
icon = common.WoxImage{} // Use empty icon as fallback
}
items = append(items, MRUItem{
PluginID: record.PluginID,
Title: record.Title,
SubTitle: record.SubTitle,
Icon: icon,
ContextData: record.ContextData,
LastUsed: record.LastUsed,
UseCount: record.UseCount,
})
}
return items, nil
}
// RemoveMRUItem removes an MRU item by hash
func (m *MRUManager) RemoveMRUItem(ctx context.Context, pluginID, title, subTitle string) error {
hash := NewResultHash(pluginID, title, subTitle)
result := m.db.Where("hash = ?", string(hash)).Delete(&database.MRURecord{})
if result.Error != nil {
return fmt.Errorf("failed to remove MRU item: %w", result.Error)
}
util.GetLogger().Debug(ctx, fmt.Sprintf("removed MRU item: %s", hash))
return nil
}
// CleanupOldMRUItems removes old MRU items to keep the database size manageable
func (m *MRUManager) CleanupOldMRUItems(ctx context.Context, keepCount int) error {
// Keep only the most recent keepCount items
subQuery := m.db.Model(&database.MRURecord{}).
Select("hash").
Order("last_used DESC, use_count DESC").
Limit(keepCount)
result := m.db.Where("hash NOT IN (?)", subQuery).Delete(&database.MRURecord{})
if result.Error != nil {
return fmt.Errorf("failed to cleanup old MRU items: %w", result.Error)
}
if result.RowsAffected > 0 {
util.GetLogger().Info(ctx, fmt.Sprintf("cleaned up %d old MRU items", result.RowsAffected))
}
return nil
}
// GetMRUCount returns the total number of MRU items
func (m *MRUManager) GetMRUCount(ctx context.Context) (int64, error) {
var count int64
err := m.db.Model(&database.MRURecord{}).Count(&count).Error
return count, err
}

View File

@ -22,7 +22,7 @@ type WoxSetting struct {
LangCode *WoxSettingValue[i18n.LangCode]
QueryHotkeys *PlatformValue[[]QueryHotkey]
QueryShortcuts *WoxSettingValue[[]QueryShortcut]
LastQueryMode *WoxSettingValue[LastQueryMode]
QueryMode *WoxSettingValue[QueryMode]
ShowPosition *WoxSettingValue[PositionType]
AIProviders *WoxSettingValue[[]AIProvider]
EnableAutoBackup *WoxSettingValue[bool]
@ -49,7 +49,7 @@ type WoxSetting struct {
ActionedResults *WoxSettingValue[*util.HashMap[ResultHash, []ActionedResult]]
}
type LastQueryMode = string
type QueryMode = string
type PositionType string
@ -60,8 +60,9 @@ const (
)
const (
LastQueryModePreserve LastQueryMode = "preserve" // preserve last query and select all for quick modify
LastQueryModeEmpty LastQueryMode = "empty" // empty last query
QueryModePreserve QueryMode = "preserve" // preserve last query and select all for quick modify
QueryModeEmpty QueryMode = "empty" // empty last query
QueryModeMRU QueryMode = "mru" // show MRU (Most Recently Used) list
)
const (
@ -134,7 +135,7 @@ func NewWoxSetting(store *WoxSettingStore) *WoxSetting {
LangCode: NewWoxSettingValueWithValidator(store, "LangCode", defaultLangCode, func(code i18n.LangCode) bool {
return i18n.IsSupportedLangCode(string(code))
}),
LastQueryMode: NewWoxSettingValue(store, "LastQueryMode", LastQueryModeEmpty),
QueryMode: NewWoxSettingValue(store, "QueryMode", QueryModeEmpty),
ShowPosition: NewWoxSettingValue(store, "ShowPosition", PositionTypeMouseScreen),
AppWidth: NewWoxSettingValue(store, "AppWidth", 800),
MaxResultCount: NewWoxSettingValue(store, "MaxResultCount", 10),

View File

@ -17,7 +17,7 @@ type WoxSettingDto struct {
LangCode i18n.LangCode
QueryHotkeys []setting.QueryHotkey
QueryShortcuts []setting.QueryShortcut
LastQueryMode setting.LastQueryMode
QueryMode setting.QueryMode
AIProviders []setting.AIProvider
HttpProxyEnabled bool
HttpProxyUrl string

View File

@ -88,6 +88,7 @@ var routers = map[string]func(w http.ResponseWriter, r *http.Request){
"/hotkey/available": handleHotkeyAvailable,
"/query/icon": handleQueryIcon,
"/query/ratio": handleQueryRatio,
"/query/mru": handleQueryMRU,
"/deeplink": handleDeeplink,
"/version": handleVersion,
}
@ -484,7 +485,7 @@ func handleSettingWox(w http.ResponseWriter, r *http.Request) {
settingDto.LangCode = woxSetting.LangCode.Get()
settingDto.QueryHotkeys = woxSetting.QueryHotkeys.Get()
settingDto.QueryShortcuts = woxSetting.QueryShortcuts.Get()
settingDto.LastQueryMode = woxSetting.LastQueryMode.Get()
settingDto.QueryMode = woxSetting.QueryMode.Get()
settingDto.AIProviders = woxSetting.AIProviders.Get()
settingDto.HttpProxyEnabled = woxSetting.HttpProxyEnabled.Get()
settingDto.HttpProxyUrl = woxSetting.HttpProxyUrl.Get()
@ -537,8 +538,8 @@ func handleSettingWoxUpdate(w http.ResponseWriter, r *http.Request) {
woxSetting.ShowTray.Set(kv.Value.(bool))
case "LangCode":
woxSetting.LangCode.Set(i18n.LangCode(kv.Value.(string)))
case "LastQueryMode":
woxSetting.LastQueryMode.Set(setting.LastQueryMode(kv.Value.(string)))
case "QueryMode":
woxSetting.QueryMode.Set(setting.QueryMode(kv.Value.(string)))
case "ShowPosition":
woxSetting.ShowPosition.Set(setting.PositionType(kv.Value.(string)))
case "EnableAutoBackup":
@ -1155,3 +1156,23 @@ func handlePluginDetail(w http.ResponseWriter, r *http.Request) {
func handleVersion(w http.ResponseWriter, r *http.Request) {
writeSuccessResponse(w, updater.CURRENT_VERSION)
}
func handleQueryMRU(w http.ResponseWriter, r *http.Request) {
body, readErr := io.ReadAll(r.Body)
if readErr != nil {
writeErrorResponse(w, fmt.Sprintf("failed to read request body: %s", readErr.Error()))
return
}
var ctx context.Context
traceIdResult := gjson.GetBytes(body, "traceId")
if traceIdResult.Exists() && traceIdResult.String() != "" {
ctx = util.NewTraceContextWith(traceIdResult.String())
} else {
ctx = util.NewTraceContext()
}
mruResults := plugin.GetPluginManager().QueryMRU(ctx)
logger.Info(ctx, fmt.Sprintf("found %d MRU results", len(mruResults)))
writeSuccessResponse(w, mruResults)
}

View File

@ -203,7 +203,7 @@ func getShowAppParams(ctx context.Context, showContext common.ShowContext) map[s
"AutoFocusToChatInput": showContext.AutoFocusToChatInput,
"Position": position,
"QueryHistories": setting.GetSettingManager().GetLatestQueryHistory(ctx, 10),
"LastQueryMode": woxSetting.LastQueryMode.Get(),
"QueryMode": woxSetting.QueryMode.Get(),
}
}

View File

@ -183,6 +183,14 @@ class WoxApi {
return await WoxHttpUtil.instance.postData<List<DoctorCheckResult>>("/doctor/check", null);
}
Future<List<WoxQueryResult>> queryMRU(String traceId) async {
final response = await WoxHttpUtil.instance.postData("/query/mru", {"traceId": traceId});
if (response is List) {
return response.map((item) => WoxQueryResult.fromJson(item)).toList();
}
return [];
}
Future<String> getUserDataLocation() async {
return await WoxHttpUtil.instance.postData("/setting/userdata/location", null);
}

View File

@ -19,13 +19,14 @@ import 'package:wox/entity/wox_hotkey.dart';
import 'package:wox/entity/wox_image.dart';
import 'package:wox/entity/wox_preview.dart';
import 'package:wox/entity/wox_query.dart';
import 'package:wox/enums/wox_query_mode_enum.dart';
import 'package:wox/entity/wox_setting.dart';
import 'package:wox/entity/wox_theme.dart';
import 'package:wox/entity/wox_toolbar.dart';
import 'package:wox/entity/wox_websocket_msg.dart';
import 'package:wox/enums/wox_direction_enum.dart';
import 'package:wox/enums/wox_image_type_enum.dart';
import 'package:wox/enums/wox_last_query_mode_enum.dart';
import 'package:wox/enums/wox_msg_method_enum.dart';
import 'package:wox/enums/wox_msg_type_enum.dart';
import 'package:wox/enums/wox_position_type_enum.dart';
@ -78,7 +79,7 @@ class WoxLauncherController extends GetxController {
var currentQueryHistoryIndex = 0; // query history index, used to navigate query history
var refreshCounter = 0;
var lastQueryMode = WoxLastQueryModeEnum.WOX_LAST_QUERY_MODE_PRESERVE.code;
var lastQueryMode = WoxQueryModeEnum.WOX_QUERY_MODE_PRESERVE.code;
final isInSettingView = false.obs;
var positionBeforeOpenSetting = const Offset(0, 0);
@ -148,7 +149,6 @@ class WoxLauncherController extends GetxController {
return;
}
//ignore the results if the query id is not matched
if (currentQuery.value.queryId != receivedResults.first.queryId) {
Logger.instance.error(traceId, "query id is not matched, ignore the results");
return;
@ -215,7 +215,7 @@ class WoxLauncherController extends GetxController {
Future<void> showApp(String traceId, ShowAppParams params) async {
if (currentQuery.value.queryType == WoxQueryTypeEnum.WOX_QUERY_TYPE_INPUT.code) {
canArrowUpHistory = true;
if (lastQueryMode == WoxLastQueryModeEnum.WOX_LAST_QUERY_MODE_PRESERVE.code) {
if (lastQueryMode == WoxQueryModeEnum.WOX_QUERY_MODE_PRESERVE.code) {
//skip the first one, because it's the current query
currentQueryHistoryIndex = 0;
} else {
@ -225,7 +225,15 @@ class WoxLauncherController extends GetxController {
// update some properties to latest for later use
latestQueryHistories.assignAll(params.queryHistories);
lastQueryMode = params.lastQueryMode;
lastQueryMode = params.queryMode;
// Handle MRU mode
if (lastQueryMode == WoxQueryModeEnum.WOX_QUERY_MODE_MRU.code) {
// Clear current query and show MRU results
currentQuery.value = PlainQuery.emptyInput();
queryBoxTextFieldController.clear();
queryMRU(traceId);
}
// Handle different position types
// on linux, we need to show first and then set position or center it
@ -247,8 +255,10 @@ class WoxLauncherController extends GetxController {
}
Future<void> hideApp(String traceId) async {
//clear query box text if query type is selection or last query mode is empty
if (currentQuery.value.queryType == WoxQueryTypeEnum.WOX_QUERY_TYPE_SELECTION.code || lastQueryMode == WoxLastQueryModeEnum.WOX_LAST_QUERY_MODE_EMPTY.code) {
//clear query box text if query type is selection or last query mode is empty or MRU
if (currentQuery.value.queryType == WoxQueryTypeEnum.WOX_QUERY_TYPE_SELECTION.code ||
lastQueryMode == WoxQueryModeEnum.WOX_QUERY_MODE_EMPTY.code ||
lastQueryMode == WoxQueryModeEnum.WOX_QUERY_MODE_MRU.code) {
currentQuery.value = PlainQuery.emptyInput();
queryBoxTextFieldController.clear();
hideActionPanel(traceId);
@ -439,6 +449,24 @@ class WoxLauncherController extends GetxController {
}
}
Future<void> queryMRU(String traceId) async {
clearQueryResults(traceId);
var queryId = const UuidV4().generate();
currentQuery.value = PlainQuery.emptyInput();
currentQuery.value.queryId = queryId;
try {
final results = await WoxApi.instance.queryMRU(traceId);
for (var result in results) {
result.queryId = queryId;
}
onReceivedQueryResults(traceId, results);
} catch (e) {
Logger.instance.error(traceId, "Failed to query MRU: $e");
}
}
void onQueryChanged(String traceId, PlainQuery query, String changeReason, {bool moveCursorToEnd = false}) {
Logger.instance.debug(traceId, "query changed: ${query.queryText}, reason: $changeReason");
@ -469,7 +497,12 @@ class WoxLauncherController extends GetxController {
updateResultPreviewWidthRatioOnQueryChanged(traceId, query);
updateToolbarOnQueryChanged(traceId, query);
if (query.isEmpty) {
// Check if we should show MRU results when query is empty
if (lastQueryMode == WoxQueryModeEnum.WOX_QUERY_MODE_MRU.code) {
queryMRU(traceId);
} else {
clearQueryResults(traceId);
}
return;
}
@ -854,7 +887,7 @@ class WoxLauncherController extends GetxController {
traceId,
ShowAppParams(
queryHistories: latestQueryHistories,
lastQueryMode: lastQueryMode,
queryMode: lastQueryMode,
selectAll: true,
position: Position(
type: WoxPositionTypeEnum.POSITION_TYPE_LAST_LOCATION.code,

View File

@ -1,7 +1,7 @@
import 'package:wox/entity/wox_image.dart';
import 'package:wox/entity/wox_list_item.dart';
import 'package:wox/entity/wox_preview.dart';
import 'package:wox/enums/wox_last_query_mode_enum.dart';
import 'package:wox/enums/wox_query_mode_enum.dart';
import 'package:wox/enums/wox_position_type_enum.dart';
import 'package:wox/enums/wox_query_type_enum.dart';
import 'package:wox/enums/wox_selection_type_enum.dart';
@ -262,10 +262,10 @@ class ShowAppParams {
late bool selectAll;
late Position position;
late List<QueryHistory> queryHistories;
late WoxLastQueryMode lastQueryMode;
late WoxQueryMode queryMode;
late bool autoFocusToChatInput;
ShowAppParams({required this.selectAll, required this.position, required this.queryHistories, required this.lastQueryMode, this.autoFocusToChatInput = false});
ShowAppParams({required this.selectAll, required this.position, required this.queryHistories, required this.queryMode, this.autoFocusToChatInput = false});
ShowAppParams.fromJson(Map<String, dynamic> json) {
selectAll = json['SelectAll'];
@ -274,13 +274,10 @@ class ShowAppParams {
}
queryHistories = <QueryHistory>[];
if (json['QueryHistories'] != null) {
json['QueryHistories'].forEach((v) {
queryHistories.add(QueryHistory.fromJson(v));
});
} else {
queryHistories = <QueryHistory>[];
final List<dynamic> histories = json['QueryHistories'];
queryHistories = histories.map((v) => QueryHistory.fromJson(v)).toList();
}
lastQueryMode = json['LastQueryMode'];
queryMode = json['QueryMode'] ?? 'empty';
autoFocusToChatInput = json['AutoFocusToChatInput'] ?? false;
}
}

View File

@ -12,7 +12,7 @@ class WoxSetting {
late String langCode;
late List<QueryHotkey> queryHotkeys;
late List<QueryShortcut> queryShortcuts;
late String lastQueryMode;
late String queryMode;
late String showPosition;
late List<AIProvider> aiProviders;
late int appWidth;
@ -37,7 +37,7 @@ class WoxSetting {
required this.langCode,
required this.queryHotkeys,
required this.queryShortcuts,
required this.lastQueryMode,
required this.queryMode,
required this.showPosition,
required this.aiProviders,
required this.appWidth,
@ -81,7 +81,7 @@ class WoxSetting {
queryShortcuts = <QueryShortcut>[];
}
lastQueryMode = json['LastQueryMode'];
queryMode = json['QueryMode'] ?? 'empty';
if (json['AIProviders'] != null) {
aiProviders = <AIProvider>[];
@ -116,7 +116,7 @@ class WoxSetting {
data['LangCode'] = langCode;
data['QueryHotkeys'] = queryHotkeys;
data['QueryShortcuts'] = queryShortcuts;
data['LastQueryMode'] = lastQueryMode;
data['QueryMode'] = queryMode;
data['ShowPosition'] = showPosition;
data['AIProviders'] = aiProviders;
data['AppWidth'] = appWidth;

View File

@ -1,13 +0,0 @@
typedef WoxLastQueryMode = String;
enum WoxLastQueryModeEnum {
WOX_LAST_QUERY_MODE_PRESERVE("preserve", "preserve"),
WOX_LAST_QUERY_MODE_EMPTY("empty", "empty");
final String code;
final String value;
const WoxLastQueryModeEnum(this.code, this.value);
static String getValue(String code) => WoxLastQueryModeEnum.values.firstWhere((activity) => activity.code == code).value;
}

View File

@ -0,0 +1,14 @@
typedef WoxQueryMode = String;
enum WoxQueryModeEnum {
WOX_QUERY_MODE_PRESERVE("preserve", "preserve"),
WOX_QUERY_MODE_EMPTY("empty", "empty"),
WOX_QUERY_MODE_MRU("mru", "mru");
final String code;
final String value;
const WoxQueryModeEnum(this.code, this.value);
static String getValue(String code) => WoxQueryModeEnum.values.firstWhere((activity) => activity.code == code).value;
}

View File

@ -9,7 +9,7 @@ import 'package:wox/controllers/wox_launcher_controller.dart';
import 'package:wox/entity/setting/wox_plugin_setting_table.dart';
import 'package:wox/entity/wox_hotkey.dart';
import 'package:wox/entity/wox_lang.dart';
import 'package:wox/enums/wox_last_query_mode_enum.dart';
import 'package:wox/enums/wox_query_mode_enum.dart';
import 'package:wox/modules/setting/views/wox_setting_base.dart';
class WoxSettingGeneralView extends WoxSettingBaseView {
@ -115,26 +115,30 @@ class WoxSettingGeneralView extends WoxSettingBaseView {
}),
),
formField(
label: controller.tr("ui_last_query_mode"),
tips: controller.tr("ui_last_query_mode_tips"),
label: controller.tr("ui_query_mode"),
tips: controller.tr("ui_query_mode_tips"),
child: Obx(() {
return SizedBox(
width: 250,
child: ComboBox<String>(
items: [
ComboBoxItem(
value: WoxLastQueryModeEnum.WOX_LAST_QUERY_MODE_PRESERVE.code,
child: Text(controller.tr("ui_last_query_mode_preserve")),
value: WoxQueryModeEnum.WOX_QUERY_MODE_PRESERVE.code,
child: Text(controller.tr("ui_query_mode_preserve")),
),
ComboBoxItem(
value: WoxLastQueryModeEnum.WOX_LAST_QUERY_MODE_EMPTY.code,
child: Text(controller.tr("ui_last_query_mode_empty")),
value: WoxQueryModeEnum.WOX_QUERY_MODE_EMPTY.code,
child: Text(controller.tr("ui_query_mode_empty")),
),
ComboBoxItem(
value: WoxQueryModeEnum.WOX_QUERY_MODE_MRU.code,
child: Text(controller.tr("ui_query_mode_mru")),
),
],
value: controller.woxSetting.value.lastQueryMode,
value: controller.woxSetting.value.queryMode,
onChanged: (v) {
if (v != null) {
controller.updateConfig("LastQueryMode", v);
controller.updateConfig("QueryMode", v);
}
},
),