2020-01-09 12:05:43 -05:00
|
|
|
package zenity
|
|
|
|
|
|
|
|
import (
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2021-04-11 22:59:08 -04:00
|
|
|
"strings"
|
2020-01-09 12:05:43 -05:00
|
|
|
)
|
|
|
|
|
2020-01-23 06:44:28 -05:00
|
|
|
// SelectFile displays the file selection dialog.
|
|
|
|
//
|
2022-06-02 07:24:24 -04:00
|
|
|
// Valid options: Title, WindowIcon, Attach, Modal, Directory, Filename,
|
|
|
|
// ShowHidden, FileFilter(s).
|
2022-03-25 20:58:27 -04:00
|
|
|
//
|
|
|
|
// May return: ErrCanceled.
|
2020-01-23 06:44:28 -05:00
|
|
|
func SelectFile(options ...Option) (string, error) {
|
2021-03-04 07:42:30 -05:00
|
|
|
return selectFile(applyOptions(options))
|
2020-01-23 06:44:28 -05:00
|
|
|
}
|
|
|
|
|
2022-05-19 09:09:21 -04:00
|
|
|
// SelectFileMultiple displays the multiple file selection dialog.
|
2020-01-23 06:44:28 -05:00
|
|
|
//
|
2022-06-02 07:24:24 -04:00
|
|
|
// Valid options: Title, WindowIcon, Attach, Modal, Directory, Filename,
|
|
|
|
// ShowHidden, FileFilter(s).
|
2022-03-25 20:58:27 -04:00
|
|
|
//
|
|
|
|
// May return: ErrCanceled, ErrUnsupported.
|
2022-05-19 09:09:21 -04:00
|
|
|
func SelectFileMultiple(options ...Option) ([]string, error) {
|
|
|
|
return selectFileMultiple(applyOptions(options))
|
|
|
|
}
|
|
|
|
|
|
|
|
// Deprecated: use SelectFileMultiple.
|
2020-01-23 06:44:28 -05:00
|
|
|
func SelectFileMutiple(options ...Option) ([]string, error) {
|
2022-05-19 09:09:21 -04:00
|
|
|
return SelectFileMultiple(options...)
|
2020-01-23 06:44:28 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// SelectFileSave displays the save file selection dialog.
|
|
|
|
//
|
2022-06-02 07:24:24 -04:00
|
|
|
// Valid options: Title, WindowIcon, Attach, Modal, Filename,
|
|
|
|
// ConfirmOverwrite, ConfirmCreate, ShowHidden, FileFilter(s).
|
2022-03-25 20:58:27 -04:00
|
|
|
//
|
|
|
|
// May return: ErrCanceled.
|
2020-01-23 06:44:28 -05:00
|
|
|
func SelectFileSave(options ...Option) (string, error) {
|
2021-03-04 07:42:30 -05:00
|
|
|
return selectFileSave(applyOptions(options))
|
2020-01-23 06:44:28 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// Directory returns an Option to activate directory-only selection.
|
|
|
|
func Directory() Option {
|
2020-01-24 07:52:45 -05:00
|
|
|
return funcOption(func(o *options) { o.directory = true })
|
2020-01-23 06:44:28 -05:00
|
|
|
}
|
|
|
|
|
2022-05-02 06:22:53 -04:00
|
|
|
// ConfirmOverwrite returns an Option to confirm file selection if the file
|
2020-01-23 06:44:28 -05:00
|
|
|
// already exists.
|
|
|
|
func ConfirmOverwrite() Option {
|
2020-01-24 07:52:45 -05:00
|
|
|
return funcOption(func(o *options) { o.confirmOverwrite = true })
|
2020-01-23 06:44:28 -05:00
|
|
|
}
|
|
|
|
|
2022-05-02 06:22:53 -04:00
|
|
|
// ConfirmCreate returns an Option to confirm file selection if the file
|
|
|
|
// does not yet exist (Windows only).
|
2020-01-23 06:44:28 -05:00
|
|
|
func ConfirmCreate() Option {
|
2020-01-24 07:52:45 -05:00
|
|
|
return funcOption(func(o *options) { o.confirmCreate = true })
|
2020-01-23 06:44:28 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// ShowHidden returns an Option to show hidden files (Windows and macOS only).
|
|
|
|
func ShowHidden() Option {
|
2020-01-24 07:52:45 -05:00
|
|
|
return funcOption(func(o *options) { o.showHidden = true })
|
2020-01-23 06:44:28 -05:00
|
|
|
}
|
|
|
|
|
2021-03-05 10:14:30 -05:00
|
|
|
// Filename returns an Option to set the filename.
|
|
|
|
//
|
|
|
|
// 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, makes it the default dialog location.
|
|
|
|
func Filename(filename string) Option {
|
|
|
|
return funcOption(func(o *options) { o.filename = filename })
|
|
|
|
}
|
|
|
|
|
2020-01-24 07:52:45 -05:00
|
|
|
// FileFilter is an Option that sets a filename filter.
|
2020-01-23 06:44:28 -05:00
|
|
|
//
|
|
|
|
// macOS hides filename filters from the user,
|
2021-06-16 08:34:50 -04:00
|
|
|
// and only supports filtering by extension
|
|
|
|
// (or "uniform type identifiers").
|
2021-04-23 09:13:13 -04:00
|
|
|
//
|
2022-03-28 19:14:51 -04:00
|
|
|
// Patterns may use the fnmatch syntax on all platforms:
|
|
|
|
// https://docs.python.org/3/library/fnmatch.html
|
2020-01-23 06:44:28 -05:00
|
|
|
type FileFilter struct {
|
|
|
|
Name string // display string that describes the filter (optional)
|
|
|
|
Patterns []string // filter patterns for the display string
|
|
|
|
}
|
|
|
|
|
2020-01-24 07:52:45 -05:00
|
|
|
func (f FileFilter) apply(o *options) {
|
|
|
|
o.fileFilters = append(o.fileFilters, f)
|
2020-01-23 06:44:28 -05:00
|
|
|
}
|
|
|
|
|
2020-01-24 07:52:45 -05:00
|
|
|
// FileFilters is an Option that sets multiple filename filters.
|
2020-01-23 06:44:28 -05:00
|
|
|
type FileFilters []FileFilter
|
|
|
|
|
2020-01-24 07:52:45 -05:00
|
|
|
func (f FileFilters) apply(o *options) {
|
|
|
|
o.fileFilters = append(o.fileFilters, f...)
|
2020-01-23 06:44:28 -05:00
|
|
|
}
|
|
|
|
|
2021-04-13 09:37:10 -04:00
|
|
|
// Windows' patterns need a name.
|
|
|
|
func (f FileFilters) name() {
|
|
|
|
for i, filter := range f {
|
|
|
|
if filter.Name == "" {
|
|
|
|
f[i].Name = strings.Join(filter.Patterns, " ")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-13 08:28:26 -04:00
|
|
|
// Windows' patterns are case insensitive, don't support character classes or escaping.
|
2021-06-16 08:34:50 -04:00
|
|
|
//
|
|
|
|
// First we remove character classes, then escaping. Patterns with literal wildcards are invalid.
|
2021-04-13 08:28:26 -04:00
|
|
|
// The semicolon is a separator, so we replace it with the single character wildcard.
|
2021-06-16 12:24:35 -04:00
|
|
|
// Empty and invalid filters/patterns are ignored.
|
2021-04-13 08:28:26 -04:00
|
|
|
func (f FileFilters) simplify() {
|
2021-06-16 12:24:35 -04:00
|
|
|
var i = 0
|
|
|
|
for _, filter := range f {
|
|
|
|
var j = 0
|
2021-06-16 08:34:50 -04:00
|
|
|
for _, pattern := range filter.Patterns {
|
2021-04-13 08:28:26 -04:00
|
|
|
var escape, invalid bool
|
|
|
|
var buf strings.Builder
|
|
|
|
for _, r := range removeClasses(pattern) {
|
|
|
|
if !escape && r == '\\' {
|
|
|
|
escape = true
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if escape && (r == '*' || r == '?') {
|
|
|
|
invalid = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if r == ';' {
|
|
|
|
r = '?'
|
|
|
|
}
|
|
|
|
buf.WriteRune(r)
|
|
|
|
escape = false
|
|
|
|
}
|
2021-06-16 08:34:50 -04:00
|
|
|
if buf.Len() > 0 && !invalid {
|
2021-06-16 12:24:35 -04:00
|
|
|
filter.Patterns[j] = buf.String()
|
|
|
|
j++
|
2021-04-13 08:28:26 -04:00
|
|
|
}
|
|
|
|
}
|
2021-06-16 12:24:35 -04:00
|
|
|
if j > 0 {
|
|
|
|
filter.Patterns = filter.Patterns[:j]
|
|
|
|
f[i] = filter
|
|
|
|
i++
|
2021-06-16 08:34:50 -04:00
|
|
|
}
|
2021-04-13 08:28:26 -04:00
|
|
|
}
|
2021-06-16 12:24:35 -04:00
|
|
|
for ; i < len(f); i++ {
|
|
|
|
f[i] = FileFilter{}
|
|
|
|
}
|
2021-04-13 08:28:26 -04:00
|
|
|
}
|
|
|
|
|
2021-06-16 08:34:50 -04:00
|
|
|
// macOS types may be specified as extension strings without the leading period,
|
|
|
|
// or as uniform type identifiers:
|
|
|
|
// https://developer.apple.com/library/archive/documentation/LanguagesUtilities/Conceptual/MacAutomationScriptingGuide/PromptforaFileorFolder.html
|
|
|
|
//
|
|
|
|
// First check for uniform type identifiers.
|
|
|
|
// Then we extract the extension from each pattern, remove character classes, then escaping.
|
2021-04-13 08:28:26 -04:00
|
|
|
// If an extension contains a wildcard, any type is accepted.
|
|
|
|
func (f FileFilters) types() []string {
|
2021-04-11 22:59:08 -04:00
|
|
|
var res []string
|
|
|
|
for _, filter := range f {
|
|
|
|
for _, pattern := range filter.Patterns {
|
2021-06-16 08:34:50 -04:00
|
|
|
if isUniformTypeIdentifier(pattern) {
|
|
|
|
res = append(res, pattern)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2021-04-11 22:59:08 -04:00
|
|
|
ext := pattern[strings.LastIndexByte(pattern, '.')+1:]
|
|
|
|
|
|
|
|
var escape bool
|
|
|
|
var buf strings.Builder
|
|
|
|
for _, r := range removeClasses(ext) {
|
|
|
|
switch {
|
|
|
|
case escape:
|
|
|
|
escape = false
|
|
|
|
case r == '\\':
|
|
|
|
escape = true
|
|
|
|
continue
|
|
|
|
case r == '*' || r == '?':
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
buf.WriteRune(r)
|
|
|
|
}
|
2021-06-16 08:34:50 -04:00
|
|
|
if buf.Len() > 0 {
|
|
|
|
res = append(res, buf.String())
|
|
|
|
}
|
2021-04-11 22:59:08 -04:00
|
|
|
}
|
|
|
|
}
|
2021-06-16 08:34:50 -04:00
|
|
|
if res == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
// Workaround for macOS bug: first type cannot be a four letter extension, so prepend empty string.
|
|
|
|
return append([]string{""}, res...)
|
2021-04-11 22:59:08 -04:00
|
|
|
}
|
|
|
|
|
2021-04-13 08:28:26 -04:00
|
|
|
// Remove character classes from pattern, assuming case insensitivity.
|
|
|
|
// Classes of one character (case insensitive) are replaced by the character.
|
|
|
|
// Others are replaced by the single character wildcard.
|
2021-04-11 22:59:08 -04:00
|
|
|
func removeClasses(pattern string) string {
|
|
|
|
var res strings.Builder
|
|
|
|
for {
|
|
|
|
i, j := findClass(pattern)
|
|
|
|
if i < 0 {
|
|
|
|
res.WriteString(pattern)
|
|
|
|
return res.String()
|
|
|
|
}
|
|
|
|
res.WriteString(pattern[:i])
|
|
|
|
|
|
|
|
var char string
|
|
|
|
var escape, many bool
|
|
|
|
for _, r := range pattern[i+1 : j-1] {
|
|
|
|
if escape {
|
|
|
|
escape = false
|
|
|
|
} else if r == '\\' {
|
|
|
|
escape = true
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if char == "" {
|
|
|
|
char = string(r)
|
|
|
|
} else if !strings.EqualFold(char, string(r)) {
|
|
|
|
many = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if many {
|
|
|
|
res.WriteByte('?')
|
|
|
|
} else {
|
|
|
|
res.WriteByte('\\')
|
|
|
|
res.WriteString(char)
|
|
|
|
}
|
|
|
|
pattern = pattern[j:]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func findClass(pattern string) (start, end int) {
|
|
|
|
start = -1
|
|
|
|
escape := false
|
|
|
|
for i, b := range []byte(pattern) {
|
|
|
|
switch {
|
|
|
|
case escape:
|
|
|
|
escape = false
|
|
|
|
case b == '\\':
|
|
|
|
escape = true
|
|
|
|
case start < 0:
|
|
|
|
if b == '[' {
|
|
|
|
start = i
|
|
|
|
}
|
|
|
|
case 0 <= start && start < i-1:
|
|
|
|
if b == ']' {
|
|
|
|
return start, i + 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return -1, -1
|
|
|
|
}
|
|
|
|
|
2021-06-16 08:34:50 -04:00
|
|
|
// Uniform type identifiers use the reverse-DNS format:
|
|
|
|
// https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/understanding_utis/understand_utis_conc/understand_utis_conc.html
|
|
|
|
func isUniformTypeIdentifier(pattern string) bool {
|
|
|
|
labels := strings.Split(pattern, ".")
|
|
|
|
if len(labels) < 2 {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, label := range labels {
|
|
|
|
if len := len(label); len == 0 || label[0] == '-' || label[len-1] == '-' {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for _, r := range label {
|
|
|
|
switch {
|
|
|
|
case r == '-' || r > '\x7f' ||
|
|
|
|
'a' <= r && r <= 'z' ||
|
|
|
|
'A' <= r && r <= 'Z' ||
|
|
|
|
'0' <= r && r <= '9':
|
|
|
|
continue
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2020-01-09 12:05:43 -05:00
|
|
|
func splitDirAndName(path string) (dir, name string) {
|
2021-02-18 08:24:02 -05:00
|
|
|
if path != "" {
|
|
|
|
fi, err := os.Stat(path)
|
|
|
|
if err == nil && fi.IsDir() {
|
|
|
|
return path, ""
|
|
|
|
}
|
2020-01-09 12:05:43 -05:00
|
|
|
}
|
|
|
|
return filepath.Split(path)
|
|
|
|
}
|