Add RFC-facing GUI route aliases

This commit is contained in:
Snider 2026-04-15 16:50:14 +01:00
parent b496b909dc
commit ae842df5bc
6 changed files with 264 additions and 80 deletions

View file

@ -14,18 +14,22 @@ type Service struct {
}
func (s *Service) OnStartup(_ context.Context) core.Result {
s.Core().Action("browser.openURL", func(_ context.Context, opts core.Options) core.Result {
openURL := func(_ context.Context, opts core.Options) core.Result {
if err := s.platform.OpenURL(opts.String("url")); err != nil {
return core.Result{Value: err, OK: false}
}
return core.Result{OK: true}
})
s.Core().Action("browser.openFile", func(_ context.Context, opts core.Options) core.Result {
}
openFile := func(_ context.Context, opts core.Options) core.Result {
if err := s.platform.OpenFile(opts.String("path")); err != nil {
return core.Result{Value: err, OK: false}
}
return core.Result{OK: true}
})
}
s.Core().Action("browser.openURL", openURL)
s.Core().Action("gui.browser.open", openURL)
s.Core().Action("browser.openFile", openFile)
s.Core().Action("gui.browser.openFile", openFile)
return core.Result{OK: true}
}

View file

@ -27,11 +27,11 @@ func Register(p Platform) func(*core.Core) core.Result {
func (s *Service) OnStartup(_ context.Context) core.Result {
s.Core().RegisterQuery(s.handleQuery)
s.Core().Action("clipboard.setText", func(_ context.Context, opts core.Options) core.Result {
setText := func(_ context.Context, opts core.Options) core.Result {
success := s.platform.SetText(opts.String("text"))
return core.Result{Value: success, OK: true}
})
s.Core().Action("clipboard.setImage", func(_ context.Context, opts core.Options) core.Result {
}
setImage := func(_ context.Context, opts core.Options) core.Result {
imgPlatform, ok := s.platform.(ImagePlatform)
if !ok {
return core.Result{Value: false, OK: true}
@ -39,14 +39,24 @@ func (s *Service) OnStartup(_ context.Context) core.Result {
data, _ := opts.Get("data").Value.([]byte)
success := imgPlatform.SetImage(data)
return core.Result{Value: success, OK: true}
})
s.Core().Action("clipboard.clear", func(_ context.Context, _ core.Options) core.Result {
}
clear := func(_ context.Context, _ core.Options) core.Result {
success := s.platform.SetText("")
if imgPlatform, ok := s.platform.(ImagePlatform); ok {
success = imgPlatform.SetImage(nil) && success
}
return core.Result{Value: success, OK: true}
})
}
read := func(_ context.Context, _ core.Options) core.Result {
text, ok := s.platform.Text()
return core.Result{Value: ClipboardContent{Text: text, HasContent: ok && text != ""}, OK: true}
}
s.Core().Action("clipboard.setText", setText)
s.Core().Action("gui.clipboard.write", setText)
s.Core().Action("clipboard.setImage", setImage)
s.Core().Action("clipboard.clear", clear)
s.Core().Action("gui.clipboard.clear", clear)
s.Core().Action("gui.clipboard.read", read)
return core.Result{OK: true}
}

View file

@ -31,78 +31,78 @@ func Register(p Platform) func(*core.Core) core.Result {
}
func (s *Service) OnStartup(_ context.Context) core.Result {
s.Core().Action("dialog.openFile", func(_ context.Context, opts core.Options) core.Result {
var openOpts OpenFileOptions
switch v := opts.Get("task").Value.(type) {
case TaskOpenFile:
openOpts = v.Options
case TaskOpenFileWithOptions:
if v.Options != nil {
openOpts = *v.Options
}
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)
})
s.Core().Action("dialog.saveFile", func(_ context.Context, opts core.Options) core.Result {
var saveOpts SaveFileOptions
switch v := opts.Get("task").Value.(type) {
case TaskSaveFile:
saveOpts = v.Options
case TaskSaveFileWithOptions:
if v.Options != nil {
saveOpts = *v.Options
}
}
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)
})
s.Core().Action("dialog.openDirectory", func(_ context.Context, opts core.Options) core.Result {
t, _ := opts.Get("task").Value.(TaskOpenDirectory)
path, err := s.platform.OpenDirectory(t.Options)
}
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)
})
s.Core().Action("dialog.message", func(_ context.Context, opts core.Options) core.Result {
t, _ := opts.Get("task").Value.(TaskMessageDialog)
button, err := s.platform.MessageDialog(t.Options)
}
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)
})
s.Core().Action("dialog.info", func(_ context.Context, opts core.Options) core.Result {
}
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)
})
s.Core().Action("dialog.question", func(_ context.Context, opts core.Options) core.Result {
t, _ := opts.Get("task").Value.(TaskQuestion)
button, err := s.platform.MessageDialog(MessageDialogOptions{
Type: DialogQuestion, Title: t.Title, Message: t.Message, Buttons: t.Buttons,
})
}
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)
})
s.Core().Action("dialog.warning", func(_ context.Context, opts core.Options) core.Result {
}
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)
})
s.Core().Action("dialog.error", func(_ context.Context, opts core.Options) core.Result {
}
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)
})
s.Core().Action("dialog.prompt", func(_ context.Context, opts core.Options) core.Result {
t, _ := opts.Get("task").Value.(TaskPrompt)
}
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(t.Title, t.Message, t.DefaultValue)
result := s.Core().Action("webview.evaluate").Run(context.Background(), core.NewOptions(
script := promptScript(promptOpts.Title, promptOpts.Message, promptOpts.DefaultValue)
result := s.Core().Action("gui.webview.eval").Run(context.Background(), core.NewOptions(
core.Option{Key: "task", Value: webview.TaskEvaluate{Window: windowName, Script: script}},
))
if !result.OK {
@ -119,7 +119,21 @@ func (s *Service) OnStartup(_ context.Context) core.Result {
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("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}
}
@ -127,6 +141,115 @@ 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 {

View file

@ -309,18 +309,23 @@ func (s *Service) injectElectronShim() string {
};
const shell = {
openExternal(url) {
return invokeBridge('browser.openURL', { url }).then(() => undefined);
return invokeBridge('gui.browser.open', { url }).then(() => undefined);
},
openPath(path) {
return invokeBridge('browser.openFile', { path }).then(() => "");
return invokeBridge('gui.browser.openFile', { path }).then(() => "");
}
};
const clipboard = {
readText() {
return globalThis.navigator?.clipboard?.readText?.() ?? Promise.resolve("");
return invokeBridge('gui.clipboard.read', {}).then((value) => {
if (typeof value === "string") {
return value;
}
return value?.text ?? value?.Text ?? "";
});
},
writeText(text) {
return invokeBridge('clipboard.setText', { text }).then(() => undefined);
return invokeBridge('gui.clipboard.write', { text }).then(() => undefined);
}
};
const invokeBridge = (route, payload) => (globalThis.__coreBridge?.invoke?.(route, payload) ?? Promise.resolve({ route, payload }));

View file

@ -8,6 +8,7 @@ import (
"time"
core "dappco.re/go/core"
coreerr "dappco.re/go/core/log"
"forge.lthn.ai/core/gui/pkg/dialog"
)
@ -34,10 +35,13 @@ func Register(p Platform) func(*core.Core) core.Result {
func (s *Service) OnStartup(_ context.Context) core.Result {
s.Core().RegisterQuery(s.handleQuery)
s.Core().Action("notification.send", func(_ context.Context, opts core.Options) core.Result {
t, _ := opts.Get("task").Value.(TaskSend)
return core.Result{Value: nil, OK: true}.New(s.send(t.Options))
})
send := func(_ context.Context, opts core.Options) core.Result {
options, err := notificationOptionsFrom(opts)
if err != nil {
return core.Result{Value: err, OK: false}
}
return core.Result{Value: nil, OK: true}.New(s.send(options))
}
s.Core().Action("notification.requestPermission", func(_ context.Context, _ core.Options) core.Result {
granted, err := s.platform.RequestPermission()
return core.Result{}.New(granted, err)
@ -54,6 +58,8 @@ func (s *Service) OnStartup(_ context.Context) core.Result {
t, _ := opts.Get("task").Value.(TaskClear)
return core.Result{Value: nil, OK: true}.New(s.clear(t.ID))
})
s.Core().Action("notification.send", send)
s.Core().Action("gui.notification.send", send)
return core.Result{OK: true}
}
@ -162,3 +168,34 @@ func (s *Service) removeActive(id string) []string {
clear(s.active)
return ids
}
func notificationOptionsFrom(opts core.Options) (NotificationOptions, error) {
if task := opts.Get("task"); task.OK {
switch v := task.Value.(type) {
case TaskSend:
return v.Options, nil
case NotificationOptions:
return v, nil
}
}
return decodeOptions[NotificationOptions](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("notification.decodeOptions", "failed to decode notification options", nil)
}
return input, nil
}

View file

@ -301,7 +301,12 @@ func (s *Service) handleQuery(_ *core.Core, q core.Query) core.Result {
// registerTaskActions registers all webview task handlers as named Core actions.
func (s *Service) registerTaskActions() {
c := s.Core()
c.Action("webview.evaluate", func(_ context.Context, opts core.Options) core.Result {
register := func(names []string, handler func(context.Context, core.Options) core.Result) {
for _, name := range names {
c.Action(name, handler)
}
}
register([]string{"webview.evaluate", "gui.webview.eval"}, func(_ context.Context, opts core.Options) core.Result {
t, _ := opts.Get("task").Value.(TaskEvaluate)
conn, err := s.getConn(t.Window)
if err != nil {
@ -310,7 +315,7 @@ func (s *Service) registerTaskActions() {
result, err := conn.Evaluate(t.Script)
return core.Result{}.New(result, err)
})
c.Action("webview.click", func(_ context.Context, opts core.Options) core.Result {
register([]string{"webview.click", "gui.webview.click"}, func(_ context.Context, opts core.Options) core.Result {
t, _ := opts.Get("task").Value.(TaskClick)
conn, err := s.getConn(t.Window)
if err != nil {
@ -318,7 +323,7 @@ func (s *Service) registerTaskActions() {
}
return core.Result{Value: nil, OK: true}.New(conn.Click(t.Selector))
})
c.Action("webview.type", func(_ context.Context, opts core.Options) core.Result {
register([]string{"webview.type", "gui.webview.type"}, func(_ context.Context, opts core.Options) core.Result {
t, _ := opts.Get("task").Value.(TaskType)
conn, err := s.getConn(t.Window)
if err != nil {
@ -326,7 +331,7 @@ func (s *Service) registerTaskActions() {
}
return core.Result{Value: nil, OK: true}.New(conn.Type(t.Selector, t.Text))
})
c.Action("webview.navigate", func(_ context.Context, opts core.Options) core.Result {
register([]string{"webview.navigate", "gui.webview.navigate"}, func(_ context.Context, opts core.Options) core.Result {
t, _ := opts.Get("task").Value.(TaskNavigate)
conn, err := s.getConn(t.Window)
if err != nil {
@ -334,7 +339,7 @@ func (s *Service) registerTaskActions() {
}
return core.Result{Value: nil, OK: true}.New(conn.Navigate(t.URL))
})
c.Action("webview.screenshot", func(_ context.Context, opts core.Options) core.Result {
register([]string{"webview.screenshot", "gui.webview.screenshot"}, func(_ context.Context, opts core.Options) core.Result {
t, _ := opts.Get("task").Value.(TaskScreenshot)
conn, err := s.getConn(t.Window)
if err != nil {
@ -349,7 +354,7 @@ func (s *Service) registerTaskActions() {
MimeType: "image/png",
}, OK: true}
})
c.Action("webview.scroll", func(_ context.Context, opts core.Options) core.Result {
register([]string{"webview.scroll", "gui.webview.scroll"}, func(_ context.Context, opts core.Options) core.Result {
t, _ := opts.Get("task").Value.(TaskScroll)
conn, err := s.getConn(t.Window)
if err != nil {
@ -358,7 +363,7 @@ func (s *Service) registerTaskActions() {
_, err = conn.Evaluate("window.scrollTo(" + strconv.Itoa(t.X) + "," + strconv.Itoa(t.Y) + ")")
return core.Result{Value: nil, OK: true}.New(err)
})
c.Action("webview.hover", func(_ context.Context, opts core.Options) core.Result {
register([]string{"webview.hover", "gui.webview.hover"}, func(_ context.Context, opts core.Options) core.Result {
t, _ := opts.Get("task").Value.(TaskHover)
conn, err := s.getConn(t.Window)
if err != nil {
@ -366,7 +371,7 @@ func (s *Service) registerTaskActions() {
}
return core.Result{Value: nil, OK: true}.New(conn.Hover(t.Selector))
})
c.Action("webview.select", func(_ context.Context, opts core.Options) core.Result {
register([]string{"webview.select", "gui.webview.select"}, func(_ context.Context, opts core.Options) core.Result {
t, _ := opts.Get("task").Value.(TaskSelect)
conn, err := s.getConn(t.Window)
if err != nil {
@ -374,7 +379,7 @@ func (s *Service) registerTaskActions() {
}
return core.Result{Value: nil, OK: true}.New(conn.Select(t.Selector, t.Value))
})
c.Action("webview.check", func(_ context.Context, opts core.Options) core.Result {
register([]string{"webview.check", "gui.webview.check"}, func(_ context.Context, opts core.Options) core.Result {
t, _ := opts.Get("task").Value.(TaskCheck)
conn, err := s.getConn(t.Window)
if err != nil {
@ -382,7 +387,7 @@ func (s *Service) registerTaskActions() {
}
return core.Result{Value: nil, OK: true}.New(conn.Check(t.Selector, t.Checked))
})
c.Action("webview.uploadFile", func(_ context.Context, opts core.Options) core.Result {
register([]string{"webview.uploadFile", "gui.webview.uploadFile"}, func(_ context.Context, opts core.Options) core.Result {
t, _ := opts.Get("task").Value.(TaskUploadFile)
conn, err := s.getConn(t.Window)
if err != nil {
@ -390,7 +395,7 @@ func (s *Service) registerTaskActions() {
}
return core.Result{Value: nil, OK: true}.New(conn.UploadFile(t.Selector, t.Paths))
})
c.Action("webview.setViewport", func(_ context.Context, opts core.Options) core.Result {
register([]string{"webview.setViewport", "gui.webview.setViewport"}, func(_ context.Context, opts core.Options) core.Result {
t, _ := opts.Get("task").Value.(TaskSetViewport)
conn, err := s.getConn(t.Window)
if err != nil {
@ -398,7 +403,7 @@ func (s *Service) registerTaskActions() {
}
return core.Result{Value: nil, OK: true}.New(conn.SetViewport(t.Width, t.Height))
})
c.Action("webview.clearConsole", func(_ context.Context, opts core.Options) core.Result {
register([]string{"webview.clearConsole", "gui.webview.clearConsole"}, func(_ context.Context, opts core.Options) core.Result {
t, _ := opts.Get("task").Value.(TaskClearConsole)
conn, err := s.getConn(t.Window)
if err != nil {
@ -407,7 +412,7 @@ func (s *Service) registerTaskActions() {
conn.ClearConsole()
return core.Result{OK: true}
})
c.Action("webview.setURL", func(_ context.Context, opts core.Options) core.Result {
register([]string{"webview.setURL", "gui.webview.setURL"}, func(_ context.Context, opts core.Options) core.Result {
t, _ := opts.Get("task").Value.(TaskSetURL)
conn, err := s.getConn(t.Window)
if err != nil {
@ -415,7 +420,7 @@ func (s *Service) registerTaskActions() {
}
return core.Result{Value: nil, OK: true}.New(conn.Navigate(t.URL))
})
c.Action("webview.setZoom", func(_ context.Context, opts core.Options) core.Result {
register([]string{"webview.setZoom", "gui.webview.setZoom"}, func(_ context.Context, opts core.Options) core.Result {
t, _ := opts.Get("task").Value.(TaskSetZoom)
conn, err := s.getConn(t.Window)
if err != nil {
@ -423,7 +428,7 @@ func (s *Service) registerTaskActions() {
}
return core.Result{Value: nil, OK: true}.New(conn.SetZoom(t.Zoom))
})
c.Action("webview.print", func(_ context.Context, opts core.Options) core.Result {
register([]string{"webview.print", "gui.webview.print"}, func(_ context.Context, opts core.Options) core.Result {
t, _ := opts.Get("task").Value.(TaskPrint)
conn, err := s.getConn(t.Window)
if err != nil {
@ -441,11 +446,11 @@ func (s *Service) registerTaskActions() {
MimeType: "application/pdf",
}, OK: true}
})
c.Action("webview.devtoolsOpen", func(_ context.Context, opts core.Options) core.Result {
register([]string{"webview.devtoolsOpen", "gui.webview.devtoolsOpen"}, func(_ context.Context, opts core.Options) core.Result {
t, _ := opts.Get("task").Value.(TaskDevToolsOpen)
return core.Result{Value: nil, OK: true}.New(s.devToolsOpen(t.Window))
})
c.Action("webview.devtoolsClose", func(_ context.Context, opts core.Options) core.Result {
register([]string{"webview.devtoolsClose", "gui.webview.devtoolsClose"}, func(_ context.Context, opts core.Options) core.Result {
t, _ := opts.Get("task").Value.(TaskDevToolsClose)
return core.Result{Value: nil, OK: true}.New(s.devToolsClose(t.Window))
})