381 lines
11 KiB
Go
381 lines
11 KiB
Go
package plugin_ui
|
|
|
|
import (
|
|
goja_util "seanime/internal/util/goja"
|
|
"seanime/internal/util/result"
|
|
"slices"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/dop251/goja"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// CommandPaletteManager is a manager for the command palette.
|
|
// Unlike the Tray, command palette items are not reactive to state changes.
|
|
// They are only rendered when the setItems function is called or the refresh function is called.
|
|
type CommandPaletteManager struct {
|
|
ctx *Context
|
|
updateMutex sync.Mutex
|
|
lastUpdated time.Time
|
|
componentManager *ComponentManager
|
|
|
|
placeholder string
|
|
keyboardShortcut string
|
|
|
|
// registered is true if the command palette has been registered
|
|
registered bool
|
|
|
|
items *result.Map[string, *commandItem]
|
|
renderedItems []*CommandItemJSON // Store rendered items when setItems is called
|
|
}
|
|
|
|
type (
|
|
commandItem struct {
|
|
index int
|
|
id string
|
|
label string
|
|
value string
|
|
filterType string // "includes" or "startsWith" or ""
|
|
heading string
|
|
renderFunc func(goja.FunctionCall) goja.Value
|
|
onSelectFunc func(goja.FunctionCall) goja.Value
|
|
}
|
|
|
|
// CommandItemJSON is the JSON representation of a command item.
|
|
// It is used to send the command item to the client.
|
|
CommandItemJSON struct {
|
|
Index int `json:"index"`
|
|
ID string `json:"id"`
|
|
Label string `json:"label"`
|
|
Value string `json:"value"`
|
|
FilterType string `json:"filterType"`
|
|
Heading string `json:"heading"`
|
|
Components interface{} `json:"components"`
|
|
}
|
|
)
|
|
|
|
func NewCommandPaletteManager(ctx *Context) *CommandPaletteManager {
|
|
return &CommandPaletteManager{
|
|
ctx: ctx,
|
|
componentManager: &ComponentManager{ctx: ctx},
|
|
items: result.NewResultMap[string, *commandItem](),
|
|
renderedItems: make([]*CommandItemJSON, 0),
|
|
}
|
|
}
|
|
|
|
type NewCommandPaletteOptions struct {
|
|
Placeholder string `json:"placeholder,omitempty"`
|
|
KeyboardShortcut string `json:"keyboardShortcut,omitempty"`
|
|
}
|
|
|
|
// sendInfoToClient sends the command palette info to the client after it's been requested.
|
|
func (c *CommandPaletteManager) sendInfoToClient() {
|
|
if c.registered {
|
|
c.ctx.SendEventToClient(ServerCommandPaletteInfoEvent, ServerCommandPaletteInfoEventPayload{
|
|
Placeholder: c.placeholder,
|
|
KeyboardShortcut: c.keyboardShortcut,
|
|
})
|
|
}
|
|
}
|
|
|
|
func (c *CommandPaletteManager) jsNewCommandPalette(options NewCommandPaletteOptions) goja.Value {
|
|
c.registered = true
|
|
c.keyboardShortcut = options.KeyboardShortcut
|
|
c.placeholder = options.Placeholder
|
|
|
|
cmdObj := c.ctx.vm.NewObject()
|
|
|
|
_ = cmdObj.Set("setItems", func(items []interface{}) {
|
|
c.items.Clear()
|
|
|
|
for idx, item := range items {
|
|
itemMap := item.(map[string]interface{})
|
|
id := uuid.New().String()
|
|
label, _ := itemMap["label"].(string)
|
|
value, ok := itemMap["value"].(string)
|
|
if !ok {
|
|
c.ctx.handleTypeError("value must be a string")
|
|
return
|
|
}
|
|
filterType, _ := itemMap["filterType"].(string)
|
|
if filterType != "includes" && filterType != "startsWith" && filterType != "" {
|
|
c.ctx.handleTypeError("filterType must be 'includes', 'startsWith'")
|
|
return
|
|
}
|
|
heading, _ := itemMap["heading"].(string)
|
|
renderFunc, ok := itemMap["render"].(func(goja.FunctionCall) goja.Value)
|
|
if len(label) == 0 && !ok {
|
|
c.ctx.handleTypeError("label or render function must be provided")
|
|
return
|
|
}
|
|
onSelectFunc, ok := itemMap["onSelect"].(func(goja.FunctionCall) goja.Value)
|
|
if !ok {
|
|
c.ctx.handleTypeError("onSelect must be a function")
|
|
return
|
|
}
|
|
|
|
c.items.Set(id, &commandItem{
|
|
index: idx,
|
|
id: id,
|
|
label: label,
|
|
value: value,
|
|
filterType: filterType,
|
|
heading: heading,
|
|
renderFunc: renderFunc,
|
|
onSelectFunc: onSelectFunc,
|
|
})
|
|
}
|
|
|
|
// Convert the items to JSON
|
|
itemsJSON := make([]*CommandItemJSON, 0)
|
|
c.items.Range(func(key string, value *commandItem) bool {
|
|
itemsJSON = append(itemsJSON, value.ToJSON(c.ctx, c.componentManager, c.ctx.scheduler))
|
|
return true
|
|
})
|
|
// Store the converted items
|
|
c.renderedItems = itemsJSON
|
|
|
|
c.renderCommandPaletteScheduled()
|
|
})
|
|
|
|
_ = cmdObj.Set("refresh", func() {
|
|
// Convert the items to JSON
|
|
itemsJSON := make([]*CommandItemJSON, 0)
|
|
c.items.Range(func(key string, value *commandItem) bool {
|
|
itemsJSON = append(itemsJSON, value.ToJSON(c.ctx, c.componentManager, c.ctx.scheduler))
|
|
return true
|
|
})
|
|
|
|
c.renderedItems = itemsJSON
|
|
|
|
c.renderCommandPaletteScheduled()
|
|
})
|
|
|
|
_ = cmdObj.Set("setPlaceholder", func(placeholder string) {
|
|
c.placeholder = placeholder
|
|
c.renderCommandPaletteScheduled()
|
|
})
|
|
|
|
_ = cmdObj.Set("open", func() {
|
|
c.ctx.SendEventToClient(ServerCommandPaletteOpenEvent, ServerCommandPaletteOpenEventPayload{})
|
|
})
|
|
|
|
_ = cmdObj.Set("close", func() {
|
|
c.ctx.SendEventToClient(ServerCommandPaletteCloseEvent, ServerCommandPaletteCloseEventPayload{})
|
|
})
|
|
|
|
_ = cmdObj.Set("setInput", func(input string) {
|
|
c.ctx.SendEventToClient(ServerCommandPaletteSetInputEvent, ServerCommandPaletteSetInputEventPayload{
|
|
Value: input,
|
|
})
|
|
})
|
|
|
|
_ = cmdObj.Set("getInput", func() string {
|
|
c.ctx.SendEventToClient(ServerCommandPaletteGetInputEvent, ServerCommandPaletteGetInputEventPayload{})
|
|
|
|
eventListener := c.ctx.RegisterEventListener(ClientCommandPaletteInputEvent)
|
|
defer c.ctx.UnregisterEventListener(eventListener.ID)
|
|
|
|
timeout := time.After(1500 * time.Millisecond)
|
|
input := make(chan string)
|
|
|
|
eventListener.SetCallback(func(event *ClientPluginEvent) {
|
|
payload := ClientCommandPaletteInputEventPayload{}
|
|
if event.ParsePayloadAs(ClientCommandPaletteInputEvent, &payload) {
|
|
input <- payload.Value
|
|
}
|
|
})
|
|
|
|
// go func() {
|
|
// for event := range eventListener.Channel {
|
|
// if event.ParsePayloadAs(ClientCommandPaletteInputEvent, &payload) {
|
|
// input <- payload.Value
|
|
// }
|
|
// }
|
|
// }()
|
|
|
|
select {
|
|
case <-timeout:
|
|
return ""
|
|
case input := <-input:
|
|
return input
|
|
}
|
|
})
|
|
|
|
// jsOnOpen
|
|
//
|
|
// Example:
|
|
// commandPalette.onOpen(() => {
|
|
// console.log("command palette opened by the user")
|
|
// })
|
|
_ = cmdObj.Set("onOpen", func(call goja.FunctionCall) goja.Value {
|
|
if len(call.Arguments) < 1 {
|
|
c.ctx.handleTypeError("onOpen requires a callback function")
|
|
}
|
|
|
|
callback, ok := goja.AssertFunction(call.Argument(0))
|
|
if !ok {
|
|
c.ctx.handleTypeError("onOpen requires a callback function")
|
|
}
|
|
|
|
eventListener := c.ctx.RegisterEventListener(ClientCommandPaletteOpenedEvent)
|
|
|
|
eventListener.SetCallback(func(event *ClientPluginEvent) {
|
|
payload := ClientCommandPaletteOpenedEventPayload{}
|
|
if event.ParsePayloadAs(ClientCommandPaletteOpenedEvent, &payload) {
|
|
c.ctx.scheduler.ScheduleAsync(func() error {
|
|
_, err := callback(goja.Undefined(), c.ctx.vm.ToValue(map[string]interface{}{}))
|
|
return err
|
|
})
|
|
}
|
|
})
|
|
|
|
// go func() {
|
|
// for event := range eventListener.Channel {
|
|
// if event.ParsePayloadAs(ClientCommandPaletteOpenedEvent, &payload) {
|
|
// c.ctx.scheduler.ScheduleAsync(func() error {
|
|
// _, err := callback(goja.Undefined(), c.ctx.vm.ToValue(map[string]interface{}{}))
|
|
// if err != nil {
|
|
// c.ctx.logger.Error().Err(err).Msg("plugin: Error running command palette open callback")
|
|
// }
|
|
// return err
|
|
// })
|
|
// }
|
|
// }
|
|
// }()
|
|
return goja.Undefined()
|
|
})
|
|
|
|
// jsOnClose
|
|
//
|
|
// Example:
|
|
// commandPalette.onClose(() => {
|
|
// console.log("command palette closed by the user")
|
|
// })
|
|
_ = cmdObj.Set("onClose", func(call goja.FunctionCall) goja.Value {
|
|
if len(call.Arguments) < 1 {
|
|
c.ctx.handleTypeError("onClose requires a callback function")
|
|
}
|
|
|
|
callback, ok := goja.AssertFunction(call.Argument(0))
|
|
if !ok {
|
|
c.ctx.handleTypeError("onClose requires a callback function")
|
|
}
|
|
|
|
eventListener := c.ctx.RegisterEventListener(ClientCommandPaletteClosedEvent)
|
|
|
|
eventListener.SetCallback(func(event *ClientPluginEvent) {
|
|
payload := ClientCommandPaletteClosedEventPayload{}
|
|
if event.ParsePayloadAs(ClientCommandPaletteClosedEvent, &payload) {
|
|
c.ctx.scheduler.ScheduleAsync(func() error {
|
|
_, err := callback(goja.Undefined(), c.ctx.vm.ToValue(map[string]interface{}{}))
|
|
return err
|
|
})
|
|
}
|
|
})
|
|
|
|
// go func() {
|
|
// for event := range eventListener.Channel {
|
|
// if event.ParsePayloadAs(ClientCommandPaletteClosedEvent, &payload) {
|
|
// c.ctx.scheduler.ScheduleAsync(func() error {
|
|
// _, err := callback(goja.Undefined(), c.ctx.vm.ToValue(map[string]interface{}{}))
|
|
// if err != nil {
|
|
// c.ctx.logger.Error().Err(err).Msg("plugin: Error running command palette close callback")
|
|
// }
|
|
// return err
|
|
// })
|
|
// }
|
|
// }
|
|
// }()
|
|
return goja.Undefined()
|
|
})
|
|
|
|
eventListener := c.ctx.RegisterEventListener(ClientCommandPaletteItemSelectedEvent)
|
|
eventListener.SetCallback(func(event *ClientPluginEvent) {
|
|
payload := ClientCommandPaletteItemSelectedEventPayload{}
|
|
if event.ParsePayloadAs(ClientCommandPaletteItemSelectedEvent, &payload) {
|
|
c.ctx.scheduler.ScheduleAsync(func() error {
|
|
item, found := c.items.Get(payload.ItemID)
|
|
if found {
|
|
_ = item.onSelectFunc(goja.FunctionCall{})
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
})
|
|
// go func() {
|
|
// eventListener := c.ctx.RegisterEventListener(ClientCommandPaletteItemSelectedEvent)
|
|
// payload := ClientCommandPaletteItemSelectedEventPayload{}
|
|
|
|
// for event := range eventListener.Channel {
|
|
// if event.ParsePayloadAs(ClientCommandPaletteItemSelectedEvent, &payload) {
|
|
// item, found := c.items.Get(payload.ItemID)
|
|
// if found {
|
|
// c.ctx.scheduler.ScheduleAsync(func() error {
|
|
// _ = item.onSelectFunc(goja.FunctionCall{})
|
|
// return nil
|
|
// })
|
|
// }
|
|
// }
|
|
// }
|
|
// }()
|
|
|
|
// Register components
|
|
_ = cmdObj.Set("div", c.componentManager.jsDiv)
|
|
_ = cmdObj.Set("flex", c.componentManager.jsFlex)
|
|
_ = cmdObj.Set("stack", c.componentManager.jsStack)
|
|
_ = cmdObj.Set("text", c.componentManager.jsText)
|
|
_ = cmdObj.Set("button", c.componentManager.jsButton)
|
|
_ = cmdObj.Set("anchor", c.componentManager.jsAnchor)
|
|
|
|
return cmdObj
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
func (c *commandItem) ToJSON(ctx *Context, componentManager *ComponentManager, scheduler *goja_util.Scheduler) *CommandItemJSON {
|
|
|
|
var components interface{}
|
|
if c.renderFunc != nil {
|
|
var err error
|
|
components, err = componentManager.renderComponents(c.renderFunc)
|
|
if err != nil {
|
|
ctx.logger.Error().Err(err).Msg("plugin: Failed to render command palette item")
|
|
ctx.handleException(err)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Reset the last rendered components, we don't care about diffing
|
|
componentManager.lastRenderedComponents = nil
|
|
|
|
return &CommandItemJSON{
|
|
Index: c.index,
|
|
ID: c.id,
|
|
Label: c.label,
|
|
Value: c.value,
|
|
FilterType: c.filterType,
|
|
Heading: c.heading,
|
|
Components: components,
|
|
}
|
|
}
|
|
|
|
func (c *CommandPaletteManager) renderCommandPaletteScheduled() {
|
|
c.updateMutex.Lock()
|
|
defer c.updateMutex.Unlock()
|
|
|
|
if !c.registered {
|
|
return
|
|
}
|
|
|
|
slices.SortFunc(c.renderedItems, func(a, b *CommandItemJSON) int {
|
|
return a.Index - b.Index
|
|
})
|
|
|
|
c.ctx.SendEventToClient(ServerCommandPaletteUpdatedEvent, ServerCommandPaletteUpdatedEventPayload{
|
|
Placeholder: c.placeholder,
|
|
Items: c.renderedItems,
|
|
})
|
|
}
|