diff --git a/file.go b/file.go index e6227e1..9b23b7d 100644 --- a/file.go +++ b/file.go @@ -1,6 +1,50 @@ 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 84b1113..3551757 100644 --- a/file_darwin.go +++ b/file_darwin.go @@ -8,13 +8,15 @@ import ( "strings" ) -func SelectFile(title, defaultPath string, filters []FileFilter) (string, error) { +func SelectFile(options ...FileOption) (string, error) { + opts := fileoptsParse(options) + cmd := exec.Command("osascript", "-l", "JavaScript") cmd.Stdin = scriptExpand(scriptData{ - Operation: "chooseFile", - Title: title, - DefaultPath: defaultPath, - Filter: appleFilters(filters), + Operation: "chooseFile", + Prompt: opts.title, + Location: opts.filename, + Type: appleFilters(opts.filters), }) out, err := cmd.Output() if err != nil { @@ -26,14 +28,16 @@ func SelectFile(title, defaultPath string, filters []FileFilter) (string, error) return string(out), nil } -func SelectFileMutiple(title, defaultPath string, filters []FileFilter) ([]string, error) { +func SelectFileMutiple(options ...FileOption) ([]string, error) { + opts := fileoptsParse(options) + cmd := exec.Command("osascript", "-l", "JavaScript") cmd.Stdin = scriptExpand(scriptData{ - Operation: "chooseFile", - Multiple: true, - Title: title, - DefaultPath: defaultPath, - Filter: appleFilters(filters), + Operation: "chooseFile", + Multiple: true, + Prompt: opts.title, + Location: opts.filename, + Type: appleFilters(opts.filters), }) out, err := cmd.Output() if err != nil { @@ -48,12 +52,14 @@ func SelectFileMutiple(title, defaultPath string, filters []FileFilter) ([]strin return strings.Split(string(out), "\x00"), nil } -func SelectFileSave(title, defaultPath string, confirmOverwrite bool, filters []FileFilter) (string, error) { +func SelectFileSave(options ...FileOption) (string, error) { + opts := fileoptsParse(options) + cmd := exec.Command("osascript", "-l", "JavaScript") cmd.Stdin = scriptExpand(scriptData{ - Operation: "chooseFileName", - Title: title, - DefaultPath: defaultPath, + Operation: "chooseFileName", + Prompt: opts.title, + Location: opts.filename, }) out, err := cmd.Output() if err != nil { @@ -65,12 +71,14 @@ func SelectFileSave(title, defaultPath string, confirmOverwrite bool, filters [] return string(out), nil } -func SelectDirectory(title, defaultPath string) (string, error) { +func SelectDirectory(options ...FileOption) (string, error) { + opts := fileoptsParse(options) + cmd := exec.Command("osascript", "-l", "JavaScript") cmd.Stdin = scriptExpand(scriptData{ - Operation: "chooseFolder", - Title: title, - DefaultPath: defaultPath, + Operation: "chooseFolder", + Prompt: opts.title, + Location: opts.filename, }) out, err := cmd.Output() if err != nil { @@ -93,11 +101,11 @@ func appleFilters(filters []FileFilter) []string { } type scriptData struct { - Operation string - Title string - DefaultPath string - Filter []string - Multiple bool + Operation string + Prompt string + Location string + Type []string + Multiple bool } func scriptExpand(data scriptData) io.Reader { @@ -118,14 +126,14 @@ app.includeStandardAdditions = true; app.activate(); var opts = {}; -opts.withPrompt = {{.Title}}; +opts.withPrompt = {{.Prompt}}; opts.multipleSelectionsAllowed = {{.Multiple}}; -{{if .DefaultPath}} - opts.defaultLocation = {{.DefaultPath}}; +{{if .Location}} + opts.defaultLocation = {{.Location}}; {{end}} -{{if .Filter}} - opts.ofType = {{.Filter}}; +{{if .Type}} + opts.ofType = {{.Type}}; {{end}} var res; diff --git a/file_linux.go b/file_linux.go index 123f7a2..3d204f4 100644 --- a/file_linux.go +++ b/file_linux.go @@ -5,15 +5,17 @@ import ( "strings" ) -func SelectFile(title, defaultPath string, filters []FileFilter) (string, error) { +func SelectFile(options ...FileOption) (string, error) { + opts := fileoptsParse(options) + args := []string{"--file-selection"} - if title != "" { - args = append(args, "--title="+title) + if opts.title != "" { + args = append(args, "--title="+opts.title) } - if defaultPath != "" { - args = append(args, "--filename="+defaultPath) + if opts.filename != "" { + args = append(args, "--filename="+opts.filename) } - args = append(args, zenityFilters(filters)...) + args = append(args, zenityFilters(opts.filters)...) cmd := exec.Command("zenity", args...) out, err := cmd.Output() if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 { @@ -28,15 +30,17 @@ func SelectFile(title, defaultPath string, filters []FileFilter) (string, error) return string(out), nil } -func SelectFileMutiple(title, defaultPath string, filters []FileFilter) ([]string, error) { +func SelectFileMutiple(options ...FileOption) ([]string, error) { + opts := fileoptsParse(options) + args := []string{"--file-selection", "--multiple", "--separator=\x1e"} - if title != "" { - args = append(args, "--title="+title) + if opts.title != "" { + args = append(args, "--title="+opts.title) } - if defaultPath != "" { - args = append(args, "--filename="+defaultPath) + if opts.filename != "" { + args = append(args, "--filename="+opts.filename) } - args = append(args, zenityFilters(filters)...) + args = append(args, zenityFilters(opts.filters)...) cmd := exec.Command("zenity", args...) out, err := cmd.Output() if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 { @@ -51,18 +55,20 @@ func SelectFileMutiple(title, defaultPath string, filters []FileFilter) ([]strin return strings.Split(string(out), "\x1e"), nil } -func SelectFileSave(title, defaultPath string, confirmOverwrite bool, filters []FileFilter) (string, error) { +func SelectFileSave(options ...FileOption) (string, error) { + opts := fileoptsParse(options) + args := []string{"--file-selection", "--save"} - if title != "" { - args = append(args, "--title="+title) + if opts.title != "" { + args = append(args, "--title="+opts.title) } - if defaultPath != "" { - args = append(args, "--filename="+defaultPath) + if opts.filename != "" { + args = append(args, "--filename="+opts.filename) } - if confirmOverwrite { + if opts.overwrite { args = append(args, "--confirm-overwrite") } - args = append(args, zenityFilters(filters)...) + args = append(args, zenityFilters(opts.filters)...) cmd := exec.Command("zenity", args...) out, err := cmd.Output() if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 { @@ -77,13 +83,15 @@ func SelectFileSave(title, defaultPath string, confirmOverwrite bool, filters [] return string(out), nil } -func SelectDirectory(title, defaultPath string) (string, error) { +func SelectDirectory(options ...FileOption) (string, error) { + opts := fileoptsParse(options) + args := []string{"--file-selection", "--directory"} - if title != "" { - args = append(args, "--title="+title) + if opts.title != "" { + args = append(args, "--title="+opts.title) } - if defaultPath != "" { - args = append(args, "--filename="+defaultPath) + if opts.filename != "" { + args = append(args, "--filename="+opts.filename) } cmd := exec.Command("zenity", args...) out, err := cmd.Output() diff --git a/file_test.go b/file_test.go index 42f2ae9..d5996b9 100644 --- a/file_test.go +++ b/file_test.go @@ -5,11 +5,11 @@ import "testing" const defaultPath = "" func TestSelectFile(t *testing.T) { - res, err := SelectFile("", defaultPath, []FileFilter{ + res, err := SelectFile(Filename(defaultPath), FileFilters{ {"Go files", []string{".go"}}, {"Web files", []string{".html", ".js", ".css"}}, {"Image files", []string{".png", ".gif", ".ico", ".jpg", ".webp"}}, - }) + }.New()) if err != nil { t.Error(err) @@ -19,11 +19,11 @@ func TestSelectFile(t *testing.T) { } func TestSelectFileMutiple(t *testing.T) { - res, err := SelectFileMutiple("", defaultPath, []FileFilter{ + res, err := SelectFileMutiple(Filename(defaultPath), FileFilters{ {"Go files", []string{".go"}}, {"Web files", []string{".html", ".js", ".css"}}, {"Image files", []string{".png", ".gif", ".ico", ".jpg", ".webp"}}, - }) + }.New()) if err != nil { t.Error(err) @@ -33,11 +33,11 @@ func TestSelectFileMutiple(t *testing.T) { } func TestSelectFileSave(t *testing.T) { - res, err := SelectFileSave("", defaultPath, true, []FileFilter{ + res, err := SelectFileSave(Filename(defaultPath), ConfirmOverwrite, FileFilters{ {"Go files", []string{".go"}}, {"Web files", []string{".html", ".js", ".css"}}, {"Image files", []string{".png", ".gif", ".ico", ".jpg", ".webp"}}, - }) + }.New()) if err != nil { t.Error(err) @@ -47,7 +47,7 @@ func TestSelectFileSave(t *testing.T) { } func TestSelectDirectory(t *testing.T) { - res, err := SelectDirectory("", defaultPath) + res, err := SelectDirectory(Filename(defaultPath)) if err != nil { t.Error(err) diff --git a/file_windows.go b/file_windows.go index ed5b99a..664b2c0 100644 --- a/file_windows.go +++ b/file_windows.go @@ -1,10 +1,10 @@ package zenity import ( - "errors" "fmt" "path/filepath" "reflect" + "runtime" "syscall" "unicode/utf16" "unsafe" @@ -27,60 +27,58 @@ var ( shCreateItemFromParsingName = shell32.NewProc("SHCreateItemFromParsingName") ) -func SelectFile(title, defaultPath string, filters []FileFilter) (string, error) { +func SelectFile(options ...FileOption) (string, error) { var args _OPENFILENAME args.StructSize = uint32(unsafe.Sizeof(args)) args.Flags = 0x80008 // OFN_NOCHANGEDIR|OFN_EXPLORER - if title != "" { - args.Title = syscall.StringToUTF16Ptr(title) + opts := fileoptsParse(options) + if opts.title != "" { + args.Title = syscall.StringToUTF16Ptr(opts.title) } - if defaultPath != "" { - args.InitialDir = syscall.StringToUTF16Ptr(defaultPath) + if opts.filename != "" { + args.InitialDir = syscall.StringToUTF16Ptr(opts.filename) } - args.Filter = &windowsFilters(filters)[0] + args.Filter = &windowsFilters(opts.filters)[0] res := [32768]uint16{} args.File = &res[0] args.MaxFile = uint32(len(res)) + runtime.LockOSThread() + defer runtime.UnlockOSThread() + n, _, _ := getOpenFileName.Call(uintptr(unsafe.Pointer(&args))) if n == 0 { - n, _, _ = commDlgExtendedError.Call() - if n == 0 { - return "", nil - } else { - return "", fmt.Errorf("Common Dialog error: %x", n) - } + return "", commDlgError() } return syscall.UTF16ToString(res[:]), nil } -func SelectFileMutiple(title, defaultPath string, filters []FileFilter) ([]string, error) { +func SelectFileMutiple(options ...FileOption) ([]string, error) { var args _OPENFILENAME args.StructSize = uint32(unsafe.Sizeof(args)) args.Flags = 0x80208 // OFN_NOCHANGEDIR|OFN_ALLOWMULTISELECT|OFN_EXPLORER - if title != "" { - args.Title = syscall.StringToUTF16Ptr(title) + opts := fileoptsParse(options) + if opts.title != "" { + args.Title = syscall.StringToUTF16Ptr(opts.title) } - if defaultPath != "" { - args.InitialDir = syscall.StringToUTF16Ptr(defaultPath) + if opts.filename != "" { + args.InitialDir = syscall.StringToUTF16Ptr(opts.filename) } - args.Filter = &windowsFilters(filters)[0] + args.Filter = &windowsFilters(opts.filters)[0] res := [32768 + 1024*256]uint16{} args.File = &res[0] args.MaxFile = uint32(len(res)) + runtime.LockOSThread() + defer runtime.UnlockOSThread() + n, _, _ := getOpenFileName.Call(uintptr(unsafe.Pointer(&args))) if n == 0 { - n, _, _ = commDlgExtendedError.Call() - if n == 0 { - return nil, nil - } else { - return nil, fmt.Errorf("Common Dialog error: %x", n) - } + return nil, commDlgError() } var i int @@ -110,72 +108,78 @@ func SelectFileMutiple(title, defaultPath string, filters []FileFilter) ([]strin return split, nil } -func SelectFileSave(title, defaultPath string, confirmOverwrite bool, filters []FileFilter) (string, error) { +func SelectFileSave(options ...FileOption) (string, error) { var args _OPENFILENAME args.StructSize = uint32(unsafe.Sizeof(args)) args.Flags = 0x80008 // OFN_NOCHANGEDIR|OFN_EXPLORER - if title != "" { - args.Title = syscall.StringToUTF16Ptr(title) + opts := fileoptsParse(options) + if opts.title != "" { + args.Title = syscall.StringToUTF16Ptr(opts.title) } - if defaultPath != "" { - args.InitialDir = syscall.StringToUTF16Ptr(defaultPath) + if opts.filename != "" { + args.InitialDir = syscall.StringToUTF16Ptr(opts.filename) } - if confirmOverwrite { + if opts.overwrite { args.Flags |= 0x2 // OFN_OVERWRITEPROMPT } - args.Filter = &windowsFilters(filters)[0] + args.Filter = &windowsFilters(opts.filters)[0] res := [32768]uint16{} args.File = &res[0] args.MaxFile = uint32(len(res)) + runtime.LockOSThread() + defer runtime.UnlockOSThread() + n, _, _ := getSaveFileName.Call(uintptr(unsafe.Pointer(&args))) if n == 0 { - n, _, _ = commDlgExtendedError.Call() - if n == 0 { - return "", nil - } else { - return "", fmt.Errorf("Common Dialog error: %x", n) - } + return "", commDlgError() } return syscall.UTF16ToString(res[:]), nil } -func SelectDirectory(title, defaultPath string) (string, error) { +func SelectDirectory(options ...FileOption) (string, error) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + hr, _, _ := coInitializeEx.Call(0, 0x6) // COINIT_APARTMENTTHREADED|COINIT_DISABLE_OLE1DDE - if hr < 0 { - return "", errors.New("COM initialization failed.") + if hr != 0x80010106 { // RPC_E_CHANGED_MODE + if int32(hr) < 0 { + return "", syscall.Errno(hr) + } + defer coUninitialize.Call() } - defer coUninitialize.Call() + + opts := fileoptsParse(options) var dialog *_IFileOpenDialog hr, _, _ = coCreateInstance.Call( _CLSID_FileOpenDialog, 0, 0x17, // CLSCTX_ALL _IID_IFileOpenDialog, uintptr(unsafe.Pointer(&dialog))) - if hr < 0 || dialog == nil { - return browseForFolder(title, defaultPath) + if int32(hr) < 0 { + return browseForFolder(opts.title) } defer dialog.Call(dialog.vtbl.Release) - var opts int - hr, _, _ = dialog.Call(dialog.vtbl.GetOptions, uintptr(unsafe.Pointer(&opts))) - if hr < 0 { - return "", fmt.Errorf("IFileOpenDialog.GetOptions error: %x", hr) + var flgs int + hr, _, _ = dialog.Call(dialog.vtbl.GetOptions, uintptr(unsafe.Pointer(&flgs))) + if int32(hr) < 0 { + return "", syscall.Errno(hr) } - hr, _, _ = dialog.Call(dialog.vtbl.SetOptions, uintptr(opts|0x68)) // FOS_NOCHANGEDIR|FOS_PICKFOLDERS|FOS_FORCEFILESYSTEM - if hr < 0 { - return "", fmt.Errorf("IFileOpenDialog.SetOptions error: %x", hr) + hr, _, _ = dialog.Call(dialog.vtbl.SetOptions, uintptr(flgs|0x68)) // FOS_NOCHANGEDIR|FOS_PICKFOLDERS|FOS_FORCEFILESYSTEM + if int32(hr) < 0 { + return "", syscall.Errno(hr) } - if title != "" { - ptr := syscall.StringToUTF16Ptr(title) + if opts.title != "" { + ptr := syscall.StringToUTF16Ptr(opts.title) dialog.Call(dialog.vtbl.SetTitle, uintptr(unsafe.Pointer(ptr))) } - if defaultPath != "" { + if opts.filename != "" { var item *_IShellItem - ptr := syscall.StringToUTF16Ptr(defaultPath) + ptr := syscall.StringToUTF16Ptr(opts.filename) hr, _, _ = shCreateItemFromParsingName.Call( uintptr(unsafe.Pointer(ptr)), 0, _IID_IShellItem, @@ -188,17 +192,17 @@ func SelectDirectory(title, defaultPath string) (string, error) { } hr, _, _ = dialog.Call(dialog.vtbl.Show, 0) - if hr < 0 { - return "", fmt.Errorf("IFileOpenDialog.Show error: %x", hr) + if hr == 0x800704c7 { // ERROR_CANCELLED + return "", nil + } + if int32(hr) < 0 { + return "", syscall.Errno(hr) } var item *_IShellItem hr, _, _ = dialog.Call(dialog.vtbl.GetResult, uintptr(unsafe.Pointer(&item))) - if hr < 0 { - return "", fmt.Errorf("IFileOpenDialog.GetResult error: %x", hr) - } - if item == nil { - return "", nil + if int32(hr) < 0 { + return "", syscall.Errno(hr) } defer item.Call(item.vtbl.Release) @@ -206,8 +210,8 @@ func SelectDirectory(title, defaultPath string) (string, error) { hr, _, _ = item.Call(item.vtbl.GetDisplayName, 0x80058000, // SIGDN_FILESYSPATH uintptr(unsafe.Pointer(&ptr))) - if hr < 0 { - return "", fmt.Errorf("IShellItem.GetDisplayName error: %x", hr) + if int32(hr) < 0 { + return "", syscall.Errno(hr) } defer coTaskMemFree.Call(ptr) @@ -215,7 +219,7 @@ func SelectDirectory(title, defaultPath string) (string, error) { return syscall.UTF16ToString(*(*[]uint16)(unsafe.Pointer(&res))), nil } -func browseForFolder(title, defaultPath string) (string, error) { +func browseForFolder(title string) (string, error) { var args _BROWSEINFO args.Flags = 0x1 // BIF_RETURNONLYFSDIRS @@ -253,6 +257,15 @@ func windowsFilters(filters []FileFilter) []uint16 { return res } +func commDlgError() error { + n, _, _ := commDlgExtendedError.Call() + if n == 0 { + return nil + } else { + return fmt.Errorf("Common Dialog error: %x", n) + } +} + type _OPENFILENAME struct { StructSize uint32 Owner uintptr diff --git a/file_windows_test.go b/init_windows_test.go similarity index 53% rename from file_windows_test.go rename to init_windows_test.go index cc2f736..f1a4489 100644 --- a/file_windows_test.go +++ b/init_windows_test.go @@ -1,10 +1,5 @@ package zenity -import ( - "syscall" -) - func init() { - user32 := syscall.NewLazyDLL("user32.dll") user32.NewProc("SetProcessDPIAware").Call() }