From 0c7d5c4c8b5825a06be6a5d585d4c91fd5f66d3d Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Fri, 10 Jan 2020 01:46:53 +0000 Subject: [PATCH] Multiple directory selection. --- cmd/zenity/main.go | 5 +- file_darwin.go | 66 ++++++++++++------------- file_test.go | 12 ++++- file_unix.go | 30 +++--------- file_windows.go | 117 +++++++++++++++++++++++++++++++++------------ msg_darwin.go | 30 +++++------- zenity.go | 5 ++ 7 files changed, 153 insertions(+), 112 deletions(-) diff --git a/cmd/zenity/main.go b/cmd/zenity/main.go index 5da225c..b99bb3d 100644 --- a/cmd/zenity/main.go +++ b/cmd/zenity/main.go @@ -65,8 +65,6 @@ func main() { strResult(zenity.SelectFile(opts...)) case save: strResult(zenity.SelectFileSave(opts...)) - case directory: - strResult(zenity.SelectDirectory(opts...)) case multiple: lstResult(zenity.SelectFileMutiple(opts...)) } @@ -173,6 +171,9 @@ func loadFlags() []zenity.Option { options = append(options, fileFilters.New()) options = append(options, zenity.Filename(filename)) + if directory { + options = append(options, zenity.Directory) + } if confirmOverwrite { options = append(options, zenity.ConfirmOverwrite) } diff --git a/file_darwin.go b/file_darwin.go index 67d90e5..e2bc9cb 100644 --- a/file_darwin.go +++ b/file_darwin.go @@ -10,13 +10,19 @@ import ( func SelectFile(options ...Option) (string, error) { opts := optsParse(options) - dir, _ := splitDirAndName(opts.filename) - out, err := osa.Run("file", osa.File{ - Operation: "chooseFile", - Prompt: opts.title, - Type: appleFilters(opts.filters), - Location: dir, - }) + + data := osa.File{ + Prompt: opts.title, + } + if opts.directory { + data.Operation = "chooseFolder" + } else { + data.Operation = "chooseFile" + data.Type = appleFilters(opts.filters) + } + data.Location, _ = splitDirAndName(opts.filename) + + out, err := osa.Run("file", data) if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 { return "", nil } @@ -31,15 +37,21 @@ func SelectFile(options ...Option) (string, error) { func SelectFileMutiple(options ...Option) ([]string, error) { opts := optsParse(options) - dir, _ := splitDirAndName(opts.filename) - out, err := osa.Run("file", osa.File{ - Operation: "chooseFile", + + data := osa.File{ Multiple: true, Prompt: opts.title, Separator: cmd.Separator, - Type: appleFilters(opts.filters), - Location: dir, - }) + } + if opts.directory { + data.Operation = "chooseFolder" + } else { + data.Operation = "chooseFile" + data.Type = appleFilters(opts.filters) + } + data.Location, _ = splitDirAndName(opts.filename) + + out, err := osa.Run("file", data) if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 { return nil, nil } @@ -57,33 +69,15 @@ func SelectFileMutiple(options ...Option) ([]string, error) { func SelectFileSave(options ...Option) (string, error) { opts := optsParse(options) - dir, name := splitDirAndName(opts.filename) - out, err := osa.Run("file", osa.File{ + + data := osa.File{ Operation: "chooseFileName", Prompt: opts.title, - Location: dir, - Name: name, - }) - if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 { - return "", nil + Type: appleFilters(opts.filters), } - if err != nil { - return "", err - } - if len(out) > 0 { - out = out[:len(out)-1] - } - return string(out), nil -} + data.Location, data.Name = splitDirAndName(opts.filename) -func SelectDirectory(options ...Option) (string, error) { - opts := optsParse(options) - dir, _ := splitDirAndName(opts.filename) - out, err := osa.Run("file", osa.File{ - Operation: "chooseFolder", - Prompt: opts.title, - Location: dir, - }) + out, err := osa.Run("file", data) if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 { return "", nil } diff --git a/file_test.go b/file_test.go index 4797ad0..4fda4ab 100644 --- a/file_test.go +++ b/file_test.go @@ -47,7 +47,17 @@ func TestSelectFileSave(t *testing.T) { } func TestSelectDirectory(t *testing.T) { - res, err := SelectDirectory(Filename(defaultPath)) + res, err := SelectFile(Directory, Filename(defaultPath)) + + if err != nil { + t.Error(err) + } else { + t.Logf("%#v", res) + } +} + +func TestSelectDirectoryMultiple(t *testing.T) { + res, err := SelectFileMutiple(Directory, Filename(defaultPath)) if err != nil { t.Error(err) diff --git a/file_unix.go b/file_unix.go index 224fc24..17ea30d 100644 --- a/file_unix.go +++ b/file_unix.go @@ -14,6 +14,9 @@ func SelectFile(options ...Option) (string, error) { opts := optsParse(options) args := []string{"--file-selection"} + if opts.directory { + args = append(args, "--directory") + } if opts.title != "" { args = append(args, "--title", opts.title) } @@ -39,6 +42,9 @@ func SelectFileMutiple(options ...Option) ([]string, error) { opts := optsParse(options) args := []string{"--file-selection", "--multiple", "--separator", cmd.Separator} + if opts.directory { + args = append(args, "--directory") + } if opts.title != "" { args = append(args, "--title", opts.title) } @@ -88,30 +94,6 @@ func SelectFileSave(options ...Option) (string, error) { return string(out), nil } -func SelectDirectory(options ...Option) (string, error) { - opts := optsParse(options) - - args := []string{"--file-selection", "--directory"} - if opts.title != "" { - args = append(args, "--title", opts.title) - } - if opts.filename != "" { - args = append(args, "--filename", opts.filename) - } - - out, err := zen.Run(args) - if err, ok := err.(*exec.ExitError); ok && err.ExitCode() != 255 { - return "", nil - } - if err != nil { - return "", err - } - if len(out) > 0 { - out = out[:len(out)-1] - } - return string(out), nil -} - func zenityFilters(filters []FileFilter) []string { var res []string for _, f := range filters { diff --git a/file_windows.go b/file_windows.go index e0fd633..e80bf67 100644 --- a/file_windows.go +++ b/file_windows.go @@ -20,11 +20,16 @@ var ( ) func SelectFile(options ...Option) (string, error) { + opts := optsParse(options) + if opts.directory { + res, _, err := pickFolders(opts, false) + return res, err + } + var args _OPENFILENAME args.StructSize = uint32(unsafe.Sizeof(args)) args.Flags = 0x80008 // OFN_NOCHANGEDIR|OFN_EXPLORER - opts := optsParse(options) if opts.title != "" { args.Title = syscall.StringToUTF16Ptr(opts.title) } @@ -48,11 +53,16 @@ func SelectFile(options ...Option) (string, error) { } func SelectFileMutiple(options ...Option) ([]string, error) { + opts := optsParse(options) + if opts.directory { + _, res, err := pickFolders(opts, true) + return res, err + } + var args _OPENFILENAME args.StructSize = uint32(unsafe.Sizeof(args)) args.Flags = 0x80208 // OFN_NOCHANGEDIR|OFN_ALLOWMULTISELECT|OFN_EXPLORER - opts := optsParse(options) if opts.title != "" { args.Title = syscall.StringToUTF16Ptr(opts.title) } @@ -101,11 +111,12 @@ func SelectFileMutiple(options ...Option) ([]string, error) { } func SelectFileSave(options ...Option) (string, error) { + opts := optsParse(options) + var args _OPENFILENAME args.StructSize = uint32(unsafe.Sizeof(args)) args.Flags = 0x80008 // OFN_NOCHANGEDIR|OFN_EXPLORER - opts := optsParse(options) if opts.title != "" { args.Title = syscall.StringToUTF16Ptr(opts.title) } @@ -131,20 +142,18 @@ func SelectFileSave(options ...Option) (string, error) { return syscall.UTF16ToString(res[:]), nil } -func SelectDirectory(options ...Option) (string, error) { +func pickFolders(opts options, multi bool) (str string, lst []string, err error) { runtime.LockOSThread() defer runtime.UnlockOSThread() hr, _, _ := coInitializeEx.Call(0, 0x6) // COINIT_APARTMENTTHREADED|COINIT_DISABLE_OLE1DDE if hr != 0x80010106 { // RPC_E_CHANGED_MODE if int32(hr) < 0 { - return "", syscall.Errno(hr) + return "", nil, syscall.Errno(hr) } defer coUninitialize.Call() } - opts := optsParse(options) - var dialog *_IFileOpenDialog hr, _, _ = coCreateInstance.Call( _CLSID_FileOpenDialog, 0, 0x17, // CLSCTX_ALL @@ -157,11 +166,14 @@ func SelectDirectory(options ...Option) (string, error) { var flgs int hr, _, _ = dialog.Call(dialog.vtbl.GetOptions, uintptr(unsafe.Pointer(&flgs))) if int32(hr) < 0 { - return "", syscall.Errno(hr) + return "", nil, syscall.Errno(hr) + } + if multi { + flgs |= 0x200 // FOS_ALLOWMULTISELECT } hr, _, _ = dialog.Call(dialog.vtbl.SetOptions, uintptr(flgs|0x68)) // FOS_NOCHANGEDIR|FOS_PICKFOLDERS|FOS_FORCEFILESYSTEM if int32(hr) < 0 { - return "", syscall.Errno(hr) + return "", nil, syscall.Errno(hr) } if opts.title != "" { @@ -185,33 +197,61 @@ func SelectDirectory(options ...Option) (string, error) { hr, _, _ = dialog.Call(dialog.vtbl.Show, 0) if hr == 0x800704c7 { // ERROR_CANCELLED - return "", nil + return "", nil, nil } if int32(hr) < 0 { - return "", syscall.Errno(hr) + return "", nil, syscall.Errno(hr) } - var item *_IShellItem - hr, _, _ = dialog.Call(dialog.vtbl.GetResult, uintptr(unsafe.Pointer(&item))) - if int32(hr) < 0 { - return "", syscall.Errno(hr) - } - defer item.Call(item.vtbl.Release) + shellItemPath := func(obj *_COMObject, trap uintptr, a ...uintptr) error { + var item *_IShellItem + hr, _, _ := obj.Call(trap, append(a, uintptr(unsafe.Pointer(&item)))...) + if int32(hr) < 0 { + return syscall.Errno(hr) + } + defer item.Call(item.vtbl.Release) - var ptr uintptr - hr, _, _ = item.Call(item.vtbl.GetDisplayName, - 0x80058000, // SIGDN_FILESYSPATH - uintptr(unsafe.Pointer(&ptr))) - if int32(hr) < 0 { - return "", syscall.Errno(hr) - } - defer coTaskMemFree.Call(ptr) + var ptr uintptr + hr, _, _ = item.Call(item.vtbl.GetDisplayName, + 0x80058000, // SIGDN_FILESYSPATH + uintptr(unsafe.Pointer(&ptr))) + if int32(hr) < 0 { + return syscall.Errno(hr) + } + defer coTaskMemFree.Call(ptr) - res := reflect.SliceHeader{Data: ptr, Len: 32768, Cap: 32768} - return syscall.UTF16ToString(*(*[]uint16)(unsafe.Pointer(&res))), nil + res := reflect.SliceHeader{Data: ptr, Len: 32768, Cap: 32768} + str = syscall.UTF16ToString(*(*[]uint16)(unsafe.Pointer(&res))) + lst = append(lst, str) + return nil + } + + if multi { + var items *_IShellItemArray + hr, _, _ = dialog.Call(dialog.vtbl.GetResults, uintptr(unsafe.Pointer(&items))) + if int32(hr) < 0 { + return "", nil, syscall.Errno(hr) + } + defer items.Call(items.vtbl.Release) + + var count uint32 + hr, _, _ = items.Call(items.vtbl.GetCount, uintptr(unsafe.Pointer(&count))) + if int32(hr) < 0 { + return "", nil, syscall.Errno(hr) + } + for i := uintptr(0); i < uintptr(count); i++ { + err = shellItemPath(&items._COMObject, items.vtbl.GetItemAt, i) + if err != nil { + return + } + } + } else { + err = shellItemPath(&dialog._COMObject, dialog.vtbl.GetResult) + } + return } -func browseForFolder(title string) (string, error) { +func browseForFolder(title string) (string, []string, error) { var args _BROWSEINFO args.Flags = 0x1 // BIF_RETURNONLYFSDIRS @@ -221,14 +261,15 @@ func browseForFolder(title string) (string, error) { ptr, _, _ := shBrowseForFolder.Call(uintptr(unsafe.Pointer(&args))) if ptr == 0 { - return "", nil + return "", nil, nil } defer coTaskMemFree.Call(ptr) res := [32768]uint16{} shGetPathFromIDListEx.Call(ptr, uintptr(unsafe.Pointer(&res[0])), uintptr(len(res)), 0) - return syscall.UTF16ToString(res[:]), nil + str := syscall.UTF16ToString(res[:]) + return str, []string{str}, nil } func initDirAndName(filename string, name []uint16) (dir *uint16) { @@ -344,6 +385,11 @@ type _IShellItem struct { vtbl *_IShellItemVtbl } +type _IShellItemArray struct { + _COMObject + vtbl *_IShellItemArrayVtbl +} + type _IFileOpenDialogVtbl struct { _IFileDialogVtbl GetResults uintptr @@ -391,6 +437,17 @@ type _IShellItemVtbl struct { Compare uintptr } +type _IShellItemArrayVtbl struct { + _IUnknownVtbl + BindToHandler uintptr + GetPropertyStore uintptr + GetPropertyDescriptionList uintptr + GetAttributes uintptr + GetCount uintptr + GetItemAt uintptr + EnumItems uintptr +} + type _IUnknownVtbl struct { QueryInterface uintptr AddRef uintptr diff --git a/msg_darwin.go b/msg_darwin.go index 4cc33d0..dd6338b 100644 --- a/msg_darwin.go +++ b/msg_darwin.go @@ -24,22 +24,13 @@ func Warning(text string, options ...Option) (bool, error) { func message(typ int, text string, options []Option) (bool, error) { opts := optsParse(options) - - dialog := opts.icon != 0 || typ == 2 - var op string - if dialog { - op = "displayDialog" - } else { - op = "displayAlert" - } - - data := osa.Msg{ - Operation: op, - Text: text, - Title: opts.title, - } + data := osa.Msg{Text: text} + dialog := typ == 2 || opts.icon != 0 if dialog { + data.Operation = "displayDialog" + data.Title = opts.title + switch opts.icon { case ErrorIcon: data.Icon = "stop" @@ -49,6 +40,12 @@ func message(typ int, text string, options []Option) (bool, error) { data.Icon = "caution" } } else { + data.Operation = "displayAlert" + if opts.title != "" { + data.Message = text + data.Text = opts.title + } + switch typ { case 0: data.As = "critical" @@ -57,11 +54,6 @@ func message(typ int, text string, options []Option) (bool, error) { case 3: data.As = "warning" } - - if opts.title != "" { - data.Text = opts.title - data.Message = text - } } if typ != 2 { diff --git a/zenity.go b/zenity.go index 263ef77..08b5f6f 100644 --- a/zenity.go +++ b/zenity.go @@ -18,6 +18,7 @@ type options struct { // File selection options filename string + directory bool overwrite bool filters []FileFilter @@ -56,6 +57,10 @@ func Filename(filename string) Option { } } +func Directory(o *options) { + o.directory = true +} + func ConfirmOverwrite(o *options) { o.overwrite = true }