gui/pkg/dialog/service.go
Snider 32a8e4a44f
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run
Align GUI bridge with backend APIs
2026-04-15 21:20:49 +01:00

289 lines
8.7 KiB
Go

// pkg/dialog/service.go
package dialog
import (
"context"
"fmt"
core "dappco.re/go/core"
coreerr "dappco.re/go/core/log"
"forge.lthn.ai/core/gui/pkg/webview"
"forge.lthn.ai/core/gui/pkg/window"
)
type Options struct{}
type Service struct {
*core.ServiceRuntime[Options]
platform Platform
}
// Register(p) binds the dialog service to a Core instance.
//
// c.WithService(dialog.Register(wailsDialog))
func Register(p Platform) func(*core.Core) core.Result {
return func(c *core.Core) core.Result {
return core.Result{Value: &Service{
ServiceRuntime: core.NewServiceRuntime[Options](c, Options{}),
platform: p,
}, OK: true}
}
}
func (s *Service) OnStartup(_ context.Context) core.Result {
openFile := func(_ context.Context, opts core.Options) core.Result {
openOpts, err := openFileOptionsFrom(opts)
if err != nil {
return core.Result{Value: err, OK: false}
}
paths, err := s.platform.OpenFile(openOpts)
return core.Result{}.New(paths, err)
}
saveFile := func(_ context.Context, opts core.Options) core.Result {
saveOpts, err := saveFileOptionsFrom(opts)
if err != nil {
return core.Result{Value: err, OK: false}
}
path, err := s.platform.SaveFile(saveOpts)
return core.Result{}.New(path, err)
}
openDirectory := func(_ context.Context, opts core.Options) core.Result {
openOpts, err := openDirectoryOptionsFrom(opts)
if err != nil {
return core.Result{Value: err, OK: false}
}
path, err := s.platform.OpenDirectory(openOpts)
return core.Result{}.New(path, err)
}
messageDialog := func(_ context.Context, opts core.Options) core.Result {
messageOpts, err := messageDialogOptionsFrom(opts)
if err != nil {
return core.Result{Value: err, OK: false}
}
button, err := s.platform.MessageDialog(messageOpts)
return core.Result{}.New(button, err)
}
info := func(_ context.Context, opts core.Options) core.Result {
t, _ := opts.Get("task").Value.(TaskInfo)
button, err := s.platform.MessageDialog(MessageDialogOptions{
Type: DialogInfo, Title: t.Title, Message: t.Message, Buttons: t.Buttons,
})
return core.Result{}.New(button, err)
}
question := func(_ context.Context, opts core.Options) core.Result {
questionOpts, err := questionDialogOptionsFrom(opts)
if err != nil {
return core.Result{Value: err, OK: false}
}
button, err := s.platform.MessageDialog(questionOpts)
return core.Result{}.New(button, err)
}
warning := func(_ context.Context, opts core.Options) core.Result {
t, _ := opts.Get("task").Value.(TaskWarning)
button, err := s.platform.MessageDialog(MessageDialogOptions{
Type: DialogWarning, Title: t.Title, Message: t.Message, Buttons: t.Buttons,
})
return core.Result{}.New(button, err)
}
errDialog := func(_ context.Context, opts core.Options) core.Result {
t, _ := opts.Get("task").Value.(TaskError)
button, err := s.platform.MessageDialog(MessageDialogOptions{
Type: DialogError, Title: t.Title, Message: t.Message, Buttons: t.Buttons,
})
return core.Result{}.New(button, err)
}
prompt := func(_ context.Context, opts core.Options) core.Result {
promptOpts, err := promptOptionsFrom(opts)
if err != nil {
return core.Result{Value: err, OK: false}
}
windowName, err := s.promptWindowName()
if err != nil {
return core.Result{Value: err, OK: false}
}
script := promptScript(promptOpts.Title, promptOpts.Message, promptOpts.DefaultValue)
task := core.NewOptions(core.Option{Key: "task", Value: webview.TaskEvaluate{Window: windowName, Script: script}})
result := s.Core().Action("webview.evaluate").Run(context.Background(), task)
if !result.OK {
// Keep the legacy GUI alias as a fallback for older startup wiring.
result = s.Core().Action("gui.webview.eval").Run(context.Background(), task)
}
if !result.OK {
if e, ok := result.Value.(error); ok {
return core.Result{Value: e, OK: false}
}
return core.Result{OK: false}
}
switch value := result.Value.(type) {
case nil:
return core.Result{Value: PromptResult{Confirmed: false}, OK: true}
case string:
return core.Result{Value: PromptResult{Value: value, Confirmed: true}, OK: true}
default:
return core.Result{Value: PromptResult{Value: fmt.Sprint(value), Confirmed: true}, OK: true}
}
}
s.Core().Action("dialog.openFile", openFile)
s.Core().Action("gui.dialog.open", openFile)
s.Core().Action("dialog.saveFile", saveFile)
s.Core().Action("gui.dialog.save", saveFile)
s.Core().Action("dialog.openDirectory", openDirectory)
s.Core().Action("gui.dialog.openDirectory", openDirectory)
s.Core().Action("dialog.message", messageDialog)
s.Core().Action("gui.dialog.message", messageDialog)
s.Core().Action("dialog.info", info)
s.Core().Action("dialog.question", question)
s.Core().Action("gui.dialog.confirm", question)
s.Core().Action("dialog.warning", warning)
s.Core().Action("dialog.error", errDialog)
s.Core().Action("dialog.prompt", prompt)
s.Core().Action("gui.dialog.prompt", prompt)
return core.Result{OK: true}
}
func (s *Service) HandleIPCEvents(_ *core.Core, _ core.Message) core.Result {
return core.Result{OK: true}
}
func openFileOptionsFrom(opts core.Options) (OpenFileOptions, error) {
if task := opts.Get("task"); task.OK {
switch v := task.Value.(type) {
case TaskOpenFile:
return v.Options, nil
case TaskOpenFileWithOptions:
if v.Options != nil {
return *v.Options, nil
}
case OpenFileOptions:
return v, nil
}
}
return decodeOptions[OpenFileOptions](opts)
}
func saveFileOptionsFrom(opts core.Options) (SaveFileOptions, error) {
if task := opts.Get("task"); task.OK {
switch v := task.Value.(type) {
case TaskSaveFile:
return v.Options, nil
case TaskSaveFileWithOptions:
if v.Options != nil {
return *v.Options, nil
}
case SaveFileOptions:
return v, nil
}
}
return decodeOptions[SaveFileOptions](opts)
}
func openDirectoryOptionsFrom(opts core.Options) (OpenDirectoryOptions, error) {
if task := opts.Get("task"); task.OK {
switch v := task.Value.(type) {
case TaskOpenDirectory:
return v.Options, nil
case OpenDirectoryOptions:
return v, nil
}
}
return decodeOptions[OpenDirectoryOptions](opts)
}
func messageDialogOptionsFrom(opts core.Options) (MessageDialogOptions, error) {
if task := opts.Get("task"); task.OK {
switch v := task.Value.(type) {
case TaskMessageDialog:
return v.Options, nil
case MessageDialogOptions:
return v, nil
}
}
return decodeOptions[MessageDialogOptions](opts)
}
func questionDialogOptionsFrom(opts core.Options) (MessageDialogOptions, error) {
if task := opts.Get("task"); task.OK {
switch v := task.Value.(type) {
case TaskQuestion:
return MessageDialogOptions{
Type: DialogQuestion,
Title: v.Title,
Message: v.Message,
Buttons: v.Buttons,
}, nil
case MessageDialogOptions:
return v, nil
}
}
if direct, err := decodeOptions[TaskQuestion](opts); err == nil && (direct.Title != "" || direct.Message != "" || len(direct.Buttons) > 0) {
return MessageDialogOptions{
Type: DialogQuestion,
Title: direct.Title,
Message: direct.Message,
Buttons: direct.Buttons,
}, nil
}
return decodeOptions[MessageDialogOptions](opts)
}
func promptOptionsFrom(opts core.Options) (TaskPrompt, error) {
if task := opts.Get("task"); task.OK {
if v, ok := task.Value.(TaskPrompt); ok {
return v, nil
}
}
return decodeOptions[TaskPrompt](opts)
}
func decodeOptions[T any](opts core.Options) (T, error) {
var input T
items := make(map[string]any, opts.Len())
for _, item := range opts.Items() {
items[item.Key] = item.Value
}
if len(items) == 0 {
return input, nil
}
result := core.JSONUnmarshalString(core.JSONMarshalString(items), &input)
if !result.OK {
if err, ok := result.Value.(error); ok {
return input, err
}
return input, coreerr.E("dialog.decodeOptions", "failed to decode dialog options", nil)
}
return input, nil
}
func (s *Service) promptWindowName() (string, error) {
r := s.Core().QUERY(window.QueryWindowList{})
if !r.OK {
return "", coreerr.E("dialog.promptWindowName", "window service unavailable", nil)
}
windows, ok := r.Value.([]window.WindowInfo)
if !ok {
return "", coreerr.E("dialog.promptWindowName", "unexpected window list result type", nil)
}
for _, info := range windows {
if info.Focused {
return info.Name, nil
}
}
if len(windows) > 0 {
return windows[0].Name, nil
}
return "", coreerr.E("dialog.promptWindowName", "no application window available for prompt", nil)
}
func promptScript(title, message, defaultValue string) string {
promptText := title
if message != "" {
if promptText != "" {
promptText += "\n\n"
}
promptText += message
}
return core.Sprintf(`(() => {
const value = window.prompt(%s, %s);
return value === null ? null : value;
})()`, core.JSONMarshalString(promptText), core.JSONMarshalString(defaultValue))
}