mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-26 10:12:17 +02:00
Add a bunch of settings to control filesystem scanning
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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`)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user