paths.go 8.3 KB

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