zenity/file.go

343 lines
8.2 KiB
Go
Raw Normal View History

2020-01-09 12:05:43 -05:00
package zenity
import (
"os"
"path/filepath"
2021-04-11 22:59:08 -04:00
"strings"
2022-12-14 22:05:44 -05:00
"unicode"
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))
}
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
//
2022-12-14 22:05:44 -05:00
// On Windows and macOS filtering is always case-insensitive.
//
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
2022-12-14 22:05:44 -05:00
CaseFold bool // if set patterns are matched case-insensitively
2020-01-23 06:44:28 -05:00
}
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
}
2022-12-14 22:05:44 -05:00
// Windows patterns need a name.
2021-04-13 09:37:10 -04:00
func (f FileFilters) name() {
for i, filter := range f {
if filter.Name == "" {
f[i].Name = strings.Join(filter.Patterns, " ")
}
}
}
2022-12-14 22:05:44 -05: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.
func (f FileFilters) simplify() {
2022-12-14 22:05:44 -05:00
for i := range f {
2021-06-16 12:24:35 -04:00
var j = 0
2022-12-14 22:05:44 -05:00
for _, pattern := range f[i].Patterns {
2021-04-13 08:28:26 -04:00
var escape, invalid bool
var buf strings.Builder
2022-12-14 22:05:44 -05:00
for _, b := range []byte(removeClasses(pattern)) {
if !escape && b == '\\' {
2021-04-13 08:28:26 -04:00
escape = true
continue
}
2022-12-14 22:05:44 -05:00
if escape && (b == '*' || b == '?') {
2021-04-13 08:28:26 -04:00
invalid = true
break
}
2022-12-14 22:05:44 -05:00
if b == ';' {
b = '?'
2021-04-13 08:28:26 -04:00
}
2022-12-14 22:05:44 -05:00
buf.WriteByte(b)
2021-04-13 08:28:26 -04:00
escape = false
}
2021-06-16 08:34:50 -04:00
if buf.Len() > 0 && !invalid {
2022-12-14 22:05:44 -05:00
f[i].Patterns[j] = buf.String()
2021-06-16 12:24:35 -04:00
j++
2021-04-13 08:28:26 -04:00
}
}
2022-12-14 22:05:44 -05:00
if j != 0 {
f[i].Patterns = f[i].Patterns[:j]
} else {
f[i].Patterns = nil
2021-06-16 08:34:50 -04:00
}
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
2022-12-14 22:05:44 -05:00
for _, b := range []byte(removeClasses(ext)) {
2021-04-11 22:59:08 -04:00
switch {
case escape:
escape = false
2022-12-14 22:05:44 -05:00
case b == '\\':
2021-04-11 22:59:08 -04:00
escape = true
continue
2022-12-14 22:05:44 -05:00
case b == '*' || b == '?':
2021-04-11 22:59:08 -04:00
return nil
}
2022-12-14 22:05:44 -05:00
buf.WriteByte(b)
2021-04-11 22:59:08 -04:00
}
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
}
2022-12-14 22:05:44 -05:00
// Unix patterns are case-sensitive. Fold them if requested.
func (f FileFilters) casefold() {
for i := range f {
if !f[i].CaseFold {
continue
}
for j, pattern := range f[i].Patterns {
var class = -1
var escape bool
var buf strings.Builder
for i, r := range pattern {
switch {
case escape:
escape = false
case r == '\\':
escape = true
case class < 0:
if r == '[' {
class = i
}
case class < i-1:
if r == ']' {
class = -1
}
}
nr := unicode.SimpleFold(r)
if r == nr {
buf.WriteRune(r)
continue
}
if class < 0 {
buf.WriteByte('[')
}
buf.WriteRune(r)
for r != nr {
buf.WriteRune(nr)
nr = unicode.SimpleFold(nr)
}
if class < 0 {
buf.WriteByte(']')
}
}
f[i].Patterns[j] = buf.String()
}
}
}
2021-04-13 08:28:26 -04:00
// Remove character classes from pattern, assuming case insensitivity.
2022-12-14 22:05:44 -05:00
// Classes of one character (case-insensitive) are replaced by the character.
2021-04-13 08:28:26 -04:00
// 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:]
}
}
2022-12-07 07:49:03 -05:00
// Find a character class in the pattern.
2021-04-11 22:59:08 -04:00
func findClass(pattern string) (start, end int) {
start = -1
2022-12-14 22:05:44 -05:00
var escape bool
2021-04-11 22:59:08 -04:00
for i, b := range []byte(pattern) {
switch {
case escape:
escape = false
case b == '\\':
escape = true
case start < 0:
if b == '[' {
start = i
}
2022-12-14 22:05:44 -05:00
case start < i-1:
2021-04-11 22:59:08 -04:00
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
}
2023-04-12 06:22:30 -04:00
func splitDirAndName(path string) (dir, name string, err error) {
if path == "" {
return "", "", nil
}
fi, err := os.Stat(path)
if err == nil && fi.IsDir() {
return path, "", nil
2020-01-09 12:05:43 -05:00
}
2023-04-12 06:22:30 -04:00
dir, name = filepath.Split(path)
2023-04-22 05:40:23 -04:00
if dir == "" {
return "", name, nil
}
2023-04-12 06:22:30 -04:00
_, err = os.Stat(dir)
return dir, name, err
2020-01-09 12:05:43 -05:00
}