files.go 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. // License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package cli
  3. import (
  4. "fmt"
  5. "mime"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. "golang.org/x/sys/unix"
  10. "kitty/tools/utils"
  11. )
  12. var _ = fmt.Print
  13. func absolutize_path(path string) string {
  14. path = utils.Expanduser(path)
  15. q, err := filepath.Abs(path)
  16. if err == nil {
  17. path = q
  18. }
  19. return path
  20. }
  21. type FileEntry struct {
  22. Name, CompletionCandidate, Abspath string
  23. Mode os.FileMode
  24. IsDir, IsSymlink, IsEmptyDir bool
  25. }
  26. func CompleteFiles(prefix string, callback func(*FileEntry), cwd string) error {
  27. if cwd == "" {
  28. var err error
  29. cwd, err = os.Getwd()
  30. if err != nil {
  31. return err
  32. }
  33. }
  34. location := absolutize_path(prefix)
  35. base_dir := ""
  36. joinable_prefix := ""
  37. switch prefix {
  38. case ".":
  39. base_dir = "."
  40. joinable_prefix = ""
  41. case "./":
  42. base_dir = "."
  43. joinable_prefix = "./"
  44. case "/":
  45. base_dir = "/"
  46. joinable_prefix = "/"
  47. case "~":
  48. base_dir = location
  49. joinable_prefix = "~/"
  50. case "":
  51. base_dir = cwd
  52. joinable_prefix = ""
  53. default:
  54. if strings.HasSuffix(prefix, utils.Sep) {
  55. base_dir = location
  56. joinable_prefix = prefix
  57. } else {
  58. idx := strings.LastIndex(prefix, utils.Sep)
  59. if idx > 0 {
  60. joinable_prefix = prefix[:idx+1]
  61. base_dir = filepath.Dir(location)
  62. }
  63. }
  64. }
  65. if base_dir == "" {
  66. base_dir = cwd
  67. }
  68. if !strings.HasPrefix(base_dir, "~") && !filepath.IsAbs(base_dir) {
  69. base_dir = filepath.Join(cwd, base_dir)
  70. }
  71. // fmt.Printf("prefix=%#v base_dir=%#v joinable_prefix=%#v\n", prefix, base_dir, joinable_prefix)
  72. entries, err := os.ReadDir(base_dir)
  73. if err != nil {
  74. return err
  75. }
  76. for _, entry := range entries {
  77. q := joinable_prefix + entry.Name()
  78. if !strings.HasPrefix(q, prefix) {
  79. continue
  80. }
  81. abspath := filepath.Join(base_dir, entry.Name())
  82. dir_to_check := ""
  83. data := FileEntry{
  84. Name: entry.Name(), Abspath: abspath, Mode: entry.Type(), IsDir: entry.IsDir(),
  85. IsSymlink: entry.Type()&os.ModeSymlink == os.ModeSymlink, CompletionCandidate: q}
  86. if data.IsSymlink {
  87. target, err := filepath.EvalSymlinks(abspath)
  88. if err == nil && target != base_dir {
  89. td, err := os.Stat(target)
  90. if err == nil && td.IsDir() {
  91. dir_to_check = target
  92. data.IsDir = true
  93. }
  94. }
  95. }
  96. if dir_to_check != "" {
  97. subentries, err := os.ReadDir(dir_to_check)
  98. data.IsEmptyDir = err != nil || len(subentries) == 0
  99. }
  100. if data.IsDir {
  101. data.CompletionCandidate += utils.Sep
  102. }
  103. callback(&data)
  104. }
  105. return nil
  106. }
  107. func CompleteExecutablesInPath(prefix string, paths ...string) []string {
  108. ans := make([]string, 0, 1024)
  109. if len(paths) == 0 {
  110. paths = filepath.SplitList(os.Getenv("PATH"))
  111. }
  112. for _, dir := range paths {
  113. entries, err := os.ReadDir(dir)
  114. if err == nil {
  115. for _, e := range entries {
  116. if strings.HasPrefix(e.Name(), prefix) && !e.IsDir() && unix.Access(filepath.Join(dir, e.Name()), unix.X_OK) == nil {
  117. ans = append(ans, e.Name())
  118. }
  119. }
  120. }
  121. }
  122. return ans
  123. }
  124. func is_dir_or_symlink_to_dir(entry os.DirEntry, path string) bool {
  125. if entry.IsDir() {
  126. return true
  127. }
  128. if entry.Type()&os.ModeSymlink == os.ModeSymlink {
  129. p, err := filepath.EvalSymlinks(path)
  130. if err == nil {
  131. s, err := os.Stat(p)
  132. if err == nil && s.IsDir() {
  133. return true
  134. }
  135. }
  136. }
  137. return false
  138. }
  139. func fname_based_completer(prefix, cwd string, is_match func(string) bool) []string {
  140. ans := make([]string, 0, 1024)
  141. _ = CompleteFiles(prefix, func(entry *FileEntry) {
  142. if entry.IsDir && !entry.IsEmptyDir {
  143. entries, err := os.ReadDir(entry.Abspath)
  144. if err == nil {
  145. for _, e := range entries {
  146. if is_match(e.Name()) || is_dir_or_symlink_to_dir(e, filepath.Join(entry.Abspath, e.Name())) {
  147. ans = append(ans, entry.CompletionCandidate)
  148. return
  149. }
  150. }
  151. }
  152. return
  153. }
  154. q := strings.ToLower(entry.Name)
  155. if is_match(q) {
  156. ans = append(ans, entry.CompletionCandidate)
  157. }
  158. }, cwd)
  159. return ans
  160. }
  161. func complete_by_fnmatch(prefix, cwd string, patterns []string) []string {
  162. return fname_based_completer(prefix, cwd, func(name string) bool {
  163. for _, pat := range patterns {
  164. matched, err := filepath.Match(pat, name)
  165. if err == nil && matched {
  166. return true
  167. }
  168. }
  169. return false
  170. })
  171. }
  172. func complete_by_mimepat(prefix, cwd string, patterns []string) []string {
  173. all_allowed := false
  174. for _, p := range patterns {
  175. if p == "*" {
  176. all_allowed = true
  177. break
  178. }
  179. }
  180. return fname_based_completer(prefix, cwd, func(name string) bool {
  181. if all_allowed {
  182. return true
  183. }
  184. idx := strings.Index(name, ".")
  185. if idx < 1 {
  186. return false
  187. }
  188. ext := name[idx:]
  189. mt := mime.TypeByExtension(ext)
  190. if mt == "" {
  191. ext = filepath.Ext(name)
  192. mt = mime.TypeByExtension(ext)
  193. }
  194. if mt == "" {
  195. return false
  196. }
  197. for _, pat := range patterns {
  198. matched, err := filepath.Match(pat, mt)
  199. if err == nil && matched {
  200. return true
  201. }
  202. }
  203. return false
  204. })
  205. }
  206. type relative_to int
  207. const (
  208. CWD relative_to = iota
  209. CONFIG
  210. )
  211. func get_cwd_for_completion(relative_to relative_to) string {
  212. switch relative_to {
  213. case CONFIG:
  214. return utils.ConfigDir()
  215. }
  216. return ""
  217. }
  218. func make_completer(title string, relative_to relative_to, patterns []string, f func(string, string, []string) []string) CompletionFunc {
  219. lpats := make([]string, 0, len(patterns))
  220. for _, p := range patterns {
  221. lpats = append(lpats, strings.ToLower(p))
  222. }
  223. cwd := get_cwd_for_completion(relative_to)
  224. return func(completions *Completions, word string, arg_num int) {
  225. q := f(word, cwd, lpats)
  226. if len(q) > 0 {
  227. dirs, files := make([]string, 0, len(q)), make([]string, 0, len(q))
  228. for _, x := range q {
  229. if strings.HasSuffix(x, "/") {
  230. dirs = append(dirs, x)
  231. } else {
  232. files = append(files, x)
  233. }
  234. }
  235. if len(dirs) > 0 {
  236. mg := completions.AddMatchGroup("Directories")
  237. mg.IsFiles = true
  238. mg.NoTrailingSpace = true
  239. for _, c := range dirs {
  240. mg.AddMatch(c)
  241. }
  242. }
  243. mg := completions.AddMatchGroup(title)
  244. mg.IsFiles = true
  245. for _, c := range files {
  246. mg.AddMatch(c)
  247. }
  248. }
  249. }
  250. }
  251. func CompleteExecutableFirstArg(completions *Completions, word string, arg_num int) {
  252. if arg_num > 1 {
  253. completions.Delegate.NumToRemove = completions.CurrentCmd.IndexOfFirstArg + 1 // +1 because the first word is not present in all_words
  254. completions.Delegate.Command = completions.AllWords[completions.CurrentCmd.IndexOfFirstArg]
  255. return
  256. }
  257. exes := CompleteExecutablesInPath(word)
  258. if len(exes) > 0 {
  259. mg := completions.AddMatchGroup("Executables in PATH")
  260. for _, exe := range exes {
  261. mg.AddMatch(exe)
  262. }
  263. }
  264. if len(word) > 0 {
  265. mg := completions.AddMatchGroup("Executables")
  266. mg.IsFiles = true
  267. _ = CompleteFiles(word, func(entry *FileEntry) {
  268. if entry.IsDir && !entry.IsEmptyDir {
  269. // only allow directories that have sub-dirs or executable files in them
  270. entries, err := os.ReadDir(entry.Abspath)
  271. if err == nil {
  272. for _, x := range entries {
  273. if x.IsDir() || unix.Access(filepath.Join(entry.Abspath, x.Name()), unix.X_OK) == nil {
  274. mg.AddMatch(entry.CompletionCandidate)
  275. break
  276. }
  277. }
  278. }
  279. } else if unix.Access(entry.Abspath, unix.X_OK) == nil {
  280. mg.AddMatch(entry.CompletionCandidate)
  281. }
  282. }, "")
  283. }
  284. }
  285. func FnmatchCompleter(title string, relative_to relative_to, patterns ...string) CompletionFunc {
  286. return make_completer(title, relative_to, patterns, complete_by_fnmatch)
  287. }
  288. func MimepatCompleter(title string, relative_to relative_to, patterns ...string) CompletionFunc {
  289. return make_completer(title, relative_to, patterns, complete_by_mimepat)
  290. }
  291. func DirectoryCompleter(title string, relative_to relative_to) CompletionFunc {
  292. if title == "" {
  293. title = "Directories"
  294. }
  295. cwd := get_cwd_for_completion(relative_to)
  296. return func(completions *Completions, word string, arg_num int) {
  297. mg := completions.AddMatchGroup(title)
  298. mg.NoTrailingSpace = true
  299. mg.IsFiles = true
  300. _ = CompleteFiles(word, func(entry *FileEntry) {
  301. if entry.Mode.IsDir() {
  302. mg.AddMatch(entry.CompletionCandidate)
  303. }
  304. }, cwd)
  305. }
  306. }