Add a bunch of settings to control filesystem scanning

This commit is contained in:
Kovid Goyal
2025-07-10 10:30:48 +05:30
parent 27315d6496
commit 6f511ae66a
4 changed files with 199 additions and 47 deletions

View File

@@ -9,9 +9,11 @@ import (
"slices"
"strconv"
"strings"
"sync"
"github.com/kovidgoyal/kitty/tools/cli"
"github.com/kovidgoyal/kitty/tools/config"
"github.com/kovidgoyal/kitty/tools/ignorefiles"
"github.com/kovidgoyal/kitty/tools/tty"
"github.com/kovidgoyal/kitty/tools/tui"
"github.com/kovidgoyal/kitty/tools/tui/loop"
@@ -112,6 +114,10 @@ type State struct {
current_filter string
filter_map map[string]Filter
filter_names []string
show_hidden bool
respect_ignores bool
sort_by_last_modified bool
global_ignores ignorefiles.IgnoreFile
save_file_cdir string
selections []string
@@ -121,13 +127,17 @@ type State struct {
redraw_needed bool
}
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) }
func (s State) SearchText() string { return s.search_text }
func (s State) OnlyDirs() bool { return s.mode.OnlyDirs() }
func (s State) ShowHidden() bool { return s.show_hidden }
func (s State) RespectIgnores() bool { return s.respect_ignores }
func (s State) SortByLastModified() bool { return s.sort_by_last_modified }
func (s State) GlobalIgnores() ignorefiles.IgnoreFile { return s.global_ignores }
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) }
func (s State) SearchText() string { return s.search_text }
func (s State) OnlyDirs() bool { return s.mode.OnlyDirs() }
func (s *State) SetSearchText(val string) {
if s.search_text != val {
s.search_text = val
@@ -244,7 +254,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.state.Filter())
h.result_manager.set_root_dir()
h.draw_screen()
return
}
@@ -278,7 +288,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.state.Filter())
h.result_manager.set_root_dir()
h.state.last_render = render_state{}
}
}
@@ -286,7 +296,7 @@ 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.state.Filter())
h.result_manager.set_query()
h.state.last_render = render_state{}
}
}
@@ -294,7 +304,7 @@ func (h *Handler) set_query(q string) {
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.result_manager.set_filter()
h.state.last_render = render_state{}
}
}
@@ -435,7 +445,32 @@ func (h *Handler) OnText(text string, from_key_event, in_bracketed_paste bool) (
return
}
func (h *Handler) set_state_from_config(_ *Config, opts *Options) (err error) {
type CachedValues struct {
Show_hidden bool `json:"show_hidden"`
Respect_ignores bool `json:"respect_ignores"`
Sort_by_last_modified bool `json:"sort_by_last_modified"`
}
const cache_filename = "choose-files.json"
var cached_values = sync.OnceValue(func() *CachedValues {
ans := CachedValues{Respect_ignores: true}
fname := filepath.Join(utils.CacheDir(), cache_filename)
if data, err := os.ReadFile(fname); err == nil {
_ = json.Unmarshal(data, &ans)
}
return &ans
})
func (s State) save_cached_values() {
c := CachedValues{Show_hidden: s.show_hidden, Respect_ignores: s.respect_ignores, Sort_by_last_modified: s.sort_by_last_modified}
fname := filepath.Join(utils.CacheDir(), cache_filename)
if data, err := json.Marshal(c); err == nil {
_ = os.WriteFile(fname, data, 0600)
}
}
func (h *Handler) set_state_from_config(conf *Config, opts *Options) (err error) {
h.state = State{}
switch opts.Mode {
case "file":
@@ -504,6 +539,37 @@ func (h *Handler) set_state_from_config(_ *Config, opts *Options) (err error) {
h.state.filter_map[name] = CombinedFilter(filters...)
}
}
h.state.sort_by_last_modified = false
h.state.respect_ignores = true
h.state.show_hidden = false
switch conf.Show_hidden {
case Show_hidden_true, Show_hidden_y, Show_hidden_yes:
h.state.show_hidden = true
case Show_hidden_false, Show_hidden_n, Show_hidden_no:
h.state.show_hidden = false
case Show_hidden_last:
h.state.show_hidden = cached_values().Show_hidden
}
switch conf.Respect_ignores {
case Respect_ignores_true, Respect_ignores_y, Respect_ignores_yes:
h.state.respect_ignores = true
case Respect_ignores_false, Respect_ignores_n, Respect_ignores_no:
h.state.respect_ignores = false
case Respect_ignores_last:
h.state.respect_ignores = cached_values().Respect_ignores
}
switch conf.Sort_by_last_modified {
case Sort_by_last_modified_true, Sort_by_last_modified_y, Sort_by_last_modified_yes:
h.state.sort_by_last_modified = true
case Sort_by_last_modified_false, Sort_by_last_modified_n, Sort_by_last_modified_no:
h.state.sort_by_last_modified = false
case Sort_by_last_modified_last:
h.state.sort_by_last_modified = cached_values().Sort_by_last_modified
}
h.state.global_ignores = ignorefiles.NewGitignore()
if err = h.state.global_ignores.LoadLines(conf.Ignore...); err != nil {
return err
}
return
}
@@ -606,6 +672,7 @@ func main(_ *cli.Command, opts *Options, args []string) (rc int, err error) {
return
}
err = lp.Run()
handler.state.save_cached_values()
if err != nil {
write_output(nil, false, "")
return 1, err

View File

@@ -17,6 +17,32 @@ opt = definition.add_option
map = definition.add_map
mma = definition.add_mouse_map
agr('Filesystem scanning')
opt('show_hidden', 'last', choices=('last', 'yes', 'y', 'true', 'no', 'n', 'false'), long_text='''
Whether to show hidden files. The default value of :code:`last` means remember the last
used value. This setting can be toggled withing the program.''')
opt('sort_by_last_modified', 'last', choices=('last', 'yes', 'y', 'true', 'no', 'n', 'false'), long_text='''
Whether to sort the list of entries by last modified, instead of name. Note that sorting only applies
before any query is entered. Once a query is entered entries are sorted by their matching score.
The default value of :code:`last` means remember the last
used value. This setting can be toggled withing the program.''')
opt('respect_ignores', 'last', choices=('last', 'yes', 'y', 'true', 'no', 'n', 'false'), long_text='''
Whether to respect .gitignore and .ignore files and the :opt:`ignore` setting.
The default value of :code:`last` means remember the last used value.
This setting can be toggled withing the program.''')
opt('+ignore', '', add_to_default=False, long_text='''
An ignore pattern to ignore matched files. Uses the same sytax as :code:`.gitignore` files (see :code:`man gitignore`).
Anchored patterns match with respect to whatever directory is currently being displayed.
Can be specified multiple times to use multiple patterns. Note that every pattern
has to be checked against every file, so use sparingly.
''')
egr()
def main(args: list[str]) -> None:
raise SystemExit('This must be run as kitten choose-files')

View File

@@ -3,6 +3,7 @@ package choose_files
import (
"bytes"
"cmp"
"encoding/binary"
"fmt"
"io/fs"
"math"
@@ -79,7 +80,9 @@ type FileSystemScanner struct {
file_reader func(path string) ([]byte, error)
filter_func func(filename string) bool
global_gitignore ignorefiles.IgnoreFile
global_ignore ignorefiles.IgnoreFile
respect_ignores, show_hidden bool
sort_by_last_modified bool
err error
}
@@ -92,6 +95,7 @@ func new_filesystem_scanner(root_dir string, notify chan bool, filter_func func(
ans.file_reader = os.ReadFile
ans.filter_func = utils.IfElse(filter_func == nil, accept_all, filter_func)
ans.global_gitignore = ignorefiles.NewGitignore()
ans.global_ignore = ignorefiles.NewGitignore()
ans.respect_ignores = true
ans.show_hidden = false
return ans
@@ -326,11 +330,23 @@ func (fss *FileSystemScanner) worker() {
} else {
arena[i].buf[0] = '1'
}
n := as_lower(arena[i].name, arena[i].buf[1:])
arena[i].sort_key = arena[i].buf[:1+n]
if fss.sort_by_last_modified {
var ts time.Time
if info, err := e.Info(); err == nil {
ts = info.ModTime()
}
binary.BigEndian.PutUint64(arena[i].buf[1:], uint64(ts.UnixNano()))
arena[i].sort_key = arena[i].buf[:1+8]
} else {
n := as_lower(arena[i].name, arena[i].buf[1:])
arena[i].sort_key = arena[i].buf[:1+n]
}
sortable = append(sortable, &arena[i])
}
if fss.respect_ignores {
if is_root && fss.global_ignore.Len() > 0 {
add_ignore_file_from_impl(fss.global_ignore)
}
if has_dot_git {
if fss.global_gitignore.Len() > 0 {
add_ignore_file_from_impl(fss.global_gitignore)
@@ -390,20 +406,21 @@ func (fss *FileSystemScanner) worker() {
}
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
on_results func(error, bool)
current_worker_wait *sync.WaitGroup
scorer *fzf.FuzzyMatcher
dir_reader func(path string) ([]fs.DirEntry, error)
file_reader func(path string) ([]byte, error)
global_gitignore ignorefiles.IgnoreFile
respect_ignores, show_hidden bool
scanner Scanner
keep_going, is_complete atomic.Bool
root_dir, query string
filter Filter
only_dirs bool
mutex sync.Mutex
sorted_results *SortedResults
on_results func(error, bool)
current_worker_wait *sync.WaitGroup
scorer *fzf.FuzzyMatcher
dir_reader func(path string) ([]fs.DirEntry, error)
file_reader func(path string) ([]byte, error)
global_gitignore, global_ignore ignorefiles.IgnoreFile
respect_ignores, show_hidden bool
sort_by_last_modified bool
}
func NewFileSystemScorer(root_dir, query string, filter Filter, only_dirs bool, on_results func(error, bool)) (ans *FileSystemScorer) {
@@ -433,7 +450,11 @@ func (fss *FileSystemScorer) Start() {
} else {
sc.global_gitignore = ignorefiles.GlobalGitignore()
}
if fss.global_ignore != nil {
sc.global_ignore = fss.global_ignore
}
sc.show_hidden, sc.respect_ignores = fss.show_hidden, fss.respect_ignores
sc.sort_by_last_modified = fss.sort_by_last_modified
fss.scanner = sc
fss.scanner.Start()
} else {
@@ -497,6 +518,12 @@ func (fss *FileSystemScorer) Change_respect_ignores(val bool) {
}
}
func (fss *FileSystemScorer) Change_sort_by_last_modified(val bool) {
if fss.sort_by_last_modified != val {
fss.change_scanner_setting(func() { fss.sort_by_last_modified = val })
}
}
func (fss *FileSystemScorer) worker(on_results chan bool, worker_wait *sync.WaitGroup) {
defer func() {
fss.is_complete.Store(true)
@@ -598,6 +625,11 @@ type Settings interface {
OnlyDirs() bool
CurrentDir() string
SearchText() string
ShowHidden() bool
RespectIgnores() bool
SortByLastModified() bool
Filter() Filter
GlobalIgnores() ignorefiles.IgnoreFile
}
type ResultManager struct {
@@ -619,6 +651,15 @@ func NewResultManager(err_chan chan error, settings Settings, WakeupMainThread f
return ans
}
func (m *ResultManager) new_scorer() {
root_dir := m.current_root_dir()
query := m.settings.SearchText()
m.scorer = NewFileSystemScorer(root_dir, query, m.settings.Filter(), m.settings.OnlyDirs(), m.on_results)
m.scorer.respect_ignores = m.settings.RespectIgnores()
m.scorer.show_hidden = m.settings.ShowHidden()
m.scorer.global_ignore = m.settings.GlobalIgnores()
}
func (m *ResultManager) on_results(err error, is_finished bool) {
if err != nil {
m.report_errors <- err
@@ -633,50 +674,64 @@ func (m *ResultManager) on_results(err error, is_finished bool) {
}
}
func (m *ResultManager) set_root_dir(root_dir string, filter Filter) {
func (m *ResultManager) current_root_dir() string {
var err error
root_dir := m.settings.CurrentDir()
if root_dir == "" || root_dir == "." {
if root_dir, err = os.Getwd(); err != nil {
return
return "/"
}
}
root_dir = utils.Expanduser(root_dir)
if root_dir, err = filepath.Abs(root_dir); err != nil {
return
return "/"
}
return root_dir
}
func (m *ResultManager) set_root_dir() {
if m.scorer != nil {
m.scorer.Cancel()
}
_ = 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)
_ = os.Chdir(m.current_root_dir()) // this is so the terminal emulator can read the wd for launch --directory=current
m.new_scorer()
m.mutex.Lock()
m.last_wakeup_at = time.Time{}
m.mutex.Unlock()
m.scorer.Start()
}
func (m *ResultManager) set_query(query string, filter Filter) {
func (m *ResultManager) set_something(callback func()) {
m.mutex.Lock()
m.last_wakeup_at = time.Time{}
m.mutex.Unlock()
if m.scorer == nil {
m.scorer = NewFileSystemScorer(".", "", filter, m.settings.OnlyDirs(), m.on_results)
m.new_scorer()
m.scorer.Start()
} else {
m.scorer.Change_query(query)
callback()
}
}
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 (m *ResultManager) set_query() {
m.set_something(func() { m.scorer.Change_query(m.settings.SearchText()) })
}
func (m *ResultManager) set_filter() {
m.set_something(func() { m.scorer.Change_filter(m.settings.Filter()) })
}
func (m *ResultManager) set_show_hidden() {
m.set_something(func() { m.scorer.Change_show_hidden(m.settings.ShowHidden()) })
}
func (m *ResultManager) set_respect_ignores() {
m.set_something(func() { m.scorer.Change_respect_ignores(m.settings.RespectIgnores()) })
}
func (m *ResultManager) set_sort_by_last_modified() {
m.set_something(func() { m.scorer.Change_sort_by_last_modified(m.settings.SortByLastModified()) })
}
func (h *Handler) get_results() (ans *SortedResults, is_complete bool) {

View File

@@ -154,10 +154,14 @@ func TestChooseFilesIgnore(t *testing.T) {
s.dir_reader = root.ReadDir
s.file_reader = root.ReadFile
s.global_gitignore = ignorefiles.NewGitignore()
s.global_ignore = ignorefiles.NewGitignore()
s.respect_ignores = respect_ignores
if err := s.global_gitignore.LoadLines("*.png", "s/3"); err != nil {
t.Fatal(err)
}
if err := s.global_ignore.LoadLines("x/3"); err != nil {
t.Fatal(err)
}
s.Start()
for range ch {
}
@@ -170,7 +174,7 @@ func TestChooseFilesIgnore(t *testing.T) {
t.Fatalf("Incorrect ignoring:\n%s", diff)
}
}
r(true, `x y b c.png x/s x/1 x/2 x/3 y/s y/3 y/4 y/5 x/s/m y/s/6`)
r(true, `x y b c.png x/s x/1 x/2 y/s y/3 y/4 y/5 x/s/m y/s/6`)
r(false, `x y a b c.png x/s x/1 x/2 x/3 y/s y/3 y/4 y/5 x/s/m x/s/n y/s/3 y/s/4 y/s/5 y/s/6`)
}