mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-19 15:04:50 +02:00
More work on choose files kitten
This commit is contained in:
@@ -201,6 +201,7 @@ def make_bitfields() -> None:
|
|||||||
)
|
)
|
||||||
mb('tools/vt', 'CellColor', 'is_idx 1', 'red 8', 'green 8', 'blue 8')
|
mb('tools/vt', 'CellColor', 'is_idx 1', 'red 8', 'green 8', 'blue 8')
|
||||||
mb('tools/vt', 'LineAttrs', 'prompt_kind 2',)
|
mb('tools/vt', 'LineAttrs', 'prompt_kind 2',)
|
||||||
|
mb('kittens/choose_files', 'CombinedScore', 'score 32', 'index 32')
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
# Completions {{{
|
# Completions {{{
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/kovidgoyal/kitty/tools/cli"
|
"github.com/kovidgoyal/kitty/tools/cli"
|
||||||
@@ -22,12 +20,6 @@ import (
|
|||||||
var _ = fmt.Print
|
var _ = fmt.Print
|
||||||
var debugprintln = tty.DebugPrintln
|
var debugprintln = tty.DebugPrintln
|
||||||
|
|
||||||
type ScorePattern struct {
|
|
||||||
pat *regexp.Regexp
|
|
||||||
op func(float64, float64) float64
|
|
||||||
val float64
|
|
||||||
}
|
|
||||||
|
|
||||||
type Screen int
|
type Screen int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -104,7 +96,6 @@ type State struct {
|
|||||||
current_dir string
|
current_dir string
|
||||||
select_dirs bool
|
select_dirs bool
|
||||||
multiselect bool
|
multiselect bool
|
||||||
score_patterns []ScorePattern
|
|
||||||
search_text string
|
search_text string
|
||||||
mode Mode
|
mode Mode
|
||||||
suggested_save_file_name string
|
suggested_save_file_name string
|
||||||
@@ -140,9 +131,8 @@ func (s *State) SetCurrentDir(val string) {
|
|||||||
s.current_dir = val
|
s.current_dir = val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func (s State) ScorePatterns() []ScorePattern { return s.score_patterns }
|
func (s State) CurrentIndex() int { return s.current_idx }
|
||||||
func (s State) CurrentIndex() int { return s.current_idx }
|
func (s *State) SetCurrentIndex(val int) { s.current_idx = max(0, val) }
|
||||||
func (s *State) SetCurrentIndex(val int) { s.current_idx = max(0, val) }
|
|
||||||
func (s State) CurrentDir() string {
|
func (s State) CurrentDir() string {
|
||||||
return utils.IfElse(s.current_dir == "", s.BaseDir(), s.current_dir)
|
return utils.IfElse(s.current_dir == "", s.BaseDir(), s.current_dir)
|
||||||
}
|
}
|
||||||
@@ -365,32 +355,8 @@ func (h *Handler) OnText(text string, from_key_event, in_bracketed_paste bool) (
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func mult(a, b float64) float64 { return a * b }
|
|
||||||
func sub(a, b float64) float64 { return a - b }
|
|
||||||
func add(a, b float64) float64 { return a + b }
|
|
||||||
func div(a, b float64) float64 { return a / b }
|
|
||||||
|
|
||||||
func (h *Handler) set_state_from_config(conf *Config, opts *Options) (err error) {
|
func (h *Handler) set_state_from_config(conf *Config, opts *Options) (err error) {
|
||||||
h.state = State{}
|
h.state = State{}
|
||||||
fmap := map[string]func(float64, float64) float64{
|
|
||||||
"*=": mult, "+=": add, "-=": sub, "/=": div}
|
|
||||||
h.state.score_patterns = make([]ScorePattern, len(conf.Modify_score))
|
|
||||||
for i, x := range conf.Modify_score {
|
|
||||||
p, rest, _ := strings.Cut(x, " ")
|
|
||||||
if h.state.score_patterns[i].pat, err = regexp.Compile(p); err == nil {
|
|
||||||
op, val, _ := strings.Cut(rest, " ")
|
|
||||||
if h.state.score_patterns[i].val, err = strconv.ParseFloat(val, 64); err != nil {
|
|
||||||
return fmt.Errorf("The modify score value %#v is invalid: %w", val, err)
|
|
||||||
}
|
|
||||||
if h.state.score_patterns[i].op = fmap[op]; h.state.score_patterns[i].op == nil {
|
|
||||||
return fmt.Errorf("The modify score operator %#v is unknown", op)
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("The modify score pattern %#v is invalid: %w", x, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
switch opts.Mode {
|
switch opts.Mode {
|
||||||
case "file":
|
case "file":
|
||||||
h.state.mode = SELECT_SINGLE_FILE
|
h.state.mode = SELECT_SINGLE_FILE
|
||||||
|
|||||||
@@ -17,17 +17,6 @@ opt = definition.add_option
|
|||||||
map = definition.add_map
|
map = definition.add_map
|
||||||
mma = definition.add_mouse_map
|
mma = definition.add_mouse_map
|
||||||
|
|
||||||
agr('scan', 'Scanning the filesystem')
|
|
||||||
opt('+modify_score', r'(^|/)\.[^/]+(/|$) *= 0.1', add_to_default=True, long_text='''
|
|
||||||
Modify the score of items matching the specified regular expression (matches against the absolute path).
|
|
||||||
Can be used to make certain files and directories less or more prominent in the results.
|
|
||||||
Can be specified multiple times. The default includes rules to reduce the score of hidden items and
|
|
||||||
items in some well known cache folder names. Only applies when some actual search expression is provided.
|
|
||||||
The syntax is :code:`regular-expression operator value`. Supported operators are: :code:`*=, +=, -=, /=`.
|
|
||||||
''')
|
|
||||||
opt('+modify_score', '(^|/)__pycache__(/|$) *= 0.1', add_to_default=True)
|
|
||||||
egr()
|
|
||||||
|
|
||||||
def main(args: list[str]) -> None:
|
def main(args: list[str]) -> None:
|
||||||
raise SystemExit('This must be run as kitten choose-files')
|
raise SystemExit('This must be run as kitten choose-files')
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package choose_files
|
package choose_files
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cmp"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -10,8 +12,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"github.com/kovidgoyal/kitty/tools/fzf"
|
"github.com/kovidgoyal/kitty/tools/fzf"
|
||||||
"github.com/kovidgoyal/kitty/tools/utils"
|
"github.com/kovidgoyal/kitty/tools/utils"
|
||||||
@@ -20,11 +20,10 @@ import (
|
|||||||
var _ = fmt.Print
|
var _ = fmt.Print
|
||||||
|
|
||||||
type ResultItem struct {
|
type ResultItem struct {
|
||||||
text, ltext, abspath string
|
text, abspath string
|
||||||
ftype fs.FileMode
|
ftype fs.FileMode
|
||||||
positions []int // may be nil
|
positions []int // may be nil
|
||||||
score float64
|
score CombinedScore
|
||||||
positions_sorted bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r ResultItem) String() string {
|
func (r ResultItem) String() string {
|
||||||
@@ -32,34 +31,12 @@ func (r ResultItem) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *ResultItem) sorted_positions() []int {
|
func (r *ResultItem) sorted_positions() []int {
|
||||||
if !r.positions_sorted {
|
if len(r.positions) > 1 {
|
||||||
r.positions_sorted = true
|
sort.Ints(r.positions)
|
||||||
if len(r.positions) > 1 {
|
|
||||||
sort.Ints(r.positions)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return r.positions
|
return r.positions
|
||||||
}
|
}
|
||||||
|
|
||||||
func get_modified_score(abspath string, score float64, score_patterns []ScorePattern) float64 {
|
|
||||||
for _, sp := range score_patterns {
|
|
||||||
if sp.pat.MatchString(abspath) {
|
|
||||||
score = sp.op(score, sp.val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return score
|
|
||||||
}
|
|
||||||
|
|
||||||
func count_uppercase(s string) int {
|
|
||||||
count := 0
|
|
||||||
for _, r := range s {
|
|
||||||
if unicode.IsUpper(r) {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
type ScanRequest struct {
|
type ScanRequest struct {
|
||||||
root_dir string
|
root_dir string
|
||||||
}
|
}
|
||||||
@@ -85,7 +62,6 @@ type ScoreResult struct {
|
|||||||
|
|
||||||
type Settings interface {
|
type Settings interface {
|
||||||
OnlyDirs() bool
|
OnlyDirs() bool
|
||||||
ScorePatterns() []ScorePattern
|
|
||||||
CurrentDir() string
|
CurrentDir() string
|
||||||
SearchText() string
|
SearchText() string
|
||||||
}
|
}
|
||||||
@@ -139,7 +115,7 @@ func (m *ResultManager) unlock() {
|
|||||||
m.mutex.Unlock()
|
m.mutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ResultManager) scan(dir, root_dir string, level int) (err error) {
|
func (m *ResultManager) scan(dir, root_dir string, level int, idx *uint32) (err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
st, qerr := utils.Format_stacktrace_on_panic(r)
|
st, qerr := utils.Format_stacktrace_on_panic(r)
|
||||||
@@ -153,15 +129,20 @@ func (m *ResultManager) scan(dir, root_dir string, level int) (err error) {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
m.scan_results <- ScanResult{root_dir: root_dir, items: utils.Map(func(x os.DirEntry) ResultItem {
|
ritems := utils.Map(func(x os.DirEntry) ResultItem {
|
||||||
return ResultItem{abspath: filepath.Join(dir, x.Name()), ftype: x.Type()}
|
ans := ResultItem{abspath: filepath.Join(dir, x.Name()), text: strings.ToLower(x.Name()), ftype: x.Type()}
|
||||||
}, items)}
|
ans.score.Set_index(*idx)
|
||||||
for _, x := range items {
|
*idx = *idx + 1
|
||||||
if x.IsDir() {
|
return ans
|
||||||
|
}, items)
|
||||||
|
slices.SortFunc(ritems, func(a, b ResultItem) int { return cmp.Compare(a.text, b.text) })
|
||||||
|
m.scan_results <- ScanResult{root_dir: root_dir, items: ritems}
|
||||||
|
for _, x := range ritems {
|
||||||
|
if x.ftype.IsDir() {
|
||||||
if !m.is_root_dir_current(root_dir) {
|
if !m.is_root_dir_current(root_dir) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = m.scan(filepath.Join(dir, x.Name()), root_dir, level+1); err != nil {
|
if err = m.scan(x.abspath, root_dir, level+1, idx); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,7 +153,8 @@ func (m *ResultManager) scan(dir, root_dir string, level int) (err error) {
|
|||||||
func (m *ResultManager) scan_worker() {
|
func (m *ResultManager) scan_worker() {
|
||||||
for r := range m.scan_requests {
|
for r := range m.scan_requests {
|
||||||
st := time.Now()
|
st := time.Now()
|
||||||
if err := m.scan(r.root_dir, r.root_dir, 0); err == nil {
|
var idx uint32
|
||||||
|
if err := m.scan(r.root_dir, r.root_dir, 0, &idx); err == nil {
|
||||||
m.scan_results <- ScanResult{root_dir: r.root_dir, is_finished: true}
|
m.scan_results <- ScanResult{root_dir: r.root_dir, is_finished: true}
|
||||||
}
|
}
|
||||||
debugprintln(111111111, time.Now().Sub(st), len(m.results_for_current_root_dir))
|
debugprintln(111111111, time.Now().Sub(st), len(m.results_for_current_root_dir))
|
||||||
@@ -186,7 +168,7 @@ func (m *ResultManager) create_score_query(items []ResultItem, is_finished bool)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
text = r.abspath
|
text = r.abspath
|
||||||
}
|
}
|
||||||
return ResultItem{abspath: r.abspath, text: text, ltext: strings.ToLower(text), ftype: r.ftype}
|
return ResultItem{abspath: r.abspath, text: text, ftype: r.ftype}
|
||||||
}, items), is_last_for_current_root_dir: is_finished}
|
}, items), is_last_for_current_root_dir: is_finished}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,69 +199,39 @@ func (m *ResultManager) scan_result_handler() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ResultItem) SetScoreResult(x fzf.Result) {
|
||||||
|
r.positions = x.Positions
|
||||||
|
r.score.Set_score(uint32(math.MaxUint32 - x.Score))
|
||||||
|
}
|
||||||
|
|
||||||
func (m *ResultManager) score(r ScoreRequest) (err error) {
|
func (m *ResultManager) score(r ScoreRequest) (err error) {
|
||||||
items := r.items
|
items := r.items
|
||||||
m.lock()
|
|
||||||
only_dirs := m.settings.OnlyDirs()
|
only_dirs := m.settings.OnlyDirs()
|
||||||
sp := m.settings.ScorePatterns()
|
|
||||||
m.unlock()
|
|
||||||
if only_dirs {
|
if only_dirs {
|
||||||
items = utils.Filter(items, func(r ResultItem) bool { return r.ftype.IsDir() })
|
items = utils.Filter(items, func(r ResultItem) bool { return r.ftype.IsDir() })
|
||||||
}
|
}
|
||||||
res := ScoreResult{query: r.query, items: items, root_dir: r.root_dir, is_last_for_current_root_dir: r.is_last_for_current_root_dir}
|
res := ScoreResult{
|
||||||
|
query: r.query, items: items, root_dir: r.root_dir, is_last_for_current_root_dir: r.is_last_for_current_root_dir,
|
||||||
|
}
|
||||||
if r.query != "" {
|
if r.query != "" {
|
||||||
var r []fzf.Result
|
var scores []fzf.Result
|
||||||
if r, err = m.scorer.ScoreWithCache(utils.Map(func(r ResultItem) string { return r.text }, items), res.query); err != nil {
|
if scores, err = m.scorer.ScoreWithCache(utils.Map(func(r ResultItem) string { return r.text }, items), res.query); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for i, x := range r {
|
matched_items := make([]ResultItem, 0, len(items))
|
||||||
items[i].positions = x.Positions
|
for i, x := range scores {
|
||||||
items[i].score = get_modified_score(items[i].abspath, float64(x.Score), sp)
|
if x.Score > 0 {
|
||||||
|
matched_items = append(matched_items, items[i])
|
||||||
|
item := &matched_items[len(matched_items)-1]
|
||||||
|
item.SetScoreResult(x)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
items = utils.Filter(items, func(r ResultItem) bool { return r.score > 0 })
|
items = matched_items
|
||||||
}
|
}
|
||||||
m.score_results <- res
|
m.score_results <- res
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func int_cmp(a, b int) int {
|
|
||||||
if a < b {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
if a > b {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func str_cmp(a, b string) int {
|
|
||||||
if a < b {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
if a > b {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func float_cmp(a, b float64) int { // deliberately doesnt handle NaN
|
|
||||||
if a < b {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
if a > b {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func bool_as_int(b bool) int {
|
|
||||||
return *(*int)(unsafe.Pointer(&b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func bool_cmp(a, b bool) int {
|
|
||||||
return bool_as_int(a) - bool_as_int(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *ResultManager) score_worker() {
|
func (m *ResultManager) score_worker() {
|
||||||
for r := range m.score_queries {
|
for r := range m.score_queries {
|
||||||
if m.is_query_current(r.query, r.root_dir) {
|
if m.is_query_current(r.query, r.root_dir) {
|
||||||
@@ -290,33 +242,11 @@ func (m *ResultManager) score_worker() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func cmp_with_score(a, b ResultItem) (ans int) {
|
func merge_sorted_slices(a, b []ResultItem) []ResultItem {
|
||||||
ans = float_cmp(b.score, a.score)
|
|
||||||
if ans == 0 {
|
|
||||||
ans = int_cmp(len(a.text), len(b.text))
|
|
||||||
if ans == 0 {
|
|
||||||
ans = int_cmp(count_uppercase(a.text), count_uppercase(b.text))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func cmp_without_score(a, b ResultItem) (ans int) {
|
|
||||||
ans = bool_cmp(a.ftype.IsDir(), b.ftype.IsDir())
|
|
||||||
if ans == 0 {
|
|
||||||
ans = str_cmp(a.ltext, b.ltext)
|
|
||||||
if ans == 0 {
|
|
||||||
ans = int_cmp(count_uppercase(a.text), count_uppercase(b.text))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func merge_sorted_slices(a, b []ResultItem, cmp func(a, b ResultItem) int) []ResultItem {
|
|
||||||
result := make([]ResultItem, 0, len(a)+len(b))
|
result := make([]ResultItem, 0, len(a)+len(b))
|
||||||
i, j := 0, 0
|
i, j := 0, 0
|
||||||
for i < len(a) && j < len(b) {
|
for i < len(a) && j < len(b) {
|
||||||
if cmp(a[i], b[j]) <= 0 {
|
if a[i].score <= b[j].score {
|
||||||
result = append(result, a[i])
|
result = append(result, a[i])
|
||||||
i++
|
i++
|
||||||
} else {
|
} else {
|
||||||
@@ -328,9 +258,30 @@ func merge_sorted_slices(a, b []ResultItem, cmp func(a, b ResultItem) int) []Res
|
|||||||
return append(result, b[j:]...)
|
return append(result, b[j:]...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ByRelevance []ResultItem
|
||||||
|
|
||||||
|
func (a ByRelevance) Len() int {
|
||||||
|
return len(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a ByRelevance) Swap(i, j int) {
|
||||||
|
a[i], a[j] = a[j], a[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a ByRelevance) Less(i, j int) bool {
|
||||||
|
return a[i].score < a[j].score
|
||||||
|
}
|
||||||
|
|
||||||
func (m *ResultManager) add_score_results(r ScoreResult) (err error) {
|
func (m *ResultManager) add_score_results(r ScoreResult) (err error) {
|
||||||
cmp := utils.IfElse(r.query == "", cmp_without_score, cmp_with_score)
|
min_score, max_score := CombinedScore(math.MaxUint64), CombinedScore(0)
|
||||||
slices.SortStableFunc(r.items, cmp)
|
if len(r.items) > 0 {
|
||||||
|
sort.Sort(ByRelevance(r.items))
|
||||||
|
min_score = r.items[0].score
|
||||||
|
max_score = r.items[len(r.items)-1].score
|
||||||
|
}
|
||||||
|
_, _ = min_score, max_score
|
||||||
|
// renderable_results := merge_sorted_slices(m.renderable_results, r.items)
|
||||||
|
renderable_results := append(m.renderable_results, r.items...)
|
||||||
m.lock()
|
m.lock()
|
||||||
defer func() {
|
defer func() {
|
||||||
m.unlock()
|
m.unlock()
|
||||||
@@ -339,7 +290,7 @@ func (m *ResultManager) add_score_results(r ScoreResult) (err error) {
|
|||||||
err = fmt.Errorf("%w\n%s", qerr, st)
|
err = fmt.Errorf("%w\n%s", qerr, st)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
m.renderable_results = merge_sorted_slices(m.renderable_results, r.items, cmp)
|
m.renderable_results = renderable_results
|
||||||
if r.is_last_for_current_root_dir {
|
if r.is_last_for_current_root_dir {
|
||||||
m.current_query_scoring_complete = true
|
m.current_query_scoring_complete = true
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user