mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-21 07:55:10 +02:00
Work on filter support
This commit is contained in:
78
kittens/choose_files/filters.go
Normal file
78
kittens/choose_files/filters.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package choose_files
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
type Filter struct {
|
||||
Name, Type, Pattern string
|
||||
Match func(filename string) bool
|
||||
}
|
||||
|
||||
func (f Filter) String() string {
|
||||
return fmt.Sprintf("%s:%s:%s", f.Type, f.Pattern, f.Name)
|
||||
}
|
||||
|
||||
func (f Filter) Equal(other Filter) bool {
|
||||
return f.Type == other.Type && f.Pattern == other.Pattern
|
||||
}
|
||||
|
||||
func NewFilter(spec string) (*Filter, error) {
|
||||
parts := strings.SplitN(spec, ":", 3)
|
||||
if len(parts) != 3 {
|
||||
return nil, fmt.Errorf("%#v is not a valid filter specifier, must have at least two colons", spec)
|
||||
}
|
||||
ans := &Filter{Name: parts[2], Pattern: parts[1], Type: parts[0]}
|
||||
if _, err := filepath.Match(ans.Pattern, "test"); err != nil {
|
||||
return nil, fmt.Errorf("%#v is not a valid glob pattern with error: %w", ans.Pattern, err)
|
||||
}
|
||||
if ans.Pattern != "*" && ans.Pattern != "" {
|
||||
switch ans.Type {
|
||||
case "glob":
|
||||
ans.Match = func(filename string) bool {
|
||||
m, _ := filepath.Match(ans.Pattern, filename)
|
||||
return m
|
||||
}
|
||||
case "mime":
|
||||
ans.Match = func(filename string) bool {
|
||||
mime := utils.GuessMimeType(filename)
|
||||
if mime == "" {
|
||||
return false
|
||||
}
|
||||
m, _ := filepath.Match(ans.Pattern, mime)
|
||||
return m
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("%#v is not a valid filter type", ans.Type)
|
||||
}
|
||||
}
|
||||
return ans, nil
|
||||
}
|
||||
|
||||
func CombinedFilter(filters ...Filter) Filter {
|
||||
if len(filters) == 0 {
|
||||
return Filter{}
|
||||
}
|
||||
for _, f := range filters {
|
||||
if f.Match == nil {
|
||||
return f
|
||||
}
|
||||
}
|
||||
ans := filters[0]
|
||||
matchers := utils.Map(func(f Filter) func(filename string) bool { return f.Match }, filters)
|
||||
ans.Match = func(filename string) bool {
|
||||
for _, m := range matchers {
|
||||
if m(filename) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
return ans
|
||||
}
|
||||
49
kittens/choose_files/footer.go
Normal file
49
kittens/choose_files/footer.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package choose_files
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
"github.com/kovidgoyal/kitty/tools/utils/style"
|
||||
"github.com/kovidgoyal/kitty/tools/wcswidth"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
func (h *Handler) draw_footer() (num_lines int, err error) {
|
||||
lines := []string{}
|
||||
screen_width := h.screen_size.width
|
||||
sctx := style.Context{AllowEscapeCodes: true}
|
||||
if len(h.state.filter_map) > 0 {
|
||||
buf := strings.Builder{}
|
||||
pos := 0
|
||||
current_style := sctx.SprintFunc("italic fg=green intense")
|
||||
non_current_style := sctx.SprintFunc("dim")
|
||||
w := func(text string, sfunc func(...any) string) {
|
||||
sz := wcswidth.Stringwidth(text)
|
||||
if sz+pos >= screen_width {
|
||||
lines = append(lines, buf.String())
|
||||
pos = 0
|
||||
buf.Reset()
|
||||
}
|
||||
if sfunc != nil {
|
||||
text = sfunc(text)
|
||||
}
|
||||
buf.WriteString(text)
|
||||
pos += sz
|
||||
}
|
||||
w("Filter:", nil)
|
||||
for _, name := range h.state.filter_names {
|
||||
w(" "+name, utils.IfElse(name == h.state.current_filter, current_style, non_current_style))
|
||||
}
|
||||
if s := buf.String(); s != "" {
|
||||
lines = append(lines, s)
|
||||
}
|
||||
}
|
||||
if len(lines) > 0 {
|
||||
h.lp.MoveCursorTo(1, h.screen_size.height-len(lines)+1)
|
||||
h.lp.QueueWriteString(strings.Join(lines, "\r\n"))
|
||||
}
|
||||
return len(lines), err
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
)
|
||||
|
||||
// TODO: Comboboxes, multifile selections, save file name, file/dir modes
|
||||
// TODO: multifile selections, save file name completion
|
||||
|
||||
var _ = fmt.Print
|
||||
var debugprintln = tty.DebugPrintln
|
||||
@@ -108,6 +108,9 @@ type State struct {
|
||||
suggested_save_file_name string
|
||||
window_title string
|
||||
screen Screen
|
||||
current_filter string
|
||||
filter_map map[string]Filter
|
||||
filter_names []string
|
||||
|
||||
save_file_cdir string
|
||||
selections []string
|
||||
@@ -116,6 +119,7 @@ type State struct {
|
||||
}
|
||||
|
||||
func (s State) BaseDir() string { return utils.IfElse(s.base_dir == "", default_cwd, s.base_dir) }
|
||||
func (s State) Filter() Filter { return s.filter_map[s.current_filter] }
|
||||
func (s State) SelectDirs() bool { return s.select_dirs }
|
||||
func (s State) Multiselect() bool { return s.multiselect }
|
||||
func (s State) String() string { return utils.Repr(s) }
|
||||
@@ -190,7 +194,11 @@ func (h *Handler) draw_screen() (err error) {
|
||||
h.draw_search_bar(0)
|
||||
}()
|
||||
y := SEARCH_BAR_HEIGHT
|
||||
y += h.draw_results(y, 2, matches, !is_complete)
|
||||
footer_height, err := h.draw_footer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
y += h.draw_results(y, footer_height, matches, !is_complete)
|
||||
case SAVE_FILE:
|
||||
err = h.draw_save_file_name_screen()
|
||||
}
|
||||
@@ -227,7 +235,7 @@ func (h *Handler) OnInitialize() (ans string, err error) {
|
||||
h.lp.AllowLineWrapping(false)
|
||||
h.lp.SetCursorShape(loop.BAR_CURSOR, true)
|
||||
h.lp.StartBracketedPaste()
|
||||
h.result_manager.set_root_dir(h.state.CurrentDir())
|
||||
h.result_manager.set_root_dir(h.state.CurrentDir(), h.state.Filter())
|
||||
h.draw_screen()
|
||||
return
|
||||
}
|
||||
@@ -261,7 +269,7 @@ func (h *Handler) toggle_selection() bool {
|
||||
func (h *Handler) change_current_dir(dir string) {
|
||||
if dir != h.state.CurrentDir() {
|
||||
h.state.SetCurrentDir(dir)
|
||||
h.result_manager.set_root_dir(h.state.CurrentDir())
|
||||
h.result_manager.set_root_dir(h.state.CurrentDir(), h.state.Filter())
|
||||
h.state.last_render = render_state{}
|
||||
}
|
||||
}
|
||||
@@ -269,7 +277,15 @@ func (h *Handler) change_current_dir(dir string) {
|
||||
func (h *Handler) set_query(q string) {
|
||||
if q != h.state.SearchText() {
|
||||
h.state.SetSearchText(q)
|
||||
h.result_manager.set_query(h.state.SearchText())
|
||||
h.result_manager.set_query(h.state.SearchText(), h.state.Filter())
|
||||
h.state.last_render = render_state{}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) set_filter(filter_name string) {
|
||||
if filter_name != h.state.current_filter {
|
||||
h.state.current_filter = filter_name
|
||||
h.result_manager.set_filter(h.state.Filter())
|
||||
h.state.last_render = render_state{}
|
||||
}
|
||||
}
|
||||
@@ -411,6 +427,41 @@ func (h *Handler) set_state_from_config(_ *Config, opts *Options) (err error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
h.state.filter_map = nil
|
||||
h.state.current_filter = ""
|
||||
if len(opts.FileFilter) > 0 {
|
||||
has_all_files := false
|
||||
fmap := make(map[string][]Filter)
|
||||
seen := utils.NewSet[string](len(opts.FileFilter))
|
||||
for _, x := range opts.FileFilter {
|
||||
f, ferr := NewFilter(x)
|
||||
if ferr != nil {
|
||||
return ferr
|
||||
}
|
||||
if f.Match == nil {
|
||||
has_all_files = true
|
||||
}
|
||||
if h.state.current_filter == "" {
|
||||
h.state.current_filter = f.Name
|
||||
}
|
||||
fmap[f.Name] = append(fmap[f.Name], *f)
|
||||
if !seen.Has(f.Name) {
|
||||
seen.Add(f.Name)
|
||||
h.state.filter_names = append(h.state.filter_names, f.Name)
|
||||
}
|
||||
}
|
||||
if !has_all_files {
|
||||
af, _ := NewFilter("glob:*:All files")
|
||||
fmap[af.Name] = append(fmap[af.Name], *af)
|
||||
if !seen.Has(af.Name) {
|
||||
h.state.filter_names = append(h.state.filter_names, af.Name)
|
||||
}
|
||||
}
|
||||
h.state.filter_map = make(map[string]Filter)
|
||||
for name, filters := range fmap {
|
||||
h.state.filter_map[name] = CombinedFilter(filters...)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,16 @@ default=file
|
||||
The type of object(s) to select
|
||||
|
||||
|
||||
--file-filter
|
||||
type=list
|
||||
A list of filters to restrict the displayed files. Can be either mimetypes, or glob style patterns. Can be specified multiple times.
|
||||
The syntax is :code:`type:expression:Descriptive Name`.
|
||||
For example: :code:`mime:image/png:Images` and :code:`mime:image/gif:Images` and :code:`glob:*.[tT][xX][Tt]:Text files`.
|
||||
Note that glob patterns are case-sensitive. The mimetype specification is treated as a glob expressions as well, so you can,
|
||||
for example, use :code:`mime:text/*` to match all text files. The first filter in the list will be applied by default. Use a filter
|
||||
such as :code:`glob:*:All` to match all files. Note that filtering only appies to files, not directories.
|
||||
|
||||
|
||||
--suggested-save-file-name
|
||||
A suggested name when picking a save file.
|
||||
|
||||
|
||||
@@ -278,6 +278,7 @@ type FileSystemScorer struct {
|
||||
scanner Scanner
|
||||
keep_going, is_complete atomic.Bool
|
||||
root_dir, query string
|
||||
filter Filter
|
||||
only_dirs bool
|
||||
mutex sync.Mutex
|
||||
sorted_results *SortedResults
|
||||
@@ -286,10 +287,11 @@ type FileSystemScorer struct {
|
||||
scorer *fzf.FuzzyMatcher
|
||||
}
|
||||
|
||||
func NewFileSystemScorer(root_dir, query string, only_dirs bool, on_results func(error, bool)) (ans *FileSystemScorer) {
|
||||
func NewFileSystemScorer(root_dir, query string, filter Filter, only_dirs bool, on_results func(error, bool)) (ans *FileSystemScorer) {
|
||||
return &FileSystemScorer{
|
||||
query: query, root_dir: root_dir, only_dirs: only_dirs, on_results: on_results,
|
||||
scorer: fzf.NewFuzzyMatcher(fzf.PATH_SCHEME), sorted_results: NewSortedResults()}
|
||||
query: query, root_dir: root_dir, only_dirs: only_dirs, filter: filter, on_results: on_results,
|
||||
scorer: fzf.NewFuzzyMatcher(fzf.PATH_SCHEME), sorted_results: NewSortedResults(),
|
||||
}
|
||||
}
|
||||
|
||||
func (fss *FileSystemScorer) lock() { fss.mutex.Lock() }
|
||||
@@ -325,6 +327,21 @@ func (fss *FileSystemScorer) Change_query(query string) {
|
||||
fss.Start()
|
||||
}
|
||||
|
||||
func (fss *FileSystemScorer) Change_filter(filter Filter) {
|
||||
if fss.filter.Equal(filter) {
|
||||
return
|
||||
}
|
||||
fss.keep_going.Store(false)
|
||||
if fss.current_worker_wait != nil {
|
||||
fss.current_worker_wait.Wait()
|
||||
}
|
||||
fss.lock()
|
||||
fss.filter = filter
|
||||
fss.sorted_results.Clear()
|
||||
fss.unlock()
|
||||
fss.Start()
|
||||
}
|
||||
|
||||
func (fss *FileSystemScorer) worker(on_results chan bool, worker_wait *sync.WaitGroup) {
|
||||
defer func() {
|
||||
fss.is_complete.Store(true)
|
||||
@@ -353,9 +370,18 @@ func (fss *FileSystemScorer) worker(on_results chan bool, worker_wait *sync.Wait
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rp = make([]*ResultItem, len(results))
|
||||
for i := range len(rp) {
|
||||
rp[i] = &results[i]
|
||||
if fss.filter.Match == nil {
|
||||
rp = make([]*ResultItem, len(results))
|
||||
for i := range len(rp) {
|
||||
rp[i] = &results[i]
|
||||
}
|
||||
} else {
|
||||
rp = make([]*ResultItem, 0, len(results))
|
||||
for i, r := range results {
|
||||
if r.ftype.IsDir() || fss.filter.Match(filepath.Base(r.text)) {
|
||||
rp = append(rp, &results[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(rp) > 0 {
|
||||
@@ -452,7 +478,7 @@ func (m *ResultManager) on_results(err error, is_finished bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ResultManager) set_root_dir(root_dir string) {
|
||||
func (m *ResultManager) set_root_dir(root_dir string, filter Filter) {
|
||||
var err error
|
||||
if root_dir == "" || root_dir == "." {
|
||||
if root_dir, err = os.Getwd(); err != nil {
|
||||
@@ -466,25 +492,38 @@ func (m *ResultManager) set_root_dir(root_dir string) {
|
||||
if m.scorer != nil {
|
||||
m.scorer.Cancel()
|
||||
}
|
||||
m.scorer = NewFileSystemScorer(root_dir, "", m.settings.OnlyDirs(), m.on_results)
|
||||
_ = os.Chdir(root_dir) // this is so the terminal emulator can read the wd for launch --directory=current
|
||||
m.scorer = NewFileSystemScorer(root_dir, "", filter, m.settings.OnlyDirs(), m.on_results)
|
||||
m.mutex.Lock()
|
||||
m.last_wakeup_at = time.Time{}
|
||||
m.mutex.Unlock()
|
||||
m.scorer.Start()
|
||||
}
|
||||
|
||||
func (m *ResultManager) set_query(query string) {
|
||||
func (m *ResultManager) set_query(query string, filter Filter) {
|
||||
m.mutex.Lock()
|
||||
m.last_wakeup_at = time.Time{}
|
||||
m.mutex.Unlock()
|
||||
if m.scorer == nil {
|
||||
m.scorer = NewFileSystemScorer(".", "", m.settings.OnlyDirs(), m.on_results)
|
||||
m.scorer = NewFileSystemScorer(".", "", filter, m.settings.OnlyDirs(), m.on_results)
|
||||
m.scorer.Start()
|
||||
} else {
|
||||
m.scorer.Change_query(query)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ResultManager) set_filter(f Filter) {
|
||||
m.mutex.Lock()
|
||||
m.last_wakeup_at = time.Time{}
|
||||
m.mutex.Unlock()
|
||||
if m.scorer == nil {
|
||||
m.scorer = NewFileSystemScorer(".", "", f, m.settings.OnlyDirs(), m.on_results)
|
||||
m.scorer.Start()
|
||||
} else {
|
||||
m.scorer.Change_filter(f)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) get_results() (ans *SortedResults, is_complete bool) {
|
||||
if h.result_manager.scorer == nil {
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user