api.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. // License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package config
  3. import (
  4. "bufio"
  5. "bytes"
  6. "errors"
  7. "fmt"
  8. "io"
  9. "io/fs"
  10. "os"
  11. "os/exec"
  12. "path/filepath"
  13. "regexp"
  14. "runtime"
  15. "strconv"
  16. "strings"
  17. "sync"
  18. "kitty"
  19. "kitty/tools/utils"
  20. "github.com/shirou/gopsutil/v3/process"
  21. "golang.org/x/sys/unix"
  22. )
  23. var _ = fmt.Print
  24. func StringToBool(x string) bool {
  25. x = strings.ToLower(x)
  26. return x == "y" || x == "yes" || x == "true"
  27. }
  28. type ConfigLine struct {
  29. Src_file, Line string
  30. Line_number int
  31. Err error
  32. }
  33. type ConfigParser struct {
  34. LineHandler func(key, val string) error
  35. CommentsHandler func(line string) error
  36. SourceHandler func(text, path string)
  37. bad_lines []ConfigLine
  38. seen_includes map[string]bool
  39. override_env []string
  40. }
  41. type Scanner interface {
  42. Scan() bool
  43. Text() string
  44. Err() error
  45. }
  46. func (self *ConfigParser) BadLines() []ConfigLine {
  47. return self.bad_lines
  48. }
  49. var key_pat = sync.OnceValue(func() *regexp.Regexp {
  50. return regexp.MustCompile(`([a-zA-Z][a-zA-Z0-9_-]*)\s+(.+)$`)
  51. })
  52. var kitty_os = sync.OnceValue(func() string {
  53. switch runtime.GOOS {
  54. case "linux":
  55. return "linux"
  56. case "freebsd", "netbsd", "openbsd":
  57. return "bsd"
  58. case "darwin":
  59. return "macos"
  60. }
  61. return "unknown"
  62. })
  63. func geninclude(path string) (string, error) {
  64. cmd := exec.Command(path)
  65. cmd.Env = os.Environ()
  66. cmd.Env = append(cmd.Env, "KITTY_OS="+kitty_os())
  67. if strings.HasSuffix(path, ".py") && unix.Access(path, unix.X_OK) != nil {
  68. if utils.KittyExe() == "" || strings.HasPrefix(path, ":") {
  69. cmd = exec.Command("python", path)
  70. } else {
  71. cmd = exec.Command(utils.KittyExe(), "+launch", path)
  72. }
  73. }
  74. stdout, err := cmd.StdoutPipe()
  75. if err != nil {
  76. return "", err
  77. }
  78. if err = cmd.Start(); err != nil {
  79. return "", err
  80. }
  81. data, err := io.ReadAll(stdout)
  82. if err != nil {
  83. return "", err
  84. }
  85. if err = cmd.Wait(); err != nil {
  86. return "", err
  87. }
  88. return utils.UnsafeBytesToString(data), nil
  89. }
  90. func ExpandVars(x string) string {
  91. return os.Expand(x, func(k string) string {
  92. if k == "KITTY_OS" {
  93. return kitty_os()
  94. }
  95. return os.Getenv(k)
  96. })
  97. }
  98. func (self *ConfigParser) parse(scanner Scanner, name, base_path_for_includes string, depth int) error {
  99. if self.seen_includes[name] { // avoid include loops
  100. return nil
  101. }
  102. self.seen_includes[name] = true
  103. recurse := func(r io.Reader, nname, base_path_for_includes string) error {
  104. if depth > 32 {
  105. return fmt.Errorf("Too many nested include directives while processing config file: %s", name)
  106. }
  107. escanner := bufio.NewScanner(r)
  108. return self.parse(escanner, nname, base_path_for_includes, depth+1)
  109. }
  110. make_absolute := func(path string) (string, error) {
  111. if path == "" {
  112. return "", fmt.Errorf("Empty include paths not allowed")
  113. }
  114. if !filepath.IsAbs(path) {
  115. path = filepath.Join(base_path_for_includes, path)
  116. }
  117. return path, nil
  118. }
  119. lnum := 0
  120. next_line_num := 0
  121. next_line := ""
  122. var line string
  123. add_bad_line := func(err error) {
  124. self.bad_lines = append(self.bad_lines, ConfigLine{Src_file: name, Line: line, Line_number: lnum, Err: err})
  125. }
  126. for {
  127. if next_line != "" {
  128. line = next_line
  129. } else {
  130. if scanner.Scan() {
  131. line = strings.TrimLeft(scanner.Text(), " \t")
  132. next_line_num++
  133. } else {
  134. break
  135. }
  136. if line == "" {
  137. continue
  138. }
  139. }
  140. lnum = next_line_num
  141. if scanner.Scan() {
  142. next_line = strings.TrimLeft(scanner.Text(), " \t")
  143. next_line_num++
  144. for strings.HasPrefix(next_line, `\`) {
  145. line += next_line[1:]
  146. if scanner.Scan() {
  147. next_line = strings.TrimLeft(scanner.Text(), " \t")
  148. next_line_num++
  149. } else {
  150. next_line = ""
  151. }
  152. }
  153. } else {
  154. next_line = ""
  155. }
  156. if line[0] == '#' {
  157. if self.CommentsHandler != nil {
  158. if err := self.CommentsHandler(line); err != nil {
  159. add_bad_line(err)
  160. }
  161. }
  162. continue
  163. }
  164. m := key_pat().FindStringSubmatch(line)
  165. if len(m) < 3 {
  166. add_bad_line(fmt.Errorf("Invalid config line: %#v", line))
  167. continue
  168. }
  169. key, val := m[1], m[2]
  170. for i, ch := range line {
  171. if ch == ' ' || ch == '\t' {
  172. key = line[:i]
  173. val = strings.TrimSpace(line[i+1:])
  174. break
  175. }
  176. }
  177. switch key {
  178. default:
  179. if err := self.LineHandler(key, val); err != nil {
  180. add_bad_line(err)
  181. }
  182. case "include", "globinclude", "envinclude", "geninclude":
  183. var includes []string
  184. val = ExpandVars(val)
  185. switch key {
  186. case "include":
  187. if aval, err := make_absolute(val); err == nil {
  188. includes = []string{aval}
  189. } else {
  190. add_bad_line(err)
  191. }
  192. case "globinclude":
  193. aval, err := make_absolute(val)
  194. if err == nil {
  195. matches, err := filepath.Glob(aval)
  196. if err == nil {
  197. includes = matches
  198. } else {
  199. add_bad_line(err)
  200. }
  201. } else {
  202. add_bad_line(err)
  203. }
  204. case "geninclude":
  205. if aval, err := make_absolute(val); err == nil {
  206. if g, err := geninclude(aval); err == nil {
  207. if err := recurse(strings.NewReader(g), "<gen: "+val+">", base_path_for_includes); err != nil {
  208. return err
  209. }
  210. } else {
  211. add_bad_line(err)
  212. }
  213. } else {
  214. add_bad_line(err)
  215. }
  216. case "envinclude":
  217. env := utils.IfElse(self.override_env == nil, os.Environ(), self.override_env)
  218. for _, x := range env {
  219. key, eval, _ := strings.Cut(x, "=")
  220. is_match, err := filepath.Match(val, key)
  221. if is_match && err == nil {
  222. err := recurse(strings.NewReader(eval), "<env var: "+key+">", base_path_for_includes)
  223. if err != nil {
  224. return err
  225. }
  226. }
  227. }
  228. }
  229. if len(includes) > 0 {
  230. for _, incpath := range includes {
  231. if raw, err := os.ReadFile(incpath); err == nil {
  232. if err := recurse(bytes.NewReader(raw), incpath, filepath.Dir(incpath)); err != nil {
  233. return err
  234. }
  235. } else if !errors.Is(err, fs.ErrNotExist) {
  236. add_bad_line(err)
  237. }
  238. }
  239. }
  240. }
  241. }
  242. return nil
  243. }
  244. func (self *ConfigParser) ParseFiles(paths ...string) error {
  245. for _, path := range paths {
  246. apath, err := filepath.Abs(path)
  247. if err == nil {
  248. path = apath
  249. }
  250. raw, err := os.ReadFile(path)
  251. if err != nil {
  252. return err
  253. }
  254. scanner := utils.NewLineScanner(utils.UnsafeBytesToString(raw))
  255. self.seen_includes = make(map[string]bool)
  256. err = self.parse(scanner, path, filepath.Dir(path), 0)
  257. if err != nil {
  258. return err
  259. }
  260. if self.SourceHandler != nil {
  261. self.SourceHandler(utils.UnsafeBytesToString(raw), path)
  262. }
  263. }
  264. return nil
  265. }
  266. func (self *ConfigParser) LoadConfig(name string, paths []string, overrides []string) (err error) {
  267. const SYSTEM_CONF = "/etc/xdg/kitty"
  268. system_conf := filepath.Join(SYSTEM_CONF, name)
  269. add_if_exists := func(q string) {
  270. err = self.ParseFiles(q)
  271. if err != nil && errors.Is(err, fs.ErrNotExist) {
  272. err = nil
  273. }
  274. }
  275. if add_if_exists(system_conf); err != nil {
  276. return err
  277. }
  278. if len(paths) > 0 {
  279. for _, path := range paths {
  280. if add_if_exists(path); err != nil {
  281. return err
  282. }
  283. }
  284. } else {
  285. if add_if_exists(filepath.Join(utils.ConfigDirForName(name), name)); err != nil {
  286. return err
  287. }
  288. }
  289. if len(overrides) > 0 {
  290. err = self.ParseOverrides(overrides...)
  291. if err != nil {
  292. return err
  293. }
  294. }
  295. return
  296. }
  297. type LinesScanner struct {
  298. lines []string
  299. }
  300. func (self *LinesScanner) Scan() bool {
  301. return len(self.lines) > 0
  302. }
  303. func (self *LinesScanner) Text() string {
  304. ans := self.lines[0]
  305. self.lines = self.lines[1:]
  306. return ans
  307. }
  308. func (self *LinesScanner) Err() error {
  309. return nil
  310. }
  311. func (self *ConfigParser) ParseOverrides(overrides ...string) error {
  312. s := LinesScanner{lines: utils.Map(func(x string) string {
  313. return strings.Replace(x, "=", " ", 1)
  314. }, overrides)}
  315. self.seen_includes = make(map[string]bool)
  316. return self.parse(&s, "<overrides>", utils.ConfigDir(), 0)
  317. }
  318. func is_kitty_gui_cmdline(exe string, cmd ...string) bool {
  319. if len(cmd) == 0 {
  320. return false
  321. }
  322. if filepath.Base(exe) != "kitty" {
  323. return false
  324. }
  325. if len(cmd) == 1 {
  326. return true
  327. }
  328. s := cmd[1][:1]
  329. switch s {
  330. case `@`:
  331. return false
  332. case `+`:
  333. if cmd[1] == `+` {
  334. return len(cmd) > 2 && cmd[2] == `open`
  335. }
  336. return cmd[1] == `+open`
  337. }
  338. return true
  339. }
  340. type Patcher struct {
  341. Write_backup bool
  342. Mode fs.FileMode
  343. }
  344. func (self Patcher) Patch(path, sentinel, content string, settings_to_comment_out ...string) (updated bool, err error) {
  345. if self.Mode == 0 {
  346. self.Mode = 0o644
  347. }
  348. backup_path := path
  349. if q, err := filepath.EvalSymlinks(path); err == nil {
  350. path = q
  351. }
  352. raw, err := os.ReadFile(path)
  353. if err != nil && !errors.Is(err, fs.ErrNotExist) {
  354. return false, err
  355. }
  356. add_at_top := ""
  357. backup := true
  358. if raw == nil {
  359. cc := kitty.CommentedOutDefaultConfig
  360. if idx := strings.Index(cc, "\n\n"); idx > 0 {
  361. add_at_top = cc[:idx+2]
  362. raw = []byte(cc[idx+2:])
  363. backup = false
  364. }
  365. }
  366. pat := utils.MustCompile(fmt.Sprintf(`(?m)^\s*(%s)\b`, strings.Join(settings_to_comment_out, "|")))
  367. text := pat.ReplaceAllString(utils.UnsafeBytesToString(raw), `# $1`)
  368. pat = utils.MustCompile(fmt.Sprintf(`(?ms)^# BEGIN_%s.+?# END_%s`, sentinel, sentinel))
  369. replaced := false
  370. addition := fmt.Sprintf("# BEGIN_%s\n%s\n# END_%s", sentinel, content, sentinel)
  371. ntext := pat.ReplaceAllStringFunc(text, func(string) string {
  372. replaced = true
  373. return addition
  374. })
  375. if !replaced {
  376. if add_at_top != "" {
  377. ntext = add_at_top + addition
  378. if text != "" {
  379. ntext += "\n\n" + text
  380. }
  381. } else {
  382. if text != "" {
  383. text += "\n\n"
  384. }
  385. ntext = text + addition
  386. }
  387. }
  388. nraw := utils.UnsafeStringToBytes(ntext)
  389. if !bytes.Equal(raw, nraw) {
  390. if len(raw) > 0 && self.Write_backup && backup {
  391. _ = os.WriteFile(backup_path+".bak", raw, self.Mode)
  392. }
  393. return true, utils.AtomicUpdateFile(path, bytes.NewReader(nraw), self.Mode)
  394. }
  395. return false, nil
  396. }
  397. func ReloadConfigInKitty(in_parent_only bool) error {
  398. if in_parent_only {
  399. if pid, err := strconv.Atoi(os.Getenv("KITTY_PID")); err == nil {
  400. if p, err := process.NewProcess(int32(pid)); err == nil {
  401. if exe, eerr := p.Exe(); eerr == nil {
  402. if c, err := p.CmdlineSlice(); err == nil && is_kitty_gui_cmdline(exe, c...) {
  403. return p.SendSignal(unix.SIGUSR1)
  404. }
  405. }
  406. }
  407. }
  408. return nil
  409. }
  410. // process.Processes() followed by filtering by getting the process
  411. // exe and cmdline is very slow on non-Linux systems as CGO is not allowed
  412. // which means getting exe works by calling lsof on every process. So instead do
  413. // initial filtering based on ps output.
  414. if ps_out, err := exec.Command("ps", "-x", "-o", "pid=,comm=").Output(); err == nil {
  415. for _, line := range utils.Splitlines(utils.UnsafeBytesToString(ps_out)) {
  416. line = strings.TrimSpace(line)
  417. if pid_string, argv0, found := strings.Cut(line, " "); found {
  418. if pid, err := strconv.Atoi(strings.TrimSpace(pid_string)); err == nil && strings.Contains(argv0, "kitty") {
  419. if p, err := process.NewProcess(int32(pid)); err == nil {
  420. if cmdline, err := p.CmdlineSlice(); err == nil {
  421. if exe, err := p.Exe(); err == nil && is_kitty_gui_cmdline(exe, cmdline...) {
  422. _ = p.SendSignal(unix.SIGUSR1)
  423. }
  424. }
  425. }
  426. }
  427. }
  428. }
  429. }
  430. return nil
  431. }
  432. var OverrideEffectiveConfigPath string
  433. func ReadKittyConfig(line_handler func(key, val string) error, override_effective_config_path ...string) error {
  434. kp := os.Getenv("KITTY_PID")
  435. kitty_conf_path := ""
  436. if len(override_effective_config_path) > 0 {
  437. kitty_conf_path = override_effective_config_path[0]
  438. }
  439. if _, err := strconv.Atoi(kp); err == nil && kitty_conf_path == "" {
  440. effective_config_path := filepath.Join(utils.CacheDir(), "effective-config", kp)
  441. if unix.Access(effective_config_path, unix.R_OK) == nil {
  442. kitty_conf_path = effective_config_path
  443. }
  444. }
  445. if kitty_conf_path == "" {
  446. kitty_conf_path = filepath.Join(utils.ConfigDir(), "kitty.conf")
  447. }
  448. cp := ConfigParser{LineHandler: line_handler}
  449. return cp.ParseFiles(kitty_conf_path)
  450. }