api.go 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  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. "strconv"
  15. "strings"
  16. "sync"
  17. "kitty/tools/utils"
  18. "github.com/shirou/gopsutil/v3/process"
  19. "golang.org/x/sys/unix"
  20. )
  21. var _ = fmt.Print
  22. func StringToBool(x string) bool {
  23. x = strings.ToLower(x)
  24. return x == "y" || x == "yes" || x == "true"
  25. }
  26. type ConfigLine struct {
  27. Src_file, Line string
  28. Line_number int
  29. Err error
  30. }
  31. type ConfigParser struct {
  32. LineHandler func(key, val string) error
  33. CommentsHandler func(line string) error
  34. SourceHandler func(text, path string)
  35. bad_lines []ConfigLine
  36. seen_includes map[string]bool
  37. override_env []string
  38. }
  39. type Scanner interface {
  40. Scan() bool
  41. Text() string
  42. Err() error
  43. }
  44. func (self *ConfigParser) BadLines() []ConfigLine {
  45. return self.bad_lines
  46. }
  47. var key_pat = sync.OnceValue(func() *regexp.Regexp {
  48. return regexp.MustCompile(`([a-zA-Z][a-zA-Z0-9_-]*)\s+(.+)$`)
  49. })
  50. func (self *ConfigParser) parse(scanner Scanner, name, base_path_for_includes string, depth int) error {
  51. if self.seen_includes[name] { // avoid include loops
  52. return nil
  53. }
  54. self.seen_includes[name] = true
  55. recurse := func(r io.Reader, nname, base_path_for_includes string) error {
  56. if depth > 32 {
  57. return fmt.Errorf("Too many nested include directives while processing config file: %s", name)
  58. }
  59. escanner := bufio.NewScanner(r)
  60. return self.parse(escanner, nname, base_path_for_includes, depth+1)
  61. }
  62. make_absolute := func(path string) (string, error) {
  63. if path == "" {
  64. return "", fmt.Errorf("Empty include paths not allowed")
  65. }
  66. if !filepath.IsAbs(path) {
  67. path = filepath.Join(base_path_for_includes, path)
  68. }
  69. return path, nil
  70. }
  71. lnum := 0
  72. next_line_num := 0
  73. next_line := ""
  74. var line string
  75. for {
  76. if next_line != "" {
  77. line = next_line
  78. } else {
  79. if scanner.Scan() {
  80. line = strings.TrimLeft(scanner.Text(), " \t")
  81. next_line_num++
  82. } else {
  83. break
  84. }
  85. if line == "" {
  86. continue
  87. }
  88. }
  89. lnum = next_line_num
  90. if scanner.Scan() {
  91. next_line = strings.TrimLeft(scanner.Text(), " \t")
  92. next_line_num++
  93. for strings.HasPrefix(next_line, `\`) {
  94. line += next_line[1:]
  95. if scanner.Scan() {
  96. next_line = strings.TrimLeft(scanner.Text(), " \t")
  97. next_line_num++
  98. } else {
  99. next_line = ""
  100. }
  101. }
  102. } else {
  103. next_line = ""
  104. }
  105. if line[0] == '#' {
  106. if self.CommentsHandler != nil {
  107. err := self.CommentsHandler(line)
  108. if err != nil {
  109. self.bad_lines = append(self.bad_lines, ConfigLine{Src_file: name, Line: line, Line_number: lnum, Err: err})
  110. }
  111. }
  112. continue
  113. }
  114. m := key_pat().FindStringSubmatch(line)
  115. if len(m) < 3 {
  116. self.bad_lines = append(self.bad_lines, ConfigLine{Src_file: name, Line: line, Line_number: lnum, Err: fmt.Errorf("Invalid config line: %#v", line)})
  117. continue
  118. }
  119. key, val := m[1], m[2]
  120. for i, ch := range line {
  121. if ch == ' ' || ch == '\t' {
  122. key = line[:i]
  123. val = strings.TrimSpace(line[i+1:])
  124. break
  125. }
  126. }
  127. switch key {
  128. default:
  129. err := self.LineHandler(key, val)
  130. if err != nil {
  131. self.bad_lines = append(self.bad_lines, ConfigLine{Src_file: name, Line: line, Line_number: lnum, Err: err})
  132. }
  133. case "include", "globinclude", "envinclude":
  134. var includes []string
  135. switch key {
  136. case "include":
  137. aval, err := make_absolute(val)
  138. if err == nil {
  139. includes = []string{aval}
  140. }
  141. case "globinclude":
  142. aval, err := make_absolute(val)
  143. if err == nil {
  144. matches, err := filepath.Glob(aval)
  145. if err == nil {
  146. includes = matches
  147. }
  148. }
  149. case "envinclude":
  150. env := self.override_env
  151. if env == nil {
  152. env = os.Environ()
  153. }
  154. for _, x := range env {
  155. key, eval, _ := strings.Cut(x, "=")
  156. is_match, err := filepath.Match(val, key)
  157. if is_match && err == nil {
  158. err := recurse(strings.NewReader(eval), "<env var: "+key+">", base_path_for_includes)
  159. if err != nil {
  160. return err
  161. }
  162. }
  163. }
  164. }
  165. if len(includes) > 0 {
  166. for _, incpath := range includes {
  167. raw, err := os.ReadFile(incpath)
  168. if err == nil {
  169. err := recurse(bytes.NewReader(raw), incpath, filepath.Dir(incpath))
  170. if err != nil {
  171. return err
  172. }
  173. } else if !errors.Is(err, fs.ErrNotExist) {
  174. return fmt.Errorf("Failed to process include %#v with error: %w", incpath, err)
  175. }
  176. }
  177. }
  178. }
  179. }
  180. return nil
  181. }
  182. func (self *ConfigParser) ParseFiles(paths ...string) error {
  183. for _, path := range paths {
  184. apath, err := filepath.Abs(path)
  185. if err == nil {
  186. path = apath
  187. }
  188. raw, err := os.ReadFile(path)
  189. if err != nil {
  190. return err
  191. }
  192. scanner := utils.NewLineScanner(utils.UnsafeBytesToString(raw))
  193. self.seen_includes = make(map[string]bool)
  194. err = self.parse(scanner, path, filepath.Dir(path), 0)
  195. if err != nil {
  196. return err
  197. }
  198. if self.SourceHandler != nil {
  199. self.SourceHandler(utils.UnsafeBytesToString(raw), path)
  200. }
  201. }
  202. return nil
  203. }
  204. func (self *ConfigParser) LoadConfig(name string, paths []string, overrides []string) (err error) {
  205. const SYSTEM_CONF = "/etc/xdg/kitty"
  206. system_conf := filepath.Join(SYSTEM_CONF, name)
  207. add_if_exists := func(q string) {
  208. err = self.ParseFiles(q)
  209. if err != nil && errors.Is(err, fs.ErrNotExist) {
  210. err = nil
  211. }
  212. }
  213. if add_if_exists(system_conf); err != nil {
  214. return err
  215. }
  216. if len(paths) > 0 {
  217. for _, path := range paths {
  218. if add_if_exists(path); err != nil {
  219. return err
  220. }
  221. }
  222. } else {
  223. if add_if_exists(filepath.Join(utils.ConfigDirForName(name), name)); err != nil {
  224. return err
  225. }
  226. }
  227. if len(overrides) > 0 {
  228. err = self.ParseOverrides(overrides...)
  229. if err != nil {
  230. return err
  231. }
  232. }
  233. return
  234. }
  235. type LinesScanner struct {
  236. lines []string
  237. }
  238. func (self *LinesScanner) Scan() bool {
  239. return len(self.lines) > 0
  240. }
  241. func (self *LinesScanner) Text() string {
  242. ans := self.lines[0]
  243. self.lines = self.lines[1:]
  244. return ans
  245. }
  246. func (self *LinesScanner) Err() error {
  247. return nil
  248. }
  249. func (self *ConfigParser) ParseOverrides(overrides ...string) error {
  250. s := LinesScanner{lines: utils.Map(func(x string) string {
  251. return strings.Replace(x, "=", " ", 1)
  252. }, overrides)}
  253. self.seen_includes = make(map[string]bool)
  254. return self.parse(&s, "<overrides>", utils.ConfigDir(), 0)
  255. }
  256. func is_kitty_gui_cmdline(exe string, cmd ...string) bool {
  257. if len(cmd) == 0 {
  258. return false
  259. }
  260. if filepath.Base(exe) != "kitty" {
  261. return false
  262. }
  263. if len(cmd) == 1 {
  264. return true
  265. }
  266. s := cmd[1][:1]
  267. switch s {
  268. case `@`:
  269. return false
  270. case `+`:
  271. if cmd[1] == `+` {
  272. return len(cmd) > 2 && cmd[2] == `open`
  273. }
  274. return cmd[1] == `+open`
  275. }
  276. return true
  277. }
  278. type Patcher struct {
  279. Write_backup bool
  280. Mode fs.FileMode
  281. }
  282. func (self Patcher) Patch(path, sentinel, content string, settings_to_comment_out ...string) (updated bool, err error) {
  283. if self.Mode == 0 {
  284. self.Mode = 0o644
  285. }
  286. backup_path := path
  287. if q, err := filepath.EvalSymlinks(path); err == nil {
  288. path = q
  289. }
  290. raw, err := os.ReadFile(path)
  291. if err != nil && !errors.Is(err, fs.ErrNotExist) {
  292. return false, err
  293. }
  294. if raw == nil {
  295. raw = []byte{}
  296. }
  297. pat := utils.MustCompile(fmt.Sprintf(`(?m)^\s*(%s)\b`, strings.Join(settings_to_comment_out, "|")))
  298. text := pat.ReplaceAllString(utils.UnsafeBytesToString(raw), `# $1`)
  299. pat = utils.MustCompile(fmt.Sprintf(`(?ms)^# BEGIN_%s.+?# END_%s`, sentinel, sentinel))
  300. replaced := false
  301. addition := fmt.Sprintf("# BEGIN_%s\n%s\n# END_%s", sentinel, content, sentinel)
  302. ntext := pat.ReplaceAllStringFunc(text, func(string) string {
  303. replaced = true
  304. return addition
  305. })
  306. if !replaced {
  307. if text != "" {
  308. text += "\n\n"
  309. }
  310. ntext = text + addition
  311. }
  312. nraw := utils.UnsafeStringToBytes(ntext)
  313. if !bytes.Equal(raw, nraw) {
  314. if len(raw) > 0 && self.Write_backup {
  315. _ = os.WriteFile(backup_path+".bak", raw, self.Mode)
  316. }
  317. return true, utils.AtomicUpdateFile(path, bytes.NewReader(nraw), self.Mode)
  318. }
  319. return false, nil
  320. }
  321. func ReloadConfigInKitty(in_parent_only bool) error {
  322. if in_parent_only {
  323. if pid, err := strconv.Atoi(os.Getenv("KITTY_PID")); err == nil {
  324. if p, err := process.NewProcess(int32(pid)); err == nil {
  325. if exe, eerr := p.Exe(); eerr == nil {
  326. if c, err := p.CmdlineSlice(); err == nil && is_kitty_gui_cmdline(exe, c...) {
  327. return p.SendSignal(unix.SIGUSR1)
  328. }
  329. }
  330. }
  331. }
  332. return nil
  333. }
  334. // process.Processes() followed by filtering by getting the process
  335. // exe and cmdline is very slow on non-Linux systems as CGO is not allowed
  336. // which means getting exe works by calling lsof on every process. So instead do
  337. // initial filtering based on ps output.
  338. if ps_out, err := exec.Command("ps", "-x", "-o", "pid=,comm=").Output(); err == nil {
  339. for _, line := range utils.Splitlines(utils.UnsafeBytesToString(ps_out)) {
  340. line = strings.TrimSpace(line)
  341. if pid_string, argv0, found := strings.Cut(line, " "); found {
  342. if pid, err := strconv.Atoi(strings.TrimSpace(pid_string)); err == nil && strings.Contains(argv0, "kitty") {
  343. if p, err := process.NewProcess(int32(pid)); err == nil {
  344. if cmdline, err := p.CmdlineSlice(); err == nil {
  345. if exe, err := p.Exe(); err == nil && is_kitty_gui_cmdline(exe, cmdline...) {
  346. _ = p.SendSignal(unix.SIGUSR1)
  347. }
  348. }
  349. }
  350. }
  351. }
  352. }
  353. }
  354. return nil
  355. }