paths.go 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. // License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package utils
  3. import (
  4. "crypto/rand"
  5. "encoding/base32"
  6. "fmt"
  7. "io/fs"
  8. not_rand "math/rand/v2"
  9. "os"
  10. "os/exec"
  11. "os/user"
  12. "path/filepath"
  13. "runtime"
  14. "sort"
  15. "strconv"
  16. "strings"
  17. "sync"
  18. "unicode/utf8"
  19. "golang.org/x/sys/unix"
  20. )
  21. var Sep = string(os.PathSeparator)
  22. func Expanduser(path string) string {
  23. if !strings.HasPrefix(path, "~") {
  24. return path
  25. }
  26. home, err := os.UserHomeDir()
  27. if err != nil {
  28. usr, err := user.Current()
  29. if err == nil {
  30. home = usr.HomeDir
  31. }
  32. }
  33. if err != nil || home == "" {
  34. return path
  35. }
  36. if path == "~" {
  37. return home
  38. }
  39. path = strings.ReplaceAll(path, Sep, "/")
  40. parts := strings.Split(path, "/")
  41. if parts[0] == "~" {
  42. parts[0] = home
  43. } else {
  44. uname := parts[0][1:]
  45. if uname != "" {
  46. u, err := user.Lookup(uname)
  47. if err == nil && u.HomeDir != "" {
  48. parts[0] = u.HomeDir
  49. }
  50. }
  51. }
  52. return strings.Join(parts, Sep)
  53. }
  54. func Abspath(path string) string {
  55. q, err := filepath.Abs(path)
  56. if err == nil {
  57. return q
  58. }
  59. return path
  60. }
  61. var KittyExe = sync.OnceValue(func() string {
  62. exe, err := os.Executable()
  63. if err == nil {
  64. ans := filepath.Join(filepath.Dir(exe), "kitty")
  65. if s, err := os.Stat(ans); err == nil && !s.IsDir() {
  66. return ans
  67. }
  68. }
  69. return os.Getenv("KITTY_PATH_TO_KITTY_EXE")
  70. })
  71. func ConfigDirForName(name string) (config_dir string) {
  72. if kcd := os.Getenv("KITTY_CONFIG_DIRECTORY"); kcd != "" {
  73. return Abspath(Expanduser(kcd))
  74. }
  75. var locations []string
  76. seen := NewSet[string]()
  77. add := func(x string) {
  78. x = Abspath(Expanduser(x))
  79. if !seen.Has(x) {
  80. seen.Add(x)
  81. locations = append(locations, x)
  82. }
  83. }
  84. if xh := os.Getenv("XDG_CONFIG_HOME"); xh != "" {
  85. add(xh)
  86. }
  87. if dirs := os.Getenv("XDG_CONFIG_DIRS"); dirs != "" {
  88. for _, candidate := range strings.Split(dirs, ":") {
  89. add(candidate)
  90. }
  91. }
  92. add("~/.config")
  93. if runtime.GOOS == "darwin" {
  94. add("~/Library/Preferences")
  95. }
  96. for _, loc := range locations {
  97. if loc != "" {
  98. q := filepath.Join(loc, "kitty")
  99. if _, err := os.Stat(filepath.Join(q, name)); err == nil {
  100. if unix.Access(q, unix.W_OK) == nil {
  101. config_dir = q
  102. return
  103. }
  104. }
  105. }
  106. }
  107. config_dir = os.Getenv("XDG_CONFIG_HOME")
  108. if config_dir == "" {
  109. config_dir = "~/.config"
  110. }
  111. config_dir = filepath.Join(Expanduser(config_dir), "kitty")
  112. return
  113. }
  114. var ConfigDir = sync.OnceValue(func() (config_dir string) {
  115. return ConfigDirForName("kitty.conf")
  116. })
  117. var CacheDir = sync.OnceValue(func() (cache_dir string) {
  118. candidate := ""
  119. if edir := os.Getenv("KITTY_CACHE_DIRECTORY"); edir != "" {
  120. candidate = Abspath(Expanduser(edir))
  121. } else if runtime.GOOS == "darwin" {
  122. candidate = Expanduser("~/Library/Caches/kitty")
  123. } else {
  124. candidate = os.Getenv("XDG_CACHE_HOME")
  125. if candidate == "" {
  126. candidate = "~/.cache"
  127. }
  128. candidate = filepath.Join(Expanduser(candidate), "kitty")
  129. }
  130. _ = os.MkdirAll(candidate, 0o755)
  131. return candidate
  132. })
  133. func macos_user_cache_dir() string {
  134. // Sadly Go does not provide confstr() so we use this hack.
  135. // Note that given a user generateduid and uid we can derive this by using
  136. // the algorithm at https://github.com/ydkhatri/MacForensics/blob/master/darwin_path_generator.py
  137. // but I cant find a good way to get the generateduid. Requires calling dscl in which case we might as well call getconf
  138. // The data is in /var/db/dslocal/nodes/Default/users/<username>.plist but it needs root
  139. // So instead we use various hacks to get it quickly, falling back to running /usr/bin/getconf
  140. is_ok := func(m string) bool {
  141. s, err := os.Stat(m)
  142. if err != nil {
  143. return false
  144. }
  145. stat, ok := s.Sys().(unix.Stat_t)
  146. return ok && s.IsDir() && int(stat.Uid) == os.Geteuid() && s.Mode().Perm() == 0o700 && unix.Access(m, unix.X_OK|unix.W_OK|unix.R_OK) == nil
  147. }
  148. if tdir := strings.TrimRight(os.Getenv("TMPDIR"), "/"); filepath.Base(tdir) == "T" {
  149. if m := filepath.Join(filepath.Dir(tdir), "C"); is_ok(m) {
  150. return m
  151. }
  152. }
  153. matches, err := filepath.Glob("/private/var/folders/*/*/C")
  154. if err == nil {
  155. for _, m := range matches {
  156. if is_ok(m) {
  157. return m
  158. }
  159. }
  160. }
  161. out, err := exec.Command("/usr/bin/getconf", "DARWIN_USER_CACHE_DIR").Output()
  162. if err == nil {
  163. return strings.TrimRight(strings.TrimSpace(UnsafeBytesToString(out)), "/")
  164. }
  165. return ""
  166. }
  167. var RuntimeDir = sync.OnceValue(func() (runtime_dir string) {
  168. var candidate string
  169. if q := os.Getenv("KITTY_RUNTIME_DIRECTORY"); q != "" {
  170. candidate = q
  171. } else if runtime.GOOS == "darwin" {
  172. candidate = macos_user_cache_dir()
  173. } else if q := os.Getenv("XDG_RUNTIME_DIR"); q != "" {
  174. candidate = q
  175. }
  176. candidate = strings.TrimRight(candidate, "/")
  177. if candidate == "" {
  178. q := fmt.Sprintf("/run/user/%d", os.Geteuid())
  179. if s, err := os.Stat(q); err == nil && s.IsDir() && unix.Access(q, unix.X_OK|unix.R_OK|unix.W_OK) == nil {
  180. candidate = q
  181. } else {
  182. candidate = filepath.Join(CacheDir(), "run")
  183. }
  184. }
  185. os.MkdirAll(candidate, 0o700)
  186. if s, err := os.Stat(candidate); err == nil && s.Mode().Perm() != 0o700 {
  187. os.Chmod(candidate, 0o700)
  188. }
  189. return candidate
  190. })
  191. type Walk_callback func(path, abspath string, d fs.DirEntry, err error) error
  192. func transform_symlink(path string) string {
  193. if q, err := filepath.EvalSymlinks(path); err == nil {
  194. return q
  195. }
  196. return path
  197. }
  198. func needs_symlink_recurse(path string, d fs.DirEntry) bool {
  199. if d.Type()&os.ModeSymlink == os.ModeSymlink {
  200. if s, serr := os.Stat(path); serr == nil && s.IsDir() {
  201. return true
  202. }
  203. }
  204. return false
  205. }
  206. type transformed_walker struct {
  207. seen map[string]bool
  208. real_callback Walk_callback
  209. transform_func func(string) string
  210. needs_recurse_func func(string, fs.DirEntry) bool
  211. }
  212. func (self *transformed_walker) walk(dirpath string) error {
  213. resolved_path := self.transform_func(dirpath)
  214. if self.seen[resolved_path] {
  215. return nil
  216. }
  217. self.seen[resolved_path] = true
  218. c := func(path string, d fs.DirEntry, err error) error {
  219. if err != nil {
  220. // Happens if ReadDir on d failed, skip it in that case
  221. return fs.SkipDir
  222. }
  223. rpath, err := filepath.Rel(resolved_path, path)
  224. if err != nil {
  225. return err
  226. }
  227. // we cant use filepath.Join here as it calls Clean() which can alter dirpath if it contains .. or . etc.
  228. path_based_on_original_dir := dirpath
  229. if !strings.HasSuffix(dirpath, Sep) && dirpath != "" {
  230. path_based_on_original_dir += Sep
  231. }
  232. path_based_on_original_dir += rpath
  233. if self.needs_recurse_func(path, d) {
  234. err = self.walk(path_based_on_original_dir)
  235. } else {
  236. err = self.real_callback(path_based_on_original_dir, path, d, err)
  237. }
  238. return err
  239. }
  240. return filepath.WalkDir(resolved_path, c)
  241. }
  242. // Walk, recursing into symlinks that point to directories. Ignores directories
  243. // that could not be read.
  244. func WalkWithSymlink(dirpath string, callback Walk_callback, transformers ...func(string) string) error {
  245. transform := func(path string) string {
  246. for _, t := range transformers {
  247. path = t(path)
  248. }
  249. return transform_symlink(path)
  250. }
  251. sw := transformed_walker{
  252. seen: make(map[string]bool), real_callback: callback, transform_func: transform, needs_recurse_func: needs_symlink_recurse}
  253. return sw.walk(dirpath)
  254. }
  255. func RandomFilename() string {
  256. b := []byte{0, 0, 0, 0, 0, 0, 0, 0}
  257. _, err := rand.Read(b)
  258. if err != nil {
  259. return strconv.FormatUint(uint64(not_rand.Uint32()), 16)
  260. }
  261. return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(b)
  262. }
  263. func ResolveConfPath(path string) string {
  264. cs := os.ExpandEnv(Expanduser(path))
  265. if !filepath.IsAbs(cs) {
  266. cs = filepath.Join(ConfigDir(), cs)
  267. }
  268. return cs
  269. }
  270. // Longest common path. Must be passed paths that have been cleaned by filepath.Clean
  271. func Commonpath(paths ...string) (longest_prefix string) {
  272. switch len(paths) {
  273. case 0:
  274. return
  275. case 1:
  276. return paths[0]
  277. default:
  278. sort.Strings(paths)
  279. a, b := paths[0], paths[len(paths)-1]
  280. sz := 0
  281. for a != "" && b != "" {
  282. ra, na := utf8.DecodeRuneInString(a)
  283. rb, nb := utf8.DecodeRuneInString(b)
  284. if ra != rb {
  285. break
  286. }
  287. sz += na
  288. a = a[na:]
  289. b = b[nb:]
  290. }
  291. longest_prefix = paths[0][:sz]
  292. }
  293. return
  294. }