diff --git a/dialog/dialog_darwin.go b/dialog/dialog_darwin.go
new file mode 100644
index 0000000..9a2780e
--- /dev/null
+++ b/dialog/dialog_darwin.go
@@ -0,0 +1,138 @@
+package dialog
+
+import (
+ "bytes"
+ "html/template"
+ "io"
+ "os/exec"
+ "strings"
+)
+
+func OpenFile(title, defaultPath string, filters []FileFilter) (string, error) {
+ cmd := exec.Command("osascript", "-l", "JavaScript")
+ cmd.Stdin = scriptExpand(scriptData{
+ Operation: "chooseFile",
+ Title: title,
+ DefaultPath: defaultPath,
+ Filter: toFilter(filters),
+ })
+ out, err := cmd.Output()
+ if err != nil {
+ return "", err
+ }
+ if len(out) > 0 {
+ out = out[:len(out)-1]
+ }
+ return string(out), nil
+}
+
+func OpenFiles(title, defaultPath string, filters []FileFilter) ([]string, error) {
+ cmd := exec.Command("osascript", "-l", "JavaScript")
+ cmd.Stdin = scriptExpand(scriptData{
+ Operation: "chooseFile",
+ Multiple: true,
+ Title: title,
+ DefaultPath: defaultPath,
+ Filter: toFilter(filters),
+ })
+ out, err := cmd.Output()
+ if err != nil {
+ return nil, err
+ }
+ if len(out) > 0 {
+ out = out[:len(out)-1]
+ }
+ return strings.Split(string(out), "\x00"), nil
+}
+
+func SaveFile(title, defaultPath string, filters []FileFilter) (string, error) {
+ cmd := exec.Command("osascript", "-l", "JavaScript")
+ cmd.Stdin = scriptExpand(scriptData{
+ Operation: "chooseFileName",
+ Title: title,
+ DefaultPath: defaultPath,
+ })
+ out, err := cmd.Output()
+ if err != nil {
+ return "", err
+ }
+ if len(out) > 0 {
+ out = out[:len(out)-1]
+ }
+ return string(out), nil
+}
+
+func PickFolder(title, defaultPath string) (string, error) {
+ cmd := exec.Command("osascript", "-l", "JavaScript")
+ cmd.Stdin = scriptExpand(scriptData{
+ Operation: "chooseFolder",
+ Title: title,
+ DefaultPath: defaultPath,
+ })
+ out, err := cmd.Output()
+ if err != nil {
+ return "", err
+ }
+ if len(out) > 0 {
+ out = out[:len(out)-1]
+ }
+ return string(out), nil
+}
+
+type FileFilter struct {
+ Name string
+ Exts []string
+}
+
+func toFilter(filters []FileFilter) []string {
+ var filter []string
+ for _, f := range filters {
+ for _, e := range f.Exts {
+ filter = append(filter, strings.TrimPrefix(e, "."))
+ }
+ }
+ return filter
+}
+
+type scriptData struct {
+ Operation string
+ Title string
+ DefaultPath string
+ Filter []string
+ Multiple bool
+}
+
+func scriptExpand(data scriptData) io.Reader {
+ var buf bytes.Buffer
+
+ err := script.Execute(&buf, data)
+ if err != nil {
+ panic(err)
+ }
+
+ var slice = buf.Bytes()
+ return bytes.NewReader(slice[len("")])
+}
+
+var script = template.Must(template.New("").Parse(``))
diff --git a/dialog/dialog_test.go b/dialog/dialog_test.go
new file mode 100644
index 0000000..211fdaa
--- /dev/null
+++ b/dialog/dialog_test.go
@@ -0,0 +1,55 @@
+package dialog
+
+import "testing"
+
+func TestOpenFile(t *testing.T) {
+ ret, err := OpenFile("", "", []FileFilter{
+ {"Go files", []string{".go"}},
+ {"Web files", []string{".html", ".js", ".css"}},
+ {"Image files", []string{".png", ".ico"}},
+ })
+
+ if err != nil {
+ t.Error(err)
+ } else {
+ t.Logf("%#v", ret)
+ }
+}
+
+func TestOpenFiles(t *testing.T) {
+ ret, err := OpenFiles("", "", []FileFilter{
+ {"Go files", []string{".go"}},
+ {"Web files", []string{".html", ".js", ".css"}},
+ {"Image files", []string{".png", ".ico"}},
+ })
+
+ if err != nil {
+ t.Error(err)
+ } else {
+ t.Logf("%#v", ret)
+ }
+}
+
+func TestSaveFile(t *testing.T) {
+ ret, err := SaveFile("", "", []FileFilter{
+ {"Go files", []string{".go"}},
+ {"Web files", []string{".html", ".js", ".css"}},
+ {"Image files", []string{".png", ".ico"}},
+ })
+
+ if err != nil {
+ t.Error(err)
+ } else {
+ t.Logf("%#v", ret)
+ }
+}
+
+func TestPickFolder(t *testing.T) {
+ ret, err := PickFolder("", "")
+
+ if err != nil {
+ t.Error(err)
+ } else {
+ t.Logf("%#v", ret)
+ }
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..ed74fb0
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module github.com/ncruces/go-ui
+
+go 1.13