Message dialogs, macos improvements.

This commit is contained in:
Nuno Cruces 2020-01-05 03:21:39 +00:00
parent 339101c9f4
commit 9b63531d6a
14 changed files with 600 additions and 146 deletions

2
.gitignore vendored
View file

@ -13,3 +13,5 @@
# Dependency directories (remove the comment below to include it) # Dependency directories (remove the comment below to include it)
# vendor/ # vendor/
osa_gen_darwin.go

50
file.go
View file

@ -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
}

View file

@ -1,24 +1,17 @@
package zenity package zenity
import ( import (
"bytes"
"html/template"
"io"
"os/exec"
"strings" "strings"
) )
func SelectFile(options ...FileOption) (string, error) { func SelectFile(options ...Option) (string, error) {
opts := fileoptsParse(options) opts := optsParse(options)
out, err := osaRun("file", osaFile{
cmd := exec.Command("osascript", "-l", "JavaScript")
cmd.Stdin = scriptExpand(scriptData{
Operation: "chooseFile", Operation: "chooseFile",
Prompt: opts.title, Prompt: opts.title,
Location: opts.filename, Location: opts.filename,
Type: appleFilters(opts.filters), Type: appleFilters(opts.filters),
}) })
out, err := cmd.Output()
if err != nil { if err != nil {
return "", err return "", err
} }
@ -28,18 +21,15 @@ func SelectFile(options ...FileOption) (string, error) {
return string(out), nil return string(out), nil
} }
func SelectFileMutiple(options ...FileOption) ([]string, error) { func SelectFileMutiple(options ...Option) ([]string, error) {
opts := fileoptsParse(options) opts := optsParse(options)
out, err := osaRun("file", osaFile{
cmd := exec.Command("osascript", "-l", "JavaScript")
cmd.Stdin = scriptExpand(scriptData{
Operation: "chooseFile", Operation: "chooseFile",
Multiple: true, Multiple: true,
Prompt: opts.title, Prompt: opts.title,
Location: opts.filename, Location: opts.filename,
Type: appleFilters(opts.filters), Type: appleFilters(opts.filters),
}) })
out, err := cmd.Output()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -52,16 +42,13 @@ func SelectFileMutiple(options ...FileOption) ([]string, error) {
return strings.Split(string(out), "\x00"), nil return strings.Split(string(out), "\x00"), nil
} }
func SelectFileSave(options ...FileOption) (string, error) { func SelectFileSave(options ...Option) (string, error) {
opts := fileoptsParse(options) opts := optsParse(options)
out, err := osaRun("file", osaFile{
cmd := exec.Command("osascript", "-l", "JavaScript")
cmd.Stdin = scriptExpand(scriptData{
Operation: "chooseFileName", Operation: "chooseFileName",
Prompt: opts.title, Prompt: opts.title,
Location: opts.filename, Location: opts.filename,
}) })
out, err := cmd.Output()
if err != nil { if err != nil {
return "", err return "", err
} }
@ -71,16 +58,13 @@ func SelectFileSave(options ...FileOption) (string, error) {
return string(out), nil return string(out), nil
} }
func SelectDirectory(options ...FileOption) (string, error) { func SelectDirectory(options ...Option) (string, error) {
opts := fileoptsParse(options) opts := optsParse(options)
out, err := osaRun("file", osaFile{
cmd := exec.Command("osascript", "-l", "JavaScript")
cmd.Stdin = scriptExpand(scriptData{
Operation: "chooseFolder", Operation: "chooseFolder",
Prompt: opts.title, Prompt: opts.title,
Location: opts.filename, Location: opts.filename,
}) })
out, err := cmd.Output()
if err != nil { if err != nil {
return "", err return "", err
} }
@ -100,53 +84,10 @@ func appleFilters(filters []FileFilter) []string {
return filter return filter
} }
type scriptData struct { type osaFile struct {
Operation string Operation string
Prompt string Prompt string
Location string Location string
Type []string Type []string
Multiple bool 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("<script>") : len(slice)-len("</script>")])
}
var script = template.Must(template.New("").Parse(`<script>
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;
}
</script>`))

View file

@ -5,15 +5,15 @@ import (
"strings" "strings"
) )
func SelectFile(options ...FileOption) (string, error) { func SelectFile(options ...Option) (string, error) {
opts := fileoptsParse(options) opts := optsParse(options)
args := []string{"--file-selection"} args := []string{"--file-selection"}
if opts.title != "" { if opts.title != "" {
args = append(args, "--title="+opts.title) args = append(args, "--title", opts.title)
} }
if opts.filename != "" { if opts.filename != "" {
args = append(args, "--filename="+opts.filename) args = append(args, "--filename", opts.filename)
} }
args = append(args, zenityFilters(opts.filters)...) args = append(args, zenityFilters(opts.filters)...)
cmd := exec.Command("zenity", args...) cmd := exec.Command("zenity", args...)
@ -30,15 +30,15 @@ func SelectFile(options ...FileOption) (string, error) {
return string(out), nil return string(out), nil
} }
func SelectFileMutiple(options ...FileOption) ([]string, error) { func SelectFileMutiple(options ...Option) ([]string, error) {
opts := fileoptsParse(options) opts := optsParse(options)
args := []string{"--file-selection", "--multiple", "--separator=\x1e"} args := []string{"--file-selection", "--multiple", "--separator=\x1e"}
if opts.title != "" { if opts.title != "" {
args = append(args, "--title="+opts.title) args = append(args, "--title", opts.title)
} }
if opts.filename != "" { if opts.filename != "" {
args = append(args, "--filename="+opts.filename) args = append(args, "--filename", opts.filename)
} }
args = append(args, zenityFilters(opts.filters)...) args = append(args, zenityFilters(opts.filters)...)
cmd := exec.Command("zenity", args...) cmd := exec.Command("zenity", args...)
@ -55,15 +55,15 @@ func SelectFileMutiple(options ...FileOption) ([]string, error) {
return strings.Split(string(out), "\x1e"), nil return strings.Split(string(out), "\x1e"), nil
} }
func SelectFileSave(options ...FileOption) (string, error) { func SelectFileSave(options ...Option) (string, error) {
opts := fileoptsParse(options) opts := optsParse(options)
args := []string{"--file-selection", "--save"} args := []string{"--file-selection", "--save"}
if opts.title != "" { if opts.title != "" {
args = append(args, "--title="+opts.title) args = append(args, "--title", opts.title)
} }
if opts.filename != "" { if opts.filename != "" {
args = append(args, "--filename="+opts.filename) args = append(args, "--filename", opts.filename)
} }
if opts.overwrite { if opts.overwrite {
args = append(args, "--confirm-overwrite") args = append(args, "--confirm-overwrite")
@ -83,15 +83,15 @@ func SelectFileSave(options ...FileOption) (string, error) {
return string(out), nil return string(out), nil
} }
func SelectDirectory(options ...FileOption) (string, error) { func SelectDirectory(options ...Option) (string, error) {
opts := fileoptsParse(options) opts := optsParse(options)
args := []string{"--file-selection", "--directory"} args := []string{"--file-selection", "--directory"}
if opts.title != "" { if opts.title != "" {
args = append(args, "--title="+opts.title) args = append(args, "--title", opts.title)
} }
if opts.filename != "" { if opts.filename != "" {
args = append(args, "--filename="+opts.filename) args = append(args, "--filename", opts.filename)
} }
cmd := exec.Command("zenity", args...) cmd := exec.Command("zenity", args...)
out, err := cmd.Output() out, err := cmd.Output()

View file

@ -27,12 +27,12 @@ var (
shCreateItemFromParsingName = shell32.NewProc("SHCreateItemFromParsingName") shCreateItemFromParsingName = shell32.NewProc("SHCreateItemFromParsingName")
) )
func SelectFile(options ...FileOption) (string, error) { func SelectFile(options ...Option) (string, error) {
var args _OPENFILENAME var args _OPENFILENAME
args.StructSize = uint32(unsafe.Sizeof(args)) args.StructSize = uint32(unsafe.Sizeof(args))
args.Flags = 0x80008 // OFN_NOCHANGEDIR|OFN_EXPLORER args.Flags = 0x80008 // OFN_NOCHANGEDIR|OFN_EXPLORER
opts := fileoptsParse(options) opts := optsParse(options)
if opts.title != "" { if opts.title != "" {
args.Title = syscall.StringToUTF16Ptr(opts.title) args.Title = syscall.StringToUTF16Ptr(opts.title)
} }
@ -55,12 +55,12 @@ func SelectFile(options ...FileOption) (string, error) {
return syscall.UTF16ToString(res[:]), nil return syscall.UTF16ToString(res[:]), nil
} }
func SelectFileMutiple(options ...FileOption) ([]string, error) { func SelectFileMutiple(options ...Option) ([]string, error) {
var args _OPENFILENAME var args _OPENFILENAME
args.StructSize = uint32(unsafe.Sizeof(args)) args.StructSize = uint32(unsafe.Sizeof(args))
args.Flags = 0x80208 // OFN_NOCHANGEDIR|OFN_ALLOWMULTISELECT|OFN_EXPLORER args.Flags = 0x80208 // OFN_NOCHANGEDIR|OFN_ALLOWMULTISELECT|OFN_EXPLORER
opts := fileoptsParse(options) opts := optsParse(options)
if opts.title != "" { if opts.title != "" {
args.Title = syscall.StringToUTF16Ptr(opts.title) args.Title = syscall.StringToUTF16Ptr(opts.title)
} }
@ -108,12 +108,12 @@ func SelectFileMutiple(options ...FileOption) ([]string, error) {
return split, nil return split, nil
} }
func SelectFileSave(options ...FileOption) (string, error) { func SelectFileSave(options ...Option) (string, error) {
var args _OPENFILENAME var args _OPENFILENAME
args.StructSize = uint32(unsafe.Sizeof(args)) args.StructSize = uint32(unsafe.Sizeof(args))
args.Flags = 0x80008 // OFN_NOCHANGEDIR|OFN_EXPLORER args.Flags = 0x80008 // OFN_NOCHANGEDIR|OFN_EXPLORER
opts := fileoptsParse(options) opts := optsParse(options)
if opts.title != "" { if opts.title != "" {
args.Title = syscall.StringToUTF16Ptr(opts.title) args.Title = syscall.StringToUTF16Ptr(opts.title)
} }
@ -139,7 +139,7 @@ func SelectFileSave(options ...FileOption) (string, error) {
return syscall.UTF16ToString(res[:]), nil return syscall.UTF16ToString(res[:]), nil
} }
func SelectDirectory(options ...FileOption) (string, error) { func SelectDirectory(options ...Option) (string, error) {
runtime.LockOSThread() runtime.LockOSThread()
defer runtime.UnlockOSThread() defer runtime.UnlockOSThread()
@ -151,7 +151,7 @@ func SelectDirectory(options ...FileOption) (string, error) {
defer coUninitialize.Call() defer coUninitialize.Call()
} }
opts := fileoptsParse(options) opts := optsParse(options)
var dialog *_IFileOpenDialog var dialog *_IFileOpenDialog
hr, _, _ = coCreateInstance.Call( hr, _, _ = coCreateInstance.Call(

119
msg_darwin.go Normal file
View file

@ -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
}

66
msg_linux.go Normal file
View file

@ -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
}

43
msg_test.go Normal file
View file

@ -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)
}
}

65
msg_windows.go Normal file
View file

@ -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
}
}

22
osa_darwin.go Normal file
View file

@ -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("<script>") : len(res)-len("</script>")])
return cmd.Output()
}

28
osa_scripts/file.gots Normal file
View file

@ -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
}

76
osa_scripts/generate.go Normal file
View file

@ -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(`"}}<script>`)
func() {
in, err := os.Open(filepath.Join(dir, name))
if err != nil {
log.Fatal(err)
}
defer in.Close()
scanner := bufio.NewScanner(in)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line != "" {
str.WriteString(line)
str.WriteRune('\n')
}
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
}()
str.WriteString("</script>{{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(` + "`{{.}}`" + `))
`))

33
osa_scripts/msg.gots Normal file
View file

@ -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 -}}

109
zenity.go Normal file
View file

@ -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
}