123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334 |
- // License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
- package cli
- import (
- "fmt"
- "mime"
- "os"
- "path/filepath"
- "strings"
- "golang.org/x/sys/unix"
- "kitty/tools/utils"
- )
- var _ = fmt.Print
- func absolutize_path(path string) string {
- path = utils.Expanduser(path)
- q, err := filepath.Abs(path)
- if err == nil {
- path = q
- }
- return path
- }
- type FileEntry struct {
- Name, CompletionCandidate, Abspath string
- Mode os.FileMode
- IsDir, IsSymlink, IsEmptyDir bool
- }
- func CompleteFiles(prefix string, callback func(*FileEntry), cwd string) error {
- if cwd == "" {
- var err error
- cwd, err = os.Getwd()
- if err != nil {
- return err
- }
- }
- location := absolutize_path(prefix)
- base_dir := ""
- joinable_prefix := ""
- switch prefix {
- case ".":
- base_dir = "."
- joinable_prefix = ""
- case "./":
- base_dir = "."
- joinable_prefix = "./"
- case "/":
- base_dir = "/"
- joinable_prefix = "/"
- case "~":
- base_dir = location
- joinable_prefix = "~/"
- case "":
- base_dir = cwd
- joinable_prefix = ""
- default:
- if strings.HasSuffix(prefix, utils.Sep) {
- base_dir = location
- joinable_prefix = prefix
- } else {
- idx := strings.LastIndex(prefix, utils.Sep)
- if idx > 0 {
- joinable_prefix = prefix[:idx+1]
- base_dir = filepath.Dir(location)
- }
- }
- }
- if base_dir == "" {
- base_dir = cwd
- }
- if !strings.HasPrefix(base_dir, "~") && !filepath.IsAbs(base_dir) {
- base_dir = filepath.Join(cwd, base_dir)
- }
- // fmt.Printf("prefix=%#v base_dir=%#v joinable_prefix=%#v\n", prefix, base_dir, joinable_prefix)
- entries, err := os.ReadDir(base_dir)
- if err != nil {
- return err
- }
- for _, entry := range entries {
- q := joinable_prefix + entry.Name()
- if !strings.HasPrefix(q, prefix) {
- continue
- }
- abspath := filepath.Join(base_dir, entry.Name())
- dir_to_check := ""
- data := FileEntry{
- Name: entry.Name(), Abspath: abspath, Mode: entry.Type(), IsDir: entry.IsDir(),
- IsSymlink: entry.Type()&os.ModeSymlink == os.ModeSymlink, CompletionCandidate: q}
- if data.IsSymlink {
- target, err := filepath.EvalSymlinks(abspath)
- if err == nil && target != base_dir {
- td, err := os.Stat(target)
- if err == nil && td.IsDir() {
- dir_to_check = target
- data.IsDir = true
- }
- }
- }
- if dir_to_check != "" {
- subentries, err := os.ReadDir(dir_to_check)
- data.IsEmptyDir = err != nil || len(subentries) == 0
- }
- if data.IsDir {
- data.CompletionCandidate += utils.Sep
- }
- callback(&data)
- }
- return nil
- }
- func CompleteExecutablesInPath(prefix string, paths ...string) []string {
- ans := make([]string, 0, 1024)
- if len(paths) == 0 {
- paths = filepath.SplitList(os.Getenv("PATH"))
- }
- for _, dir := range paths {
- entries, err := os.ReadDir(dir)
- if err == nil {
- for _, e := range entries {
- if strings.HasPrefix(e.Name(), prefix) && !e.IsDir() && unix.Access(filepath.Join(dir, e.Name()), unix.X_OK) == nil {
- ans = append(ans, e.Name())
- }
- }
- }
- }
- return ans
- }
- func is_dir_or_symlink_to_dir(entry os.DirEntry, path string) bool {
- if entry.IsDir() {
- return true
- }
- if entry.Type()&os.ModeSymlink == os.ModeSymlink {
- p, err := filepath.EvalSymlinks(path)
- if err == nil {
- s, err := os.Stat(p)
- if err == nil && s.IsDir() {
- return true
- }
- }
- }
- return false
- }
- func fname_based_completer(prefix, cwd string, is_match func(string) bool) []string {
- ans := make([]string, 0, 1024)
- _ = CompleteFiles(prefix, func(entry *FileEntry) {
- if entry.IsDir && !entry.IsEmptyDir {
- entries, err := os.ReadDir(entry.Abspath)
- if err == nil {
- for _, e := range entries {
- if is_match(e.Name()) || is_dir_or_symlink_to_dir(e, filepath.Join(entry.Abspath, e.Name())) {
- ans = append(ans, entry.CompletionCandidate)
- return
- }
- }
- }
- return
- }
- q := strings.ToLower(entry.Name)
- if is_match(q) {
- ans = append(ans, entry.CompletionCandidate)
- }
- }, cwd)
- return ans
- }
- func complete_by_fnmatch(prefix, cwd string, patterns []string) []string {
- return fname_based_completer(prefix, cwd, func(name string) bool {
- for _, pat := range patterns {
- matched, err := filepath.Match(pat, name)
- if err == nil && matched {
- return true
- }
- }
- return false
- })
- }
- func complete_by_mimepat(prefix, cwd string, patterns []string) []string {
- all_allowed := false
- for _, p := range patterns {
- if p == "*" {
- all_allowed = true
- break
- }
- }
- return fname_based_completer(prefix, cwd, func(name string) bool {
- if all_allowed {
- return true
- }
- idx := strings.Index(name, ".")
- if idx < 1 {
- return false
- }
- ext := name[idx:]
- mt := mime.TypeByExtension(ext)
- if mt == "" {
- ext = filepath.Ext(name)
- mt = mime.TypeByExtension(ext)
- }
- if mt == "" {
- return false
- }
- for _, pat := range patterns {
- matched, err := filepath.Match(pat, mt)
- if err == nil && matched {
- return true
- }
- }
- return false
- })
- }
- type relative_to int
- const (
- CWD relative_to = iota
- CONFIG
- )
- func get_cwd_for_completion(relative_to relative_to) string {
- switch relative_to {
- case CONFIG:
- return utils.ConfigDir()
- }
- return ""
- }
- func make_completer(title string, relative_to relative_to, patterns []string, f func(string, string, []string) []string) CompletionFunc {
- lpats := make([]string, 0, len(patterns))
- for _, p := range patterns {
- lpats = append(lpats, strings.ToLower(p))
- }
- cwd := get_cwd_for_completion(relative_to)
- return func(completions *Completions, word string, arg_num int) {
- q := f(word, cwd, lpats)
- if len(q) > 0 {
- dirs, files := make([]string, 0, len(q)), make([]string, 0, len(q))
- for _, x := range q {
- if strings.HasSuffix(x, "/") {
- dirs = append(dirs, x)
- } else {
- files = append(files, x)
- }
- }
- if len(dirs) > 0 {
- mg := completions.AddMatchGroup("Directories")
- mg.IsFiles = true
- mg.NoTrailingSpace = true
- for _, c := range dirs {
- mg.AddMatch(c)
- }
- }
- mg := completions.AddMatchGroup(title)
- mg.IsFiles = true
- for _, c := range files {
- mg.AddMatch(c)
- }
- }
- }
- }
- func CompleteExecutableFirstArg(completions *Completions, word string, arg_num int) {
- if arg_num > 1 {
- completions.Delegate.NumToRemove = completions.CurrentCmd.IndexOfFirstArg + 1 // +1 because the first word is not present in all_words
- completions.Delegate.Command = completions.AllWords[completions.CurrentCmd.IndexOfFirstArg]
- return
- }
- exes := CompleteExecutablesInPath(word)
- if len(exes) > 0 {
- mg := completions.AddMatchGroup("Executables in PATH")
- for _, exe := range exes {
- mg.AddMatch(exe)
- }
- }
- if len(word) > 0 {
- mg := completions.AddMatchGroup("Executables")
- mg.IsFiles = true
- _ = CompleteFiles(word, func(entry *FileEntry) {
- if entry.IsDir && !entry.IsEmptyDir {
- // only allow directories that have sub-dirs or executable files in them
- entries, err := os.ReadDir(entry.Abspath)
- if err == nil {
- for _, x := range entries {
- if x.IsDir() || unix.Access(filepath.Join(entry.Abspath, x.Name()), unix.X_OK) == nil {
- mg.AddMatch(entry.CompletionCandidate)
- break
- }
- }
- }
- } else if unix.Access(entry.Abspath, unix.X_OK) == nil {
- mg.AddMatch(entry.CompletionCandidate)
- }
- }, "")
- }
- }
- func FnmatchCompleter(title string, relative_to relative_to, patterns ...string) CompletionFunc {
- return make_completer(title, relative_to, patterns, complete_by_fnmatch)
- }
- func MimepatCompleter(title string, relative_to relative_to, patterns ...string) CompletionFunc {
- return make_completer(title, relative_to, patterns, complete_by_mimepat)
- }
- func DirectoryCompleter(title string, relative_to relative_to) CompletionFunc {
- if title == "" {
- title = "Directories"
- }
- cwd := get_cwd_for_completion(relative_to)
- return func(completions *Completions, word string, arg_num int) {
- mg := completions.AddMatchGroup(title)
- mg.NoTrailingSpace = true
- mg.IsFiles = true
- _ = CompleteFiles(word, func(entry *FileEntry) {
- if entry.Mode.IsDir() {
- mg.AddMatch(entry.CompletionCandidate)
- }
- }, cwd)
- }
- }
|