From 9b63531d6aa7083d8bf6c35712c96ce64ea6e1bd Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Sun, 5 Jan 2020 03:21:39 +0000 Subject: [PATCH] Message dialogs, macos improvements. --- .gitignore | 2 + file.go | 50 ----------------- file_darwin.go | 85 +++++----------------------- file_linux.go | 32 +++++------ file_windows.go | 16 +++--- msg_darwin.go | 119 ++++++++++++++++++++++++++++++++++++++++ msg_linux.go | 66 ++++++++++++++++++++++ msg_test.go | 43 +++++++++++++++ msg_windows.go | 65 ++++++++++++++++++++++ osa_darwin.go | 22 ++++++++ osa_scripts/file.gots | 28 ++++++++++ osa_scripts/generate.go | 76 +++++++++++++++++++++++++ osa_scripts/msg.gots | 33 +++++++++++ zenity.go | 109 ++++++++++++++++++++++++++++++++++++ 14 files changed, 600 insertions(+), 146 deletions(-) delete mode 100644 file.go create mode 100644 msg_darwin.go create mode 100644 msg_linux.go create mode 100644 msg_test.go create mode 100644 msg_windows.go create mode 100644 osa_darwin.go create mode 100644 osa_scripts/file.gots create mode 100644 osa_scripts/generate.go create mode 100644 osa_scripts/msg.gots create mode 100644 zenity.go diff --git a/.gitignore b/.gitignore index 66fd13c..8311b1d 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ # Dependency directories (remove the comment below to include it) # vendor/ + +osa_gen_darwin.go \ No newline at end of file diff --git a/file.go b/file.go deleted file mode 100644 index 9b23b7d..0000000 --- a/file.go +++ /dev/null @@ -1,50 +0,0 @@ -package zenity - -type opts struct { - title string -} - -type Option func(*opts) - -func (o *opts) Title(title string) { - o.title = title -} - -type fileopts struct { - opts - filename string - overwrite bool - filters []FileFilter -} - -type FileOption func(*fileopts) - -func Filename(filename string) FileOption { - return func(o *fileopts) { - o.filename = filename - } -} - -func ConfirmOverwrite(o *fileopts) { - o.overwrite = true -} - -type FileFilter struct { - Name string - Exts []string -} - -type FileFilters []FileFilter - -func (f FileFilters) New() FileOption { - return func(o *fileopts) { - o.filters = f - } -} - -func fileoptsParse(options []FileOption) (res fileopts) { - for _, o := range options { - o(&res) - } - return -} diff --git a/file_darwin.go b/file_darwin.go index 3551757..e46ad9e 100644 --- a/file_darwin.go +++ b/file_darwin.go @@ -1,24 +1,17 @@ package zenity import ( - "bytes" - "html/template" - "io" - "os/exec" "strings" ) -func SelectFile(options ...FileOption) (string, error) { - opts := fileoptsParse(options) - - cmd := exec.Command("osascript", "-l", "JavaScript") - cmd.Stdin = scriptExpand(scriptData{ +func SelectFile(options ...Option) (string, error) { + opts := optsParse(options) + out, err := osaRun("file", osaFile{ Operation: "chooseFile", Prompt: opts.title, Location: opts.filename, Type: appleFilters(opts.filters), }) - out, err := cmd.Output() if err != nil { return "", err } @@ -28,18 +21,15 @@ func SelectFile(options ...FileOption) (string, error) { return string(out), nil } -func SelectFileMutiple(options ...FileOption) ([]string, error) { - opts := fileoptsParse(options) - - cmd := exec.Command("osascript", "-l", "JavaScript") - cmd.Stdin = scriptExpand(scriptData{ +func SelectFileMutiple(options ...Option) ([]string, error) { + opts := optsParse(options) + out, err := osaRun("file", osaFile{ Operation: "chooseFile", Multiple: true, Prompt: opts.title, Location: opts.filename, Type: appleFilters(opts.filters), }) - out, err := cmd.Output() if err != nil { return nil, err } @@ -52,16 +42,13 @@ func SelectFileMutiple(options ...FileOption) ([]string, error) { return strings.Split(string(out), "\x00"), nil } -func SelectFileSave(options ...FileOption) (string, error) { - opts := fileoptsParse(options) - - cmd := exec.Command("osascript", "-l", "JavaScript") - cmd.Stdin = scriptExpand(scriptData{ +func SelectFileSave(options ...Option) (string, error) { + opts := optsParse(options) + out, err := osaRun("file", osaFile{ Operation: "chooseFileName", Prompt: opts.title, Location: opts.filename, }) - out, err := cmd.Output() if err != nil { return "", err } @@ -71,16 +58,13 @@ func SelectFileSave(options ...FileOption) (string, error) { return string(out), nil } -func SelectDirectory(options ...FileOption) (string, error) { - opts := fileoptsParse(options) - - cmd := exec.Command("osascript", "-l", "JavaScript") - cmd.Stdin = scriptExpand(scriptData{ +func SelectDirectory(options ...Option) (string, error) { + opts := optsParse(options) + out, err := osaRun("file", osaFile{ Operation: "chooseFolder", Prompt: opts.title, Location: opts.filename, }) - out, err := cmd.Output() if err != nil { return "", err } @@ -100,53 +84,10 @@ func appleFilters(filters []FileFilter) []string { return filter } -type scriptData struct { +type osaFile struct { Operation string Prompt string Location string Type []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/file_linux.go b/file_linux.go index 3d204f4..a6a410d 100644 --- a/file_linux.go +++ b/file_linux.go @@ -5,15 +5,15 @@ import ( "strings" ) -func SelectFile(options ...FileOption) (string, error) { - opts := fileoptsParse(options) +func SelectFile(options ...Option) (string, error) { + opts := optsParse(options) args := []string{"--file-selection"} if opts.title != "" { - args = append(args, "--title="+opts.title) + args = append(args, "--title", opts.title) } if opts.filename != "" { - args = append(args, "--filename="+opts.filename) + args = append(args, "--filename", opts.filename) } args = append(args, zenityFilters(opts.filters)...) cmd := exec.Command("zenity", args...) @@ -30,15 +30,15 @@ func SelectFile(options ...FileOption) (string, error) { return string(out), nil } -func SelectFileMutiple(options ...FileOption) ([]string, error) { - opts := fileoptsParse(options) +func SelectFileMutiple(options ...Option) ([]string, error) { + opts := optsParse(options) args := []string{"--file-selection", "--multiple", "--separator=\x1e"} if opts.title != "" { - args = append(args, "--title="+opts.title) + args = append(args, "--title", opts.title) } if opts.filename != "" { - args = append(args, "--filename="+opts.filename) + args = append(args, "--filename", opts.filename) } args = append(args, zenityFilters(opts.filters)...) cmd := exec.Command("zenity", args...) @@ -55,15 +55,15 @@ func SelectFileMutiple(options ...FileOption) ([]string, error) { return strings.Split(string(out), "\x1e"), nil } -func SelectFileSave(options ...FileOption) (string, error) { - opts := fileoptsParse(options) +func SelectFileSave(options ...Option) (string, error) { + opts := optsParse(options) args := []string{"--file-selection", "--save"} if opts.title != "" { - args = append(args, "--title="+opts.title) + args = append(args, "--title", opts.title) } if opts.filename != "" { - args = append(args, "--filename="+opts.filename) + args = append(args, "--filename", opts.filename) } if opts.overwrite { args = append(args, "--confirm-overwrite") @@ -83,15 +83,15 @@ func SelectFileSave(options ...FileOption) (string, error) { return string(out), nil } -func SelectDirectory(options ...FileOption) (string, error) { - opts := fileoptsParse(options) +func SelectDirectory(options ...Option) (string, error) { + opts := optsParse(options) args := []string{"--file-selection", "--directory"} if opts.title != "" { - args = append(args, "--title="+opts.title) + args = append(args, "--title", opts.title) } if opts.filename != "" { - args = append(args, "--filename="+opts.filename) + args = append(args, "--filename", opts.filename) } cmd := exec.Command("zenity", args...) out, err := cmd.Output() diff --git a/file_windows.go b/file_windows.go index 664b2c0..1c75dcd 100644 --- a/file_windows.go +++ b/file_windows.go @@ -27,12 +27,12 @@ var ( shCreateItemFromParsingName = shell32.NewProc("SHCreateItemFromParsingName") ) -func SelectFile(options ...FileOption) (string, error) { +func SelectFile(options ...Option) (string, error) { var args _OPENFILENAME args.StructSize = uint32(unsafe.Sizeof(args)) args.Flags = 0x80008 // OFN_NOCHANGEDIR|OFN_EXPLORER - opts := fileoptsParse(options) + opts := optsParse(options) if opts.title != "" { args.Title = syscall.StringToUTF16Ptr(opts.title) } @@ -55,12 +55,12 @@ func SelectFile(options ...FileOption) (string, error) { return syscall.UTF16ToString(res[:]), nil } -func SelectFileMutiple(options ...FileOption) ([]string, error) { +func SelectFileMutiple(options ...Option) ([]string, error) { var args _OPENFILENAME args.StructSize = uint32(unsafe.Sizeof(args)) args.Flags = 0x80208 // OFN_NOCHANGEDIR|OFN_ALLOWMULTISELECT|OFN_EXPLORER - opts := fileoptsParse(options) + opts := optsParse(options) if opts.title != "" { args.Title = syscall.StringToUTF16Ptr(opts.title) } @@ -108,12 +108,12 @@ func SelectFileMutiple(options ...FileOption) ([]string, error) { return split, nil } -func SelectFileSave(options ...FileOption) (string, error) { +func SelectFileSave(options ...Option) (string, error) { var args _OPENFILENAME args.StructSize = uint32(unsafe.Sizeof(args)) args.Flags = 0x80008 // OFN_NOCHANGEDIR|OFN_EXPLORER - opts := fileoptsParse(options) + opts := optsParse(options) if opts.title != "" { args.Title = syscall.StringToUTF16Ptr(opts.title) } @@ -139,7 +139,7 @@ func SelectFileSave(options ...FileOption) (string, error) { return syscall.UTF16ToString(res[:]), nil } -func SelectDirectory(options ...FileOption) (string, error) { +func SelectDirectory(options ...Option) (string, error) { runtime.LockOSThread() defer runtime.UnlockOSThread() @@ -151,7 +151,7 @@ func SelectDirectory(options ...FileOption) (string, error) { defer coUninitialize.Call() } - opts := fileoptsParse(options) + opts := optsParse(options) var dialog *_IFileOpenDialog hr, _, _ = coCreateInstance.Call( diff --git a/msg_darwin.go b/msg_darwin.go new file mode 100644 index 0000000..cfb945a --- /dev/null +++ b/msg_darwin.go @@ -0,0 +1,119 @@ +package zenity + +import ( + "os/exec" +) + +func Error(text string, options ...Option) (bool, error) { + return message(0, text, options) +} + +func Info(text string, options ...Option) (bool, error) { + return message(1, text, options) +} + +func Question(text string, options ...Option) (bool, error) { + return message(2, text, options) +} + +func Warning(text string, options ...Option) (bool, error) { + return message(3, text, options) +} + +func message(dialog int, text string, options []Option) (bool, error) { + opts := optsParse(options) + + data := osaMsg{ + Text: text, + Title: opts.title, + Dialog: opts.icon != 0 || dialog == 2, + } + + if data.Dialog { + switch opts.icon { + case ErrorIcon: + data.Icon = "stop" + case InfoIcon, QuestionIcon: + data.Icon = "note" + case WarningIcon: + data.Icon = "caution" + } + } else { + switch dialog { + case 0: + data.As = "critical" + case 1: + data.As = "informational" + case 3: + data.As = "warning" + } + + if opts.title != "" { + data.Text = opts.title + data.Message = text + } + } + + if dialog != 2 { + opts.cancel = "" + if data.Dialog { + opts.ok = "OK" + } + } + if opts.ok != "" || opts.cancel != "" || opts.extra != "" || true { + if opts.ok == "" { + opts.ok = "OK" + } + if opts.cancel == "" { + opts.cancel = "Cancel" + } + if dialog == 2 { + if opts.extra == "" { + data.Buttons = []string{opts.cancel, opts.ok} + data.Default = 2 + data.Cancel = 1 + } else { + data.Buttons = []string{opts.extra, opts.cancel, opts.ok} + data.Default = 3 + data.Cancel = 2 + } + } else { + if opts.extra == "" { + data.Buttons = []string{opts.ok} + data.Default = 1 + } else { + data.Buttons = []string{opts.extra, opts.ok} + data.Default = 2 + } + } + } + if opts.defcancel { + if data.Cancel != 0 { + data.Default = data.Cancel + } + if data.Dialog && data.Buttons == nil { + data.Default = 1 + } + } + + _, err := osaRun("msg", data) + if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 { + return false, nil + } + if err != nil { + return false, err + } + return true, err +} + +type osaMsg struct { + Dialog bool + Text string + Message string + As string + Title string + Icon string + Buttons []string + Cancel int + Default int +} diff --git a/msg_linux.go b/msg_linux.go new file mode 100644 index 0000000..6911a74 --- /dev/null +++ b/msg_linux.go @@ -0,0 +1,66 @@ +package zenity + +import "os/exec" + +func Error(text string, options ...Option) (bool, error) { + return message("--error", text, options) +} + +func Info(text string, options ...Option) (bool, error) { + return message("--info", text, options) +} + +func Question(text string, options ...Option) (bool, error) { + return message("--question", text, options) +} + +func Warning(text string, options ...Option) (bool, error) { + return message("--warning", text, options) +} + +func message(arg, text string, options []Option) (bool, error) { + opts := optsParse(options) + + args := []string{arg, "--text", text, "--no-markup"} + if opts.title != "" { + args = append(args, "--title", opts.title) + } + if opts.ok != "" { + args = append(args, "--ok-label", opts.ok) + } + if opts.cancel != "" { + args = append(args, "--cancel-label", opts.cancel) + } + if opts.extra != "" { + args = append(args, "--extra-button", opts.extra) + } + if opts.nowrap { + args = append(args, "--no-wrap") + } + if opts.ellipsize { + args = append(args, "--ellipsize") + } + if opts.defcancel { + args = append(args, "--default-cancel") + } + switch opts.icon { + case ErrorIcon: + args = append(args, "--icon-name=dialog-error") + case InfoIcon: + args = append(args, "--icon-name=dialog-information") + case QuestionIcon: + args = append(args, "--icon-name=dialog-question") + case WarningIcon: + args = append(args, "--icon-name=dialog-warning") + } + + cmd := exec.Command("zenity", args...) + _, err := cmd.Output() + if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 { + return false, nil + } + if err != nil { + return false, err + } + return true, err +} diff --git a/msg_test.go b/msg_test.go new file mode 100644 index 0000000..67fd5b2 --- /dev/null +++ b/msg_test.go @@ -0,0 +1,43 @@ +package zenity + +import "testing" + +func TestError(t *testing.T) { + res, err := Error("An error has occured.", Title("Error"), Icon(ErrorIcon)) + + if err != nil { + t.Error(err) + } else { + t.Logf("%#v", res) + } +} + +func TestInfo(t *testing.T) { + res, err := Info("All updates are complete.", Title("Information"), Icon(InfoIcon)) + + if err != nil { + t.Error(err) + } else { + t.Logf("%#v", res) + } +} + +func TestWarning(t *testing.T) { + res, err := Warning("Are you sure you want to proceed?", Title("Warning"), Icon(WarningIcon)) + + if err != nil { + t.Error(err) + } else { + t.Logf("%#v", res) + } +} + +func TestQuestion(t *testing.T) { + res, err := Question("Are you sure you want to proceed?", Title("Question"), Icon(QuestionIcon)) + + if err != nil { + t.Error(err) + } else { + t.Logf("%#v", res) + } +} diff --git a/msg_windows.go b/msg_windows.go new file mode 100644 index 0000000..87e94ce --- /dev/null +++ b/msg_windows.go @@ -0,0 +1,65 @@ +package zenity + +import ( + "syscall" + "unsafe" +) + +var ( + user32 = syscall.NewLazyDLL("user32.dll") + messageBox = user32.NewProc("MessageBoxW") +) + +func Error(text string, options ...Option) (bool, error) { + return message(0, text, options) +} + +func Info(text string, options ...Option) (bool, error) { + return message(1, text, options) +} + +func Question(text string, options ...Option) (bool, error) { + return message(2, text, options) +} + +func Warning(text string, options ...Option) (bool, error) { + return message(3, text, options) +} + +func message(dialog int, text string, options []Option) (bool, error) { + opts := optsParse(options) + + var flags, caption uintptr + + switch { + case dialog == 2 && opts.extra != "": + flags |= 0x3 // MB_YESNOCANCEL + case dialog == 2 || opts.extra != "": + flags |= 0x1 // MB_OKCANCEL + } + + switch opts.icon { + case ErrorIcon: + flags |= 0x10 // MB_ICONERROR + case QuestionIcon: + flags |= 0x20 // MB_ICONQUESTION + case WarningIcon: + flags |= 0x30 // MB_ICONWARNING + case InfoIcon: + flags |= 0x40 // MB_ICONINFORMATION + } + + if opts.title != "" { + caption = uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(opts.title))) + } + + n, _, err := messageBox.Call(0, + uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(text))), + caption, flags) + + if n == 0 { + return false, err + } else { + return n == 1 /* IDOK */ || n == 6 /* IDYES */, nil + } +} diff --git a/osa_darwin.go b/osa_darwin.go new file mode 100644 index 0000000..4f5c793 --- /dev/null +++ b/osa_darwin.go @@ -0,0 +1,22 @@ +package zenity + +import ( + "os/exec" + "strings" +) + +//go:generate go run osa_scripts/generate.go osa_scripts/ + +func osaRun(script string, data interface{}) ([]byte, error) { + var buf strings.Builder + + err := osaScripts.ExecuteTemplate(&buf, script, data) + if err != nil { + return nil, err + } + + var res = buf.String() + cmd := exec.Command("osascript", "-l", "JavaScript") + cmd.Stdin = strings.NewReader(res[len("")]) + return cmd.Output() +} diff --git a/osa_scripts/file.gots b/osa_scripts/file.gots new file mode 100644 index 0000000..acaf673 --- /dev/null +++ b/osa_scripts/file.gots @@ -0,0 +1,28 @@ +var app = Application.currentApplication() +app.includeStandardAdditions = true +app.activate() + +var opts = {} +opts.withPrompt = {{.Prompt}} +opts.multipleSelectionsAllowed = {{.Multiple}} + +{{if .Location -}} + opts.defaultLocation = {{.Location}} +{{end -}} +{{if .Type -}} + opts.ofType = {{.Type}} +{{end -}} + +var res +try { + res = app[{{.Operation}}](opts) +} catch (e) { + if (e.errorNumber !== -128) throw e +} +if (Array.isArray(res)) { + res.join('\0') +} else if (res != null) { + res.toString() +} else { + void 0 +} \ No newline at end of file diff --git a/osa_scripts/generate.go b/osa_scripts/generate.go new file mode 100644 index 0000000..30c3049 --- /dev/null +++ b/osa_scripts/generate.go @@ -0,0 +1,76 @@ +package main + +import ( + "bufio" + "io/ioutil" + "log" + "os" + "path/filepath" + "strings" + "text/template" +) + +func main() { + dir := os.Args[1] + + files, err := ioutil.ReadDir(dir) + if err != nil { + log.Fatal(err) + } + + var str strings.Builder + + for _, file := range files { + if name := file.Name(); filepath.Ext(name) == ".gots" { + str.WriteString("\n" + `{{define "`) + str.WriteString(strings.TrimSuffix(name, ".gots")) + str.WriteString(`"}}{{end}}") + } + } + + out, err := os.Create("osa_gen_darwin.go") + if err != nil { + log.Fatal(err) + } + + err = generator.Execute(out, str.String()) + if err != nil { + log.Fatal(err) + } + + err = out.Close() + if err != nil { + log.Fatal(err) + } +} + +var generator = template.Must(template.New("").Parse(`// Code generated by zenity; DO NOT EDIT. + +package zenity + +import "html/template" + +var osaScripts = template.Must(template.New("").Parse(` + "`{{.}}`" + `)) +`)) diff --git a/osa_scripts/msg.gots b/osa_scripts/msg.gots new file mode 100644 index 0000000..ffea618 --- /dev/null +++ b/osa_scripts/msg.gots @@ -0,0 +1,33 @@ +var app = Application.currentApplication() +app.includeStandardAdditions = true +app.activate() + +var opts = {} + +{{if .Buttons -}} + opts.buttons = {{.Buttons}} +{{end -}} +{{if .Default -}} + opts.defaultButton = {{.Default}} +{{end -}} +{{if .Cancel -}} + opts.cancelButton = {{.Cancel}} +{{end -}} + +{{if .Dialog -}} + {{if .Title -}} + opts.withTitle = {{.Title}} + {{end -}} + {{if .Icon -}} + opts.withIcon = {{.Icon}} + {{end -}} + app.displayDialog({{.Text}}, opts) +{{else -}} + {{if .As -}} + opts.as = {{.As}} + {{end -}} + {{if .Message -}} + opts.message = {{.Message}} + {{end -}} + app.displayAlert({{.Text}}, opts) +{{end -}} \ No newline at end of file diff --git a/zenity.go b/zenity.go new file mode 100644 index 0000000..97a263e --- /dev/null +++ b/zenity.go @@ -0,0 +1,109 @@ +package zenity + +type options struct { + // General options + title string + + // File selection options + filename string + overwrite bool + filters []FileFilter + + // Message options + icon MessageIcon + ok string + cancel string + extra string + nowrap bool + ellipsize bool + defcancel bool +} + +type Option func(*options) + +func optsParse(options []Option) (res options) { + for _, o := range options { + o(&res) + } + return +} + +// General options + +func Title(title string) Option { + return func(o *options) { + o.title = title + } +} + +// File selection options + +func Filename(filename string) Option { + return func(o *options) { + o.filename = filename + } +} + +func ConfirmOverwrite(o *options) { + o.overwrite = true +} + +type FileFilter struct { + Name string + Exts []string +} + +type FileFilters []FileFilter + +func (f FileFilters) New() Option { + return func(o *options) { + o.filters = f + } +} + +// Message options + +type MessageIcon int + +const ( + ErrorIcon MessageIcon = iota + 1 + InfoIcon + QuestionIcon + WarningIcon +) + +func Icon(icon MessageIcon) Option { + return func(o *options) { + o.icon = icon + } +} + +func OKLabel(ok string) Option { + return func(o *options) { + o.ok = ok + } +} + +func CancelLabel(cancel string) Option { + return func(o *options) { + o.cancel = cancel + } +} + +func ExtraButton(extra string) Option { + return func(o *options) { + o.extra = extra + } +} + +func NoWrap(o *options) { + o.nowrap = true +} + +func Ellipsize(o *options) { + o.ellipsize = true +} + +func DefaultCancel(o *options) { + o.defcancel = true +}