package main import ( "context" "fmt" "log" "sort" "strings" "github.com/wailsapp/wails/v2/pkg/runtime" "lmika.dev/pkg/modash/moslice" "ucl.lmika.dev/ucl" "ucl.lmika.dev/ucl/builtins" ) // App struct type App struct { store *Store uclInst *ucl.Inst plugins *PluginFilters ctx context.Context } // NewApp creates a new App application struct func NewApp(store *Store) *App { plugins := newPluginFilters() uclInst := ucl.New( ucl.WithModule(plugins.Module()), ucl.WithModule(builtins.CSV(nil)), ucl.WithModule(builtins.Fns()), ucl.WithModule(builtins.FS(nil)), ucl.WithModule(builtins.Itrs()), ucl.WithModule(builtins.Lists()), ucl.WithModule(builtins.Log(nil)), ucl.WithModule(builtins.OS()), ucl.WithModule(builtins.Strs()), ucl.WithModule(builtins.Time()), ) return &App{ store: store, uclInst: uclInst, plugins: plugins, } } // startup is called when the app starts. The context is saved // so we can call the runtime methods func (a *App) startup(ctx context.Context) { a.ctx = context.WithValue(ctx, uclInstKey, a.uclInst) dir, err := pluginDir() if err != nil { log.Printf("cannot determine plugin directory: %v", err) return } a.plugins.LoadDir(a.ctx, a.uclInst, dir) } // domReady is called once the frontend has loaded. Reports any plugin load errors in the status bar. func (a *App) domReady(ctx context.Context) { errs := a.plugins.Errors() if len(errs) == 0 { return } runtime.EventsEmit(a.ctx, "set-statusbar-message", SetStatusbarMessage{ Message: fmt.Sprintf("%d plugin file(s) failed to load: %v", len(errs), errs[0]), Error: true, }) } // lookupProcessor finds a text processor by action key, preferring plugin-defined filters. func (a *App) lookupProcessor(name string) (TextProcessor, bool) { if proc, ok := a.plugins.filters[name]; ok { return proc, true } proc, ok := TextFilters[name] return proc, ok } func (a *App) LoadCurrentBuffer() (string, error) { return a.store.LoadBuffer() } func (a *App) SaveCurrentBuffer(buffer string) error { return a.store.SaveBuffer(buffer) } func (a *App) ListProcessors() (resp []ListProcessorsResponse) { pluginLabels := make(map[string]struct{}, len(a.plugins.filters)) for k, v := range a.plugins.filters { pluginLabels[v.Label] = struct{}{} resp = append(resp, ListProcessorsResponse{Name: k, Label: v.Label}) } for k, v := range TextFilters { if _, shadowed := pluginLabels[v.Label]; shadowed { continue } resp = append(resp, ListProcessorsResponse{Name: k, Label: v.Label}) } sort.Slice(resp, func(i, j int) bool { return resp[i].Label < resp[j].Label }) return resp } func (a *App) TriggerTextProcess(action string) { runtime.EventsEmit(a.ctx, "request-text-process", RequestTextProcess{ Action: action, }) } func (a *App) PromptUser(label string, resp func(string)) { runtime.EventsOnce(a.ctx, "prompt-response", func(data ...interface{}) { if len(data) == 0 { return } ans, ok := data[0].(string) if !ok { return } resp(ans) }) runtime.EventsEmit(a.ctx, "prompt-request", PromptRequest{ Label: label, }) } func (a *App) ProcessText(req ProcessTextRequest) { filter, ok := a.lookupProcessor(req.Action) if !ok { log.Printf("Unknown filter: [%s]", req.Action) return } applyFilter := func(filter TextFilter) { resp, err := moslice.MapWithError(req.Input, func(span TextSpan) (TextSpan, error) { outRes, err := filter(a.ctx, span.Text) if err != nil { return TextSpan{}, err } return TextSpan{ Text: outRes.Output, Pos: span.Pos, Len: span.Len, Append: outRes.Append, }, nil }) if err != nil { runtime.EventsEmit(a.ctx, "set-statusbar-message", SetStatusbarMessage{ Message: fmt.Sprintf("Error running filter: %v", err.Error()), Error: true, }) return } runtime.EventsEmit(a.ctx, "process-text-response", ProcessTextResponse{ Output: resp, }) } switch { case filter.Analyze != nil: inBfr := strings.Builder{} for _, span := range req.Input { if inBfr.Len() > 0 { inBfr.WriteString("\n") } inBfr.WriteString(span.Text) } msg, err := filter.Analyze(a.ctx, inBfr.String()) if err != nil { runtime.EventsEmit(a.ctx, "set-statusbar-message", SetStatusbarMessage{ Message: fmt.Sprintf("Error running analysis: %v", err.Error()), Error: true, }) return } runtime.EventsEmit(a.ctx, "set-statusbar-message", SetStatusbarMessage{ Message: msg, }) case filter.Filter != nil: applyFilter(filter.Filter) case filter.FilterWithArg != nil: a.PromptUser(filter.FilterWithArg.Label, func(ans string) { filter, err := filter.FilterWithArg.OnConfirm(a.ctx, ans) if err != nil { runtime.EventsEmit(a.ctx, "set-statusbar-message", SetStatusbarMessage{ Message: fmt.Sprintf("Error running filter: %v", err.Error()), Error: true, }) return } applyFilter(filter) }) } }