mirror of https://github.com/Wox-launcher/Wox
482 lines
15 KiB
Go
482 lines
15 KiB
Go
package ui
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
"wox/common"
|
|
"wox/plugin"
|
|
"wox/setting"
|
|
"wox/util"
|
|
"wox/util/notifier"
|
|
"wox/util/selection"
|
|
"wox/util/window"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/samber/lo"
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
type uiImpl struct {
|
|
requestMap *util.HashMap[string, chan WebsocketMsg]
|
|
}
|
|
|
|
func (u *uiImpl) ChangeQuery(ctx context.Context, query common.PlainQuery) {
|
|
u.invokeWebsocketMethod(ctx, "ChangeQuery", query)
|
|
}
|
|
|
|
func (u *uiImpl) HideApp(ctx context.Context) {
|
|
u.invokeWebsocketMethod(ctx, "HideApp", nil)
|
|
}
|
|
|
|
func (u *uiImpl) ShowApp(ctx context.Context, showContext common.ShowContext) {
|
|
GetUIManager().SetActiveWindowName(window.GetActiveWindowName())
|
|
GetUIManager().SetActiveWindowPid(window.GetActiveWindowPid())
|
|
u.invokeWebsocketMethod(ctx, "ShowApp", getShowAppParams(ctx, showContext))
|
|
}
|
|
|
|
func (u *uiImpl) ToggleApp(ctx context.Context) {
|
|
GetUIManager().SetActiveWindowName(window.GetActiveWindowName())
|
|
GetUIManager().SetActiveWindowPid(window.GetActiveWindowPid())
|
|
u.invokeWebsocketMethod(ctx, "ToggleApp", getShowAppParams(ctx, common.ShowContext{SelectAll: true}))
|
|
}
|
|
|
|
func (u *uiImpl) GetServerPort(ctx context.Context) int {
|
|
return GetUIManager().serverPort
|
|
}
|
|
|
|
func (u *uiImpl) ChangeTheme(ctx context.Context, theme common.Theme) {
|
|
logger.Info(ctx, fmt.Sprintf("change theme: %s", theme.ThemeName))
|
|
woxSetting := setting.GetSettingManager().GetWoxSetting(ctx)
|
|
woxSetting.ThemeId = theme.ThemeId
|
|
setting.GetSettingManager().SaveWoxSetting(ctx)
|
|
u.invokeWebsocketMethod(ctx, "ChangeTheme", theme)
|
|
}
|
|
|
|
func (u *uiImpl) InstallTheme(ctx context.Context, theme common.Theme) {
|
|
logger.Info(ctx, fmt.Sprintf("install theme: %s", theme.ThemeName))
|
|
GetStoreManager().Install(ctx, theme)
|
|
}
|
|
|
|
func (u *uiImpl) UninstallTheme(ctx context.Context, theme common.Theme) {
|
|
logger.Info(ctx, fmt.Sprintf("uninstall theme: %s", theme.ThemeName))
|
|
GetStoreManager().Uninstall(ctx, theme)
|
|
GetUIManager().ChangeToDefaultTheme(ctx)
|
|
}
|
|
|
|
func (u *uiImpl) OpenSettingWindow(ctx context.Context, windowContext common.SettingWindowContext) {
|
|
u.invokeWebsocketMethod(ctx, "OpenSettingWindow", windowContext)
|
|
}
|
|
|
|
func (u *uiImpl) GetAllThemes(ctx context.Context) []common.Theme {
|
|
return GetUIManager().GetAllThemes(ctx)
|
|
}
|
|
|
|
func (u *uiImpl) RestoreTheme(ctx context.Context) {
|
|
GetUIManager().RestoreTheme(ctx)
|
|
}
|
|
|
|
func (u *uiImpl) Notify(ctx context.Context, msg common.NotifyMsg) {
|
|
if u.isNotifyInToolbar(ctx, msg.PluginId) {
|
|
u.invokeWebsocketMethod(ctx, "ShowToolbarMsg", msg)
|
|
} else {
|
|
notifier.Notify(msg.Text)
|
|
}
|
|
}
|
|
|
|
func (u *uiImpl) FocusToChatInput(ctx context.Context) {
|
|
u.invokeWebsocketMethod(ctx, "FocusToChatInput", nil)
|
|
}
|
|
|
|
func (u *uiImpl) SendChatResponse(ctx context.Context, aiChatData common.AIChatData) {
|
|
u.invokeWebsocketMethod(ctx, "SendChatResponse", aiChatData)
|
|
}
|
|
|
|
func (u *uiImpl) ReloadChatResources(ctx context.Context, resouceName string) {
|
|
u.invokeWebsocketMethod(ctx, "ReloadChatResources", resouceName)
|
|
}
|
|
|
|
func (u *uiImpl) UpdateResult(ctx context.Context, result common.UpdateableResult) {
|
|
u.invokeWebsocketMethod(ctx, "UpdateResult", result)
|
|
}
|
|
|
|
func (u *uiImpl) isNotifyInToolbar(ctx context.Context, pluginId string) bool {
|
|
isVisible, err := u.invokeWebsocketMethod(ctx, "IsVisible", nil)
|
|
if err != nil {
|
|
logger.Error(ctx, fmt.Sprintf("isNotifyInToolbar isVisible error: %s", err.Error()))
|
|
return false
|
|
}
|
|
if !isVisible.(bool) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (u *uiImpl) PickFiles(ctx context.Context, params common.PickFilesParams) []string {
|
|
respData, err := u.invokeWebsocketMethod(ctx, "PickFiles", params)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
if _, ok := respData.([]any); !ok {
|
|
logger.Error(ctx, fmt.Sprintf("pick files response data type error: %T", respData))
|
|
return nil
|
|
}
|
|
|
|
var result []string
|
|
lo.ForEach(respData.([]any), func(file any, _ int) {
|
|
result = append(result, file.(string))
|
|
})
|
|
return result
|
|
}
|
|
|
|
func (u *uiImpl) GetActiveWindowName() string {
|
|
return GetUIManager().GetActiveWindowName()
|
|
}
|
|
|
|
func (u *uiImpl) GetActiveWindowPid() int {
|
|
return GetUIManager().GetActiveWindowPid()
|
|
}
|
|
|
|
func (u *uiImpl) invokeWebsocketMethod(ctx context.Context, method string, data any) (responseData any, responseErr error) {
|
|
requestID := uuid.NewString()
|
|
resultChan := make(chan WebsocketMsg)
|
|
u.requestMap.Store(requestID, resultChan)
|
|
defer u.requestMap.Delete(requestID)
|
|
|
|
traceId := util.GetContextTraceId(ctx)
|
|
|
|
err := requestUI(ctx, WebsocketMsg{
|
|
RequestId: requestID,
|
|
TraceId: traceId,
|
|
Method: method,
|
|
Data: data,
|
|
})
|
|
if err != nil {
|
|
logger.Error(ctx, fmt.Sprintf("send message to UI error: %s", err.Error()))
|
|
return "", err
|
|
}
|
|
|
|
var timeout = time.Second * 2
|
|
if method == "PickFiles" {
|
|
// pick files may take a long time
|
|
timeout = time.Second * 180
|
|
}
|
|
select {
|
|
case <-time.NewTimer(timeout).C:
|
|
logger.Error(ctx, fmt.Sprintf("invoke ui method %s response timeout", method))
|
|
return "", fmt.Errorf("request timeout, request id: %s", requestID)
|
|
case response := <-resultChan:
|
|
if !response.Success {
|
|
return response.Data, errors.New("ui method response error")
|
|
} else {
|
|
return response.Data, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func getShowAppParams(ctx context.Context, showContext common.ShowContext) map[string]any {
|
|
woxSetting := setting.GetSettingManager().GetWoxSetting(ctx)
|
|
var position Position
|
|
|
|
// Now we can directly use the ShowPosition as a PositionType
|
|
switch woxSetting.ShowPosition {
|
|
case setting.PositionTypeActiveScreen:
|
|
position = NewActiveScreenPosition(woxSetting.AppWidth)
|
|
case setting.PositionTypeLastLocation:
|
|
// Use saved window position if available, otherwise use mouse screen position as fallback
|
|
if woxSetting.LastWindowX != -1 && woxSetting.LastWindowY != -1 {
|
|
logger.Info(ctx, fmt.Sprintf("Using saved window position: x=%d, y=%d", woxSetting.LastWindowX, woxSetting.LastWindowY))
|
|
position = NewLastLocationPosition(woxSetting.LastWindowX, woxSetting.LastWindowY)
|
|
} else {
|
|
logger.Info(ctx, "No saved window position, using mouse screen position as fallback")
|
|
// No saved position, fallback to mouse screen position
|
|
position = NewMouseScreenPosition(woxSetting.AppWidth)
|
|
}
|
|
default: // Default to mouse screen
|
|
position = NewMouseScreenPosition(woxSetting.AppWidth)
|
|
}
|
|
|
|
return map[string]any{
|
|
"SelectAll": showContext.SelectAll,
|
|
"AutoFocusToChatInput": showContext.AutoFocusToChatInput,
|
|
"Position": position,
|
|
"QueryHistories": setting.GetSettingManager().GetLatestQueryHistory(ctx, 10),
|
|
"LastQueryMode": woxSetting.LastQueryMode,
|
|
}
|
|
}
|
|
|
|
func onUIWebsocketRequest(ctx context.Context, request WebsocketMsg) {
|
|
if request.Method != "Log" {
|
|
logger.Debug(ctx, fmt.Sprintf("got <%s> request from ui", request.Method))
|
|
}
|
|
|
|
// we handle time/amount sensitive requests in websocket, other requests in http (see router.go)
|
|
switch request.Method {
|
|
case "Log":
|
|
handleWebsocketLog(ctx, request)
|
|
case "Query":
|
|
handleWebsocketQuery(ctx, request)
|
|
case "Action":
|
|
handleWebsocketAction(ctx, request)
|
|
case "Refresh":
|
|
handleWebsocketRefresh(ctx, request)
|
|
}
|
|
}
|
|
|
|
func onUIWebsocketResponse(ctx context.Context, response WebsocketMsg) {
|
|
logger.Debug(ctx, fmt.Sprintf("got <%s> response from ui", response.Method))
|
|
|
|
requestID := response.RequestId
|
|
if requestID == "" {
|
|
logger.Error(ctx, "response id not found")
|
|
return
|
|
}
|
|
|
|
resultChan, exist := GetUIManager().GetUI(ctx).(*uiImpl).requestMap.Load(requestID)
|
|
if !exist {
|
|
logger.Error(ctx, fmt.Sprintf("response id not found: %s", requestID))
|
|
return
|
|
}
|
|
|
|
resultChan <- response
|
|
}
|
|
|
|
func handleWebsocketLog(ctx context.Context, request WebsocketMsg) {
|
|
traceId, traceIdErr := getWebsocketMsgParameter(ctx, request, "traceId")
|
|
if traceIdErr != nil {
|
|
logger.Error(ctx, traceIdErr.Error())
|
|
responseUIError(ctx, request, traceIdErr.Error())
|
|
return
|
|
}
|
|
level, levelErr := getWebsocketMsgParameter(ctx, request, "level")
|
|
if levelErr != nil {
|
|
logger.Error(ctx, levelErr.Error())
|
|
responseUIError(ctx, request, levelErr.Error())
|
|
return
|
|
}
|
|
message, messageErr := getWebsocketMsgParameter(ctx, request, "message")
|
|
if messageErr != nil {
|
|
logger.Error(ctx, messageErr.Error())
|
|
responseUIError(ctx, request, messageErr.Error())
|
|
return
|
|
}
|
|
|
|
logCtx := util.NewComponentContext(util.NewTraceContextWith(traceId), " UI")
|
|
|
|
switch level {
|
|
case "debug":
|
|
logger.Debug(logCtx, message)
|
|
case "info":
|
|
logger.Info(logCtx, message)
|
|
case "warn":
|
|
logger.Warn(logCtx, message)
|
|
case "error":
|
|
logger.Error(logCtx, message)
|
|
default:
|
|
logger.Error(ctx, fmt.Sprintf("unsupported log level: %s", level))
|
|
responseUIError(ctx, request, fmt.Sprintf("unsupported log level: %s", level))
|
|
}
|
|
responseUISuccess(ctx, request)
|
|
}
|
|
|
|
func handleWebsocketQuery(ctx context.Context, request WebsocketMsg) {
|
|
queryId, queryIdErr := getWebsocketMsgParameter(ctx, request, "queryId")
|
|
if queryIdErr != nil {
|
|
logger.Error(ctx, queryIdErr.Error())
|
|
responseUIError(ctx, request, queryIdErr.Error())
|
|
return
|
|
}
|
|
queryType, queryTypeErr := getWebsocketMsgParameter(ctx, request, "queryType")
|
|
if queryTypeErr != nil {
|
|
logger.Error(ctx, queryTypeErr.Error())
|
|
responseUIError(ctx, request, queryTypeErr.Error())
|
|
return
|
|
}
|
|
queryText, queryTextErr := getWebsocketMsgParameter(ctx, request, "queryText")
|
|
if queryTextErr != nil {
|
|
logger.Error(ctx, queryTextErr.Error())
|
|
responseUIError(ctx, request, queryTextErr.Error())
|
|
return
|
|
}
|
|
querySelectionJson, querySelectionErr := getWebsocketMsgParameter(ctx, request, "querySelection")
|
|
if querySelectionErr != nil {
|
|
logger.Error(ctx, querySelectionErr.Error())
|
|
responseUIError(ctx, request, querySelectionErr.Error())
|
|
return
|
|
}
|
|
var querySelection selection.Selection
|
|
json.Unmarshal([]byte(querySelectionJson), &querySelection)
|
|
|
|
var changedQuery common.PlainQuery
|
|
if queryType == plugin.QueryTypeInput {
|
|
changedQuery = common.PlainQuery{
|
|
QueryType: plugin.QueryTypeInput,
|
|
QueryText: queryText,
|
|
}
|
|
} else if queryType == plugin.QueryTypeSelection {
|
|
changedQuery = common.PlainQuery{
|
|
QueryType: plugin.QueryTypeSelection,
|
|
QueryText: queryText,
|
|
QuerySelection: querySelection,
|
|
}
|
|
} else {
|
|
logger.Error(ctx, fmt.Sprintf("unsupported query type: %s", queryType))
|
|
responseUIError(ctx, request, fmt.Sprintf("unsupported query type: %s", queryType))
|
|
return
|
|
}
|
|
|
|
logger.Info(ctx, fmt.Sprintf("start to handle query changed: %s, queryId: %s", changedQuery.String(), queryId))
|
|
|
|
if changedQuery.QueryType == plugin.QueryTypeInput && changedQuery.QueryText == "" {
|
|
responseUISuccessWithData(ctx, request, []string{})
|
|
return
|
|
}
|
|
if changedQuery.QueryType == plugin.QueryTypeSelection && changedQuery.QuerySelection.String() == "" {
|
|
responseUISuccessWithData(ctx, request, []string{})
|
|
return
|
|
}
|
|
|
|
query, queryPlugin, queryErr := plugin.GetPluginManager().NewQuery(ctx, changedQuery)
|
|
if queryErr != nil {
|
|
logger.Error(ctx, queryErr.Error())
|
|
responseUIError(ctx, request, queryErr.Error())
|
|
return
|
|
}
|
|
|
|
var totalResultCount int
|
|
var startTimestamp = util.GetSystemTimestamp()
|
|
var resultDebouncer = util.NewDebouncer(24, func(results []plugin.QueryResultUI, reason string) {
|
|
logger.Info(ctx, fmt.Sprintf("query %s: %s, result flushed (reason: %s), total results: %d", query.Type, query.String(), reason, totalResultCount))
|
|
responseUISuccessWithData(ctx, request, results)
|
|
})
|
|
resultDebouncer.Start(ctx)
|
|
logger.Info(ctx, fmt.Sprintf("query %s: %s, result flushed (new start)", query.Type, query.String()))
|
|
resultChan, doneChan := plugin.GetPluginManager().Query(ctx, query)
|
|
for {
|
|
select {
|
|
case results := <-resultChan:
|
|
if len(results) == 0 {
|
|
continue
|
|
}
|
|
lo.ForEach(results, func(_ plugin.QueryResultUI, index int) {
|
|
results[index].QueryId = queryId
|
|
})
|
|
totalResultCount += len(results)
|
|
resultDebouncer.Add(ctx, results)
|
|
case <-doneChan:
|
|
logger.Info(ctx, fmt.Sprintf("query done, total results: %d, cost %d ms", totalResultCount, util.GetSystemTimestamp()-startTimestamp))
|
|
|
|
// if there is no result, show fallback search
|
|
if totalResultCount == 0 {
|
|
fallbackResults := plugin.GetPluginManager().QueryFallback(ctx, query, queryPlugin)
|
|
if len(fallbackResults) > 0 {
|
|
lo.ForEach(fallbackResults, func(_ plugin.QueryResultUI, index int) {
|
|
fallbackResults[index].QueryId = queryId
|
|
})
|
|
resultDebouncer.Add(ctx, fallbackResults)
|
|
logger.Info(ctx, fmt.Sprintf("no result, show %d fallback results", len(fallbackResults)))
|
|
} else {
|
|
logger.Info(ctx, "no result, no fallback results")
|
|
}
|
|
}
|
|
|
|
resultDebouncer.Done(ctx)
|
|
return
|
|
case <-time.After(time.Minute):
|
|
logger.Info(ctx, fmt.Sprintf("query timeout, query: %s, request id: %s", query.String(), request.RequestId))
|
|
resultDebouncer.Done(ctx)
|
|
responseUIError(ctx, request, fmt.Sprintf("query timeout, query: %s, request id: %s", query.String(), request.RequestId))
|
|
return
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func handleWebsocketAction(ctx context.Context, request WebsocketMsg) {
|
|
resultId, idErr := getWebsocketMsgParameter(ctx, request, "resultId")
|
|
if idErr != nil {
|
|
logger.Error(ctx, idErr.Error())
|
|
responseUIError(ctx, request, idErr.Error())
|
|
return
|
|
}
|
|
actionId, actionIdErr := getWebsocketMsgParameter(ctx, request, "actionId")
|
|
if actionIdErr != nil {
|
|
logger.Error(ctx, actionIdErr.Error())
|
|
responseUIError(ctx, request, actionIdErr.Error())
|
|
return
|
|
}
|
|
|
|
executeErr := plugin.GetPluginManager().ExecuteAction(ctx, resultId, actionId)
|
|
if executeErr != nil {
|
|
responseUIError(ctx, request, executeErr.Error())
|
|
return
|
|
}
|
|
|
|
responseUISuccess(ctx, request)
|
|
}
|
|
|
|
func handleWebsocketRefresh(ctx context.Context, request WebsocketMsg) {
|
|
resultStr, resultErr := getWebsocketMsgParameter(ctx, request, "refreshableResult")
|
|
if resultErr != nil {
|
|
logger.Error(ctx, resultErr.Error())
|
|
responseUIError(ctx, request, resultErr.Error())
|
|
return
|
|
}
|
|
|
|
queryId, queryIdErr := getWebsocketMsgParameter(ctx, request, "queryId")
|
|
if queryIdErr != nil {
|
|
logger.Error(ctx, queryIdErr.Error())
|
|
responseUIError(ctx, request, queryIdErr.Error())
|
|
return
|
|
}
|
|
|
|
var result plugin.RefreshableResultWithResultId
|
|
unmarshalErr := json.Unmarshal([]byte(resultStr), &result)
|
|
if unmarshalErr != nil {
|
|
logger.Error(ctx, unmarshalErr.Error())
|
|
responseUIError(ctx, request, unmarshalErr.Error())
|
|
return
|
|
}
|
|
|
|
startTime := util.GetSystemTimestamp()
|
|
logger.Debug(ctx, fmt.Sprintf("start executing refresh for result: %s (resultId:%s, queryId:%s)", result.Title, result.ResultId, queryId))
|
|
|
|
// replace remote preview with local preview
|
|
if result.Preview.PreviewType == plugin.WoxPreviewTypeRemote {
|
|
preview, err := plugin.GetPluginManager().GetResultPreview(util.NewTraceContext(), result.ResultId)
|
|
if err != nil {
|
|
logger.Error(ctx, err.Error())
|
|
responseUIError(ctx, request, err.Error())
|
|
return
|
|
}
|
|
result.Preview = preview
|
|
}
|
|
|
|
newResult, refreshErr := plugin.GetPluginManager().ExecuteRefresh(ctx, result)
|
|
logger.Debug(ctx, fmt.Sprintf("finished refresh %s, cost: %dms", result.ResultId, util.GetSystemTimestamp()-startTime))
|
|
if refreshErr != nil {
|
|
logger.Error(ctx, refreshErr.Error())
|
|
responseUIError(ctx, request, refreshErr.Error())
|
|
return
|
|
}
|
|
|
|
responseUISuccessWithData(ctx, request, newResult)
|
|
}
|
|
|
|
func getWebsocketMsgParameter(ctx context.Context, msg WebsocketMsg, key string) (string, error) {
|
|
jsonData, marshalErr := json.Marshal(msg.Data)
|
|
if marshalErr != nil {
|
|
return "", marshalErr
|
|
}
|
|
|
|
paramterData := gjson.GetBytes(jsonData, key)
|
|
if !paramterData.Exists() {
|
|
return "", fmt.Errorf("%s parameter not found", key)
|
|
}
|
|
|
|
return paramterData.String(), nil
|
|
}
|