diff --git a/cmd/zenity/main.go b/cmd/zenity/main.go index 85e75b9..b56049b 100644 --- a/cmd/zenity/main.go +++ b/cmd/zenity/main.go @@ -37,6 +37,8 @@ var ( multiple bool directory bool confirmOverwrite bool + confirmCreate bool + showHidden bool filename string separator string fileFilters FileFilters @@ -104,6 +106,8 @@ func setupFlags() { flag.BoolVar(&multiple, "multiple", false, "Allow multiple files to be selected") flag.BoolVar(&directory, "directory", false, "Activate directory-only selection") flag.BoolVar(&confirmOverwrite, "confirm-overwrite", false, "Confirm file selection if filename already exists") + flag.BoolVar(&confirmCreate, "confirm-create", false, "Confirm file selection if filename does not yet exist (Windows only)") + flag.BoolVar(&showHidden, "show-hidden", false, "Show hidden files (Windows and macOS only)") flag.StringVar(&filename, "filename", "", "Set the filename") flag.StringVar(&separator, "separator", "|", "Set output separator character") flag.Var(&fileFilters, "file-filter", "Set a filename filter (NAME | PATTERN1 PATTERN2 ...)") @@ -158,24 +162,30 @@ func loadFlags() []zenity.Option { options = append(options, zenity.CancelLabel(cancelLabel)) options = append(options, zenity.ExtraButton(extraButton)) if noWrap { - options = append(options, zenity.NoWrap) + options = append(options, zenity.NoWrap()) } if ellipsize { - options = append(options, zenity.Ellipsize) + options = append(options, zenity.Ellipsize()) } if defaultCancel { - options = append(options, zenity.DefaultCancel) + options = append(options, zenity.DefaultCancel()) } // File selection options - options = append(options, fileFilters.New()) + options = append(options, fileFilters.Build()) options = append(options, zenity.Filename(filename)) if directory { - options = append(options, zenity.Directory) + options = append(options, zenity.Directory()) } if confirmOverwrite { - options = append(options, zenity.ConfirmOverwrite) + options = append(options, zenity.ConfirmOverwrite()) + } + if confirmCreate { + options = append(options, zenity.ConfirmCreate()) + } + if showHidden { + options = append(options, zenity.ShowHidden()) } cmd.Separator = separator diff --git a/file_test.go b/file_test.go index 97fc577..6a48b05 100644 --- a/file_test.go +++ b/file_test.go @@ -12,7 +12,7 @@ func ExampleSelectFile() { {"Go files", []string{"*.go"}}, {"Web files", []string{"*.html", "*.js", "*.css"}}, {"Image files", []string{"*.png", "*.gif", "*.ico", "*.jpg", "*.webp"}}, - }.New()) + }.Build()) // Output: } @@ -23,32 +23,32 @@ func ExampleSelectFileMutiple() { {"Go files", []string{"*.go"}}, {"Web files", []string{"*.html", "*.js", "*.css"}}, {"Image files", []string{"*.png", "*.gif", "*.ico", "*.jpg", "*.webp"}}, - }.New()) + }.Build()) // Output: } func ExampleSelectFileSave() { zenity.SelectFileSave( - zenity.ConfirmOverwrite, + zenity.ConfirmOverwrite(), zenity.Filename(defaultName), zenity.FileFilters{ {"Go files", []string{"*.go"}}, {"Web files", []string{"*.html", "*.js", "*.css"}}, {"Image files", []string{"*.png", "*.gif", "*.ico", "*.jpg", "*.webp"}}, - }.New()) + }.Build()) // Output: } func ExampleSelectFile_directory() { zenity.SelectFile( zenity.Filename(defaultPath), - zenity.Directory) + zenity.Directory()) // Output: } func ExampleSelectFileMutiple_directory() { zenity.SelectFileMutiple( zenity.Filename(defaultPath), - zenity.Directory) + zenity.Directory()) // Output: } diff --git a/file_unix.go b/file_unix.go index 9aaa240..0c10f80 100644 --- a/file_unix.go +++ b/file_unix.go @@ -14,7 +14,7 @@ import ( // // Returns an empty string on cancel. // -// Valid options: Title, Directory, Filename, FileFilters. +// Valid options: Title, Directory, Filename, ShowHidden, FileFilter(s). func SelectFile(options ...Option) (string, error) { opts := optsParse(options) @@ -47,7 +47,7 @@ func SelectFile(options ...Option) (string, error) { // // Returns a nil slice on cancel. // -// Valid options: Title, Directory, Filename, FileFilters. +// Valid options: Title, Directory, Filename, ShowHidden, FileFilter(s). func SelectFileMutiple(options ...Option) ([]string, error) { opts := optsParse(options) @@ -80,7 +80,7 @@ func SelectFileMutiple(options ...Option) ([]string, error) { // // Returns an empty string on cancel. // -// Valid options: Title, Filename, ConfirmOverwrite, FileFilters. +// Valid options: Title, Filename, ConfirmOverwrite, ConfirmCreate, ShowHidden, FileFilter(s). func SelectFileSave(options ...Option) (string, error) { opts := optsParse(options) diff --git a/file_windows.go b/file_windows.go index e80bf67..5f01a81 100644 --- a/file_windows.go +++ b/file_windows.go @@ -28,11 +28,14 @@ func SelectFile(options ...Option) (string, error) { var args _OPENFILENAME args.StructSize = uint32(unsafe.Sizeof(args)) - args.Flags = 0x80008 // OFN_NOCHANGEDIR|OFN_EXPLORER + args.Flags = 0x81008 // OFN_NOCHANGEDIR|OFN_FILEMUSTEXIST|OFN_EXPLORER if opts.title != "" { args.Title = syscall.StringToUTF16Ptr(opts.title) } + if opts.hidden { + args.Flags |= 0x10000000 // OFN_FORCESHOWHIDDEN + } if opts.filters != nil { args.Filter = &windowsFilters(opts.filters)[0] } @@ -40,7 +43,7 @@ func SelectFile(options ...Option) (string, error) { res := [32768]uint16{} args.File = &res[0] args.MaxFile = uint32(len(res)) - args.InitialDir = initDirAndName(opts.filename, res[:]) + args.InitialDir, args.DefExt = initDirNameExt(opts.filename, res[:]) runtime.LockOSThread() defer runtime.UnlockOSThread() @@ -61,11 +64,14 @@ func SelectFileMutiple(options ...Option) ([]string, error) { var args _OPENFILENAME args.StructSize = uint32(unsafe.Sizeof(args)) - args.Flags = 0x80208 // OFN_NOCHANGEDIR|OFN_ALLOWMULTISELECT|OFN_EXPLORER + args.Flags = 0x81208 // OFN_NOCHANGEDIR|OFN_ALLOWMULTISELECT|OFN_FILEMUSTEXIST|OFN_EXPLORER if opts.title != "" { args.Title = syscall.StringToUTF16Ptr(opts.title) } + if opts.hidden { + args.Flags |= 0x10000000 // OFN_FORCESHOWHIDDEN + } if opts.filters != nil { args.Filter = &windowsFilters(opts.filters)[0] } @@ -73,7 +79,7 @@ func SelectFileMutiple(options ...Option) ([]string, error) { res := [32768 + 1024*256]uint16{} args.File = &res[0] args.MaxFile = uint32(len(res)) - args.InitialDir = initDirAndName(opts.filename, res[:]) + args.InitialDir, args.DefExt = initDirNameExt(opts.filename, res[:]) runtime.LockOSThread() defer runtime.UnlockOSThread() @@ -112,10 +118,14 @@ func SelectFileMutiple(options ...Option) ([]string, error) { func SelectFileSave(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 + args.Flags = 0x88808 // OFN_NOCHANGEDIR|OFN_PATHMUSTEXIST|OFN_NOREADONLYRETURN|OFN_EXPLORER if opts.title != "" { args.Title = syscall.StringToUTF16Ptr(opts.title) @@ -123,6 +133,12 @@ func SelectFileSave(options ...Option) (string, error) { if opts.overwrite { args.Flags |= 0x2 // OFN_OVERWRITEPROMPT } + if opts.create { + args.Flags |= 0x2000 // OFN_CREATEPROMPT + } + if opts.hidden { + args.Flags |= 0x10000000 // OFN_FORCESHOWHIDDEN + } if opts.filters != nil { args.Filter = &windowsFilters(opts.filters)[0] } @@ -130,7 +146,7 @@ func SelectFileSave(options ...Option) (string, error) { res := [32768]uint16{} args.File = &res[0] args.MaxFile = uint32(len(res)) - args.InitialDir = initDirAndName(opts.filename, res[:]) + args.InitialDir, args.DefExt = initDirNameExt(opts.filename, res[:]) runtime.LockOSThread() defer runtime.UnlockOSThread() @@ -171,6 +187,9 @@ func pickFolders(opts options, multi bool) (str string, lst []string, err error) if multi { flgs |= 0x200 // FOS_ALLOWMULTISELECT } + if opts.hidden { + flgs |= 0x10000000 // FOS_FORCESHOWHIDDEN + } hr, _, _ = dialog.Call(dialog.vtbl.SetOptions, uintptr(flgs|0x68)) // FOS_NOCHANGEDIR|FOS_PICKFOLDERS|FOS_FORCEFILESYSTEM if int32(hr) < 0 { return "", nil, syscall.Errno(hr) @@ -239,11 +258,8 @@ func pickFolders(opts options, multi bool) (str string, lst []string, err error) if int32(hr) < 0 { return "", nil, syscall.Errno(hr) } - for i := uintptr(0); i < uintptr(count); i++ { + for i := uintptr(0); i < uintptr(count) && err == nil; i++ { err = shellItemPath(&items._COMObject, items.vtbl.GetItemAt, i) - if err != nil { - return - } } } else { err = shellItemPath(&dialog._COMObject, dialog.vtbl.GetResult) @@ -272,17 +288,21 @@ func browseForFolder(title string) (string, []string, error) { return str, []string{str}, nil } -func initDirAndName(filename string, name []uint16) (dir *uint16) { +func initDirNameExt(filename string, name []uint16) (dir *uint16, ext *uint16) { if filename != "" { d, n := splitDirAndName(filename) + e := filepath.Ext(n) if n != "" { copy(name, syscall.StringToUTF16(n)) } if d != "" { - return syscall.StringToUTF16Ptr(d) + dir = syscall.StringToUTF16Ptr(d) + } + if e != "" { + ext = syscall.StringToUTF16Ptr(e[1:]) } } - return nil + return } func windowsFilters(filters []FileFilter) []uint16 { diff --git a/internal/osa/generator.go b/internal/osa/generator.go index 79a0946..fb3cfdc 100644 --- a/internal/osa/generator.go +++ b/internal/osa/generator.go @@ -23,33 +23,33 @@ func main() { 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}}") - } + 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("{{end}}") } out, err := os.Create("generated.go") diff --git a/zenity.go b/zenity.go index 40e52d7..40a9608 100644 --- a/zenity.go +++ b/zenity.go @@ -20,6 +20,8 @@ type options struct { filename string directory bool overwrite bool + create bool + hidden bool filters []FileFilter // Message options @@ -32,7 +34,7 @@ type options struct { defcancel bool } -// Options are arguments you pass to dialog functions to customize their behavior. +// Options are arguments passed to dialog functions to customize their behavior. type Option func(*options) func optsParse(options []Option) (res options) { @@ -46,9 +48,7 @@ func optsParse(options []Option) (res options) { // Option to set the dialog title. func Title(title string) Option { - return func(o *options) { - o.title = title - } + return func(o *options) { o.title = title } } // File selection options @@ -57,41 +57,51 @@ func Title(title string) Option { // // You can specify a file name, a directory path, or both. // Specifying a file name, makes it the default selected file. -// Specifying a directory path, make it the default dialog location. +// Specifying a directory path, makes it the default dialog location. func Filename(filename string) Option { - return func(o *options) { - o.filename = filename - } + return func(o *options) { o.filename = filename } } // Option to activate directory-only selection. -func Directory(o *options) { - o.directory = true +func Directory() Option { + return func(o *options) { o.directory = true } } // Option to confirm file selection if filename already exists. -func ConfirmOverwrite(o *options) { - o.overwrite = true +func ConfirmOverwrite() Option { + return func(o *options) { o.overwrite = true } +} + +// Option to confirm file selection if filename does not yet exist (Windows only). +func ConfirmCreate() Option { + return func(o *options) { o.create = true } +} + +// Option to show hidden files (Windows and macOS only). +func ShowHidden() Option { + return func(o *options) { o.hidden = true } } // FileFilter encapsulates a filename filter. +// +// macOS hides filename filters from the user, +// and only supports filtering by extension (or "type"). type FileFilter struct { Name string // display string that describes the filter (optional) Patterns []string // filter patterns for the display string } +// Build option to set a filename filter. +func (f FileFilter) Build() Option { + return func(o *options) { o.filters = append(o.filters, f) } +} + // FileFilters is a list of filename filters. -// -// macOS hides filename filters from the user, -// and only supports filtering by extension (or "type"). -// We make an effort to convert any "*.EXT" like patterns. type FileFilters []FileFilter -// Creates Option to set the filename filter list. -func (f FileFilters) New() Option { - return func(o *options) { - o.filters = f - } +// Build option to set filename filters. +func (f FileFilters) Build() Option { + return func(o *options) { o.filters = append(o.filters, f...) } } // Message options @@ -108,43 +118,35 @@ const ( // Option to set the dialog icon. func Icon(icon MessageIcon) Option { - return func(o *options) { - o.icon = icon - } + return func(o *options) { o.icon = icon } } // Option to set the label of the OK button. func OKLabel(ok string) Option { - return func(o *options) { - o.ok = ok - } + return func(o *options) { o.ok = ok } } // Option to set the label of the Cancel button. func CancelLabel(cancel string) Option { - return func(o *options) { - o.cancel = cancel - } + return func(o *options) { o.cancel = cancel } } // Option to add an extra button. func ExtraButton(extra string) Option { - return func(o *options) { - o.extra = extra - } + return func(o *options) { o.extra = extra } } // Option to disable enable text wrapping. -func NoWrap(o *options) { - o.nowrap = true +func NoWrap() Option { + return func(o *options) { o.nowrap = true } } // Option to enable ellipsizing in the dialog text. -func Ellipsize(o *options) { - o.ellipsize = true +func Ellipsize() Option { + return func(o *options) { o.ellipsize = true } } // Option to give Cancel button focus by default. -func DefaultCancel(o *options) { - o.defcancel = true +func DefaultCancel() Option { + return func(o *options) { o.defcancel = true } }