File filters (windows).

This commit is contained in:
Nuno Cruces 2021-04-13 13:28:26 +01:00
parent 6a81d17a04
commit 2bb3f4747e
5 changed files with 89 additions and 25 deletions

43
file.go
View file

@ -85,10 +85,42 @@ func (f FileFilters) apply(o *options) {
o.fileFilters = append(o.fileFilters, f...) o.fileFilters = append(o.fileFilters, f...)
} }
// macOS filters by case insensitive literal extension. // Windows' patterns are case insensitive, don't support character classes or escaping.
// Extract all literal extensions from all patterns. // First we remove character classes, then escaping. Patterns with literal wildcards are invalid (match nothing).
// If those contain wildcards, or classes with more than one character, accept anything. // The semicolon is a separator, so we replace it with the single character wildcard.
func (f FileFilters) darwin() []string { func (f FileFilters) simplify() {
for _, filter := range f {
for i, pattern := range filter.Patterns {
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
}
if invalid {
filter.Patterns[i] = ""
} else {
filter.Patterns[i] = buf.String()
}
}
}
}
// macOS filters by "type"; the case insensitive literal extension is a good proxy.
// So we extract the extension from each pattern, remove character classes, then escaping.
// If an extension contains a wildcard, any type is accepted.
func (f FileFilters) types() []string {
var res []string var res []string
for _, filter := range f { for _, filter := range f {
for _, pattern := range filter.Patterns { for _, pattern := range filter.Patterns {
@ -114,6 +146,9 @@ func (f FileFilters) darwin() []string {
return res return res
} }
// 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.
func removeClasses(pattern string) string { func removeClasses(pattern string) string {
var res strings.Builder var res strings.Builder
for { for {

View file

@ -14,7 +14,7 @@ func selectFile(opts options) (string, error) {
data.Operation = "chooseFolder" data.Operation = "chooseFolder"
} else { } else {
data.Operation = "chooseFile" data.Operation = "chooseFile"
data.Options.Type = opts.fileFilters.darwin() data.Options.Type = opts.fileFilters.types()
} }
out, err := zenutil.Run(opts.ctx, "file", data) out, err := zenutil.Run(opts.ctx, "file", data)
@ -34,7 +34,7 @@ func selectFileMutiple(opts options) ([]string, error) {
data.Operation = "chooseFolder" data.Operation = "chooseFolder"
} else { } else {
data.Operation = "chooseFile" data.Operation = "chooseFile"
data.Options.Type = opts.fileFilters.darwin() data.Options.Type = opts.fileFilters.types()
} }
out, err := zenutil.Run(opts.ctx, "file", data) out, err := zenutil.Run(opts.ctx, "file", data)

View file

@ -5,27 +5,55 @@ import (
"testing" "testing"
) )
func TestFileFilters_darwin(t *testing.T) { func TestFileFilters_simplify(t *testing.T) {
tests := []struct { tests := []struct {
data FileFilters data FileFilters
want []string want []string
}{ }{
{FileFilters{{"", []string{`*.png`}}}, []string{`png`}}, {FileFilters{{"", []string{`*.png`}}}, []string{"*.png"}},
{FileFilters{{"", []string{`*.pn?`}}}, nil}, {FileFilters{{"", []string{`*.pn?`}}}, []string{"*.pn?"}},
{FileFilters{{"", []string{`*.pn\?`}}}, []string{`pn?`}}, {FileFilters{{"", []string{`*.pn;`}}}, []string{"*.pn?"}},
{FileFilters{{"", []string{`*.[PpNnGg]`}}}, nil}, {FileFilters{{"", []string{`*.pn\?`}}}, []string{""}},
{FileFilters{{"", []string{`*.[Pp][Nn][Gg]`}}}, []string{`PNG`}}, {FileFilters{{"", []string{`*.[PpNnGg]`}}}, []string{"*.?"}},
{FileFilters{{"", []string{`*.[Pp][\Nn][G\g]`}}}, []string{`PNG`}}, {FileFilters{{"", []string{`*.[Pp][Nn][Gg]`}}}, []string{"*.PNG"}},
{FileFilters{{"", []string{`*.[PNG`}}}, []string{`[PNG`}}, {FileFilters{{"", []string{`*.[Pp][\Nn][G\g]`}}}, []string{"*.PNG"}},
{FileFilters{{"", []string{`*.]PNG`}}}, []string{`]PNG`}}, {FileFilters{{"", []string{`*.[PNG`}}}, []string{"*.[PNG"}},
{FileFilters{{"", []string{`*.[[]PNG`}}}, []string{`[PNG`}}, {FileFilters{{"", []string{`*.]PNG`}}}, []string{"*.]PNG"}},
{FileFilters{{"", []string{`*.[]]PNG`}}}, []string{`]PNG`}}, {FileFilters{{"", []string{`*.[[]PNG`}}}, []string{"*.[PNG"}},
{FileFilters{{"", []string{`*.[\[]PNG`}}}, []string{`[PNG`}}, {FileFilters{{"", []string{`*.[]]PNG`}}}, []string{"*.]PNG"}},
{FileFilters{{"", []string{`*.[\]]PNG`}}}, []string{`]PNG`}}, {FileFilters{{"", []string{`*.[\[]PNG`}}}, []string{"*.[PNG"}},
{FileFilters{{"", []string{`*.[\]]PNG`}}}, []string{"*.]PNG"}},
} }
for _, tt := range tests { for i, tt := range tests {
if got := tt.data.darwin(); !reflect.DeepEqual(got, tt.want) { tt.data.simplify()
t.Fatalf("FileFilters.darwin(%+v) = %v, want %v", tt.data, got, tt.want) if got := tt.data[0].Patterns; !reflect.DeepEqual(got, tt.want) {
t.Errorf("FileFilters.simplify[%d] = %q, want %q", i, got, tt.want)
}
}
}
func TestFileFilters_types(t *testing.T) {
tests := []struct {
data FileFilters
want []string
}{
{FileFilters{{"", []string{`*.png`}}}, []string{"png"}},
{FileFilters{{"", []string{`*.pn?`}}}, nil},
{FileFilters{{"", []string{`*.pn;`}}}, []string{"pn;"}},
{FileFilters{{"", []string{`*.pn\?`}}}, []string{"pn?"}},
{FileFilters{{"", []string{`*.[PpNnGg]`}}}, nil},
{FileFilters{{"", []string{`*.[Pp][Nn][Gg]`}}}, []string{"PNG"}},
{FileFilters{{"", []string{`*.[Pp][\Nn][G\g]`}}}, []string{"PNG"}},
{FileFilters{{"", []string{`*.[PNG`}}}, []string{"[PNG"}},
{FileFilters{{"", []string{`*.]PNG`}}}, []string{"]PNG"}},
{FileFilters{{"", []string{`*.[[]PNG`}}}, []string{"[PNG"}},
{FileFilters{{"", []string{`*.[]]PNG`}}}, []string{"]PNG"}},
{FileFilters{{"", []string{`*.[\[]PNG`}}}, []string{"[PNG"}},
{FileFilters{{"", []string{`*.[\]]PNG`}}}, []string{"]PNG"}},
}
for i, tt := range tests {
if got := tt.data.types(); !reflect.DeepEqual(got, tt.want) {
t.Fatalf("FileFilters.types[%d] = %v, want %v", i, got, tt.want)
} }
} }
} }

View file

@ -10,8 +10,8 @@ import (
"github.com/ncruces/zenity" "github.com/ncruces/zenity"
) )
const defaultPath = "" const defaultPath = ``
const defaultName = "" const defaultName = ``
func ExampleSelectFile() { func ExampleSelectFile() {
zenity.SelectFile( zenity.SelectFile(

View file

@ -361,7 +361,8 @@ func initDirNameExt(filename string, name []uint16) (dir *uint16, ext *uint16) {
return return
} }
func initFilters(filters []FileFilter) []uint16 { func initFilters(filters FileFilters) []uint16 {
filters.simplify()
var res []uint16 var res []uint16
for _, f := range filters { for _, f := range filters {
res = append(res, utf16.Encode([]rune(f.Name))...) res = append(res, utf16.Encode([]rune(f.Name))...)