paths.go 8.3 KB

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