collection.go 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011
  1. // License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package themes
  3. import (
  4. "archive/zip"
  5. "bytes"
  6. "encoding/json"
  7. "errors"
  8. "fmt"
  9. "io"
  10. "io/fs"
  11. "net/http"
  12. "os"
  13. "path"
  14. "path/filepath"
  15. "regexp"
  16. "strconv"
  17. "strings"
  18. "sync"
  19. "time"
  20. "kitty/tools/cli"
  21. "kitty/tools/config"
  22. "kitty/tools/tui/loop"
  23. "kitty/tools/tui/subseq"
  24. "kitty/tools/utils"
  25. "kitty/tools/utils/style"
  26. "github.com/shirou/gopsutil/v3/process"
  27. "golang.org/x/exp/maps"
  28. "golang.org/x/exp/slices"
  29. "golang.org/x/sys/unix"
  30. )
  31. var _ = fmt.Print
  32. var AllColorSettingNames = map[string]bool{ // {{{
  33. // generated by gen-config.py do not edit
  34. // ALL_COLORS_START
  35. "active_border_color": true,
  36. "active_tab_background": true,
  37. "active_tab_foreground": true,
  38. "background": true,
  39. "bell_border_color": true,
  40. "color0": true,
  41. "color1": true,
  42. "color10": true,
  43. "color100": true,
  44. "color101": true,
  45. "color102": true,
  46. "color103": true,
  47. "color104": true,
  48. "color105": true,
  49. "color106": true,
  50. "color107": true,
  51. "color108": true,
  52. "color109": true,
  53. "color11": true,
  54. "color110": true,
  55. "color111": true,
  56. "color112": true,
  57. "color113": true,
  58. "color114": true,
  59. "color115": true,
  60. "color116": true,
  61. "color117": true,
  62. "color118": true,
  63. "color119": true,
  64. "color12": true,
  65. "color120": true,
  66. "color121": true,
  67. "color122": true,
  68. "color123": true,
  69. "color124": true,
  70. "color125": true,
  71. "color126": true,
  72. "color127": true,
  73. "color128": true,
  74. "color129": true,
  75. "color13": true,
  76. "color130": true,
  77. "color131": true,
  78. "color132": true,
  79. "color133": true,
  80. "color134": true,
  81. "color135": true,
  82. "color136": true,
  83. "color137": true,
  84. "color138": true,
  85. "color139": true,
  86. "color14": true,
  87. "color140": true,
  88. "color141": true,
  89. "color142": true,
  90. "color143": true,
  91. "color144": true,
  92. "color145": true,
  93. "color146": true,
  94. "color147": true,
  95. "color148": true,
  96. "color149": true,
  97. "color15": true,
  98. "color150": true,
  99. "color151": true,
  100. "color152": true,
  101. "color153": true,
  102. "color154": true,
  103. "color155": true,
  104. "color156": true,
  105. "color157": true,
  106. "color158": true,
  107. "color159": true,
  108. "color16": true,
  109. "color160": true,
  110. "color161": true,
  111. "color162": true,
  112. "color163": true,
  113. "color164": true,
  114. "color165": true,
  115. "color166": true,
  116. "color167": true,
  117. "color168": true,
  118. "color169": true,
  119. "color17": true,
  120. "color170": true,
  121. "color171": true,
  122. "color172": true,
  123. "color173": true,
  124. "color174": true,
  125. "color175": true,
  126. "color176": true,
  127. "color177": true,
  128. "color178": true,
  129. "color179": true,
  130. "color18": true,
  131. "color180": true,
  132. "color181": true,
  133. "color182": true,
  134. "color183": true,
  135. "color184": true,
  136. "color185": true,
  137. "color186": true,
  138. "color187": true,
  139. "color188": true,
  140. "color189": true,
  141. "color19": true,
  142. "color190": true,
  143. "color191": true,
  144. "color192": true,
  145. "color193": true,
  146. "color194": true,
  147. "color195": true,
  148. "color196": true,
  149. "color197": true,
  150. "color198": true,
  151. "color199": true,
  152. "color2": true,
  153. "color20": true,
  154. "color200": true,
  155. "color201": true,
  156. "color202": true,
  157. "color203": true,
  158. "color204": true,
  159. "color205": true,
  160. "color206": true,
  161. "color207": true,
  162. "color208": true,
  163. "color209": true,
  164. "color21": true,
  165. "color210": true,
  166. "color211": true,
  167. "color212": true,
  168. "color213": true,
  169. "color214": true,
  170. "color215": true,
  171. "color216": true,
  172. "color217": true,
  173. "color218": true,
  174. "color219": true,
  175. "color22": true,
  176. "color220": true,
  177. "color221": true,
  178. "color222": true,
  179. "color223": true,
  180. "color224": true,
  181. "color225": true,
  182. "color226": true,
  183. "color227": true,
  184. "color228": true,
  185. "color229": true,
  186. "color23": true,
  187. "color230": true,
  188. "color231": true,
  189. "color232": true,
  190. "color233": true,
  191. "color234": true,
  192. "color235": true,
  193. "color236": true,
  194. "color237": true,
  195. "color238": true,
  196. "color239": true,
  197. "color24": true,
  198. "color240": true,
  199. "color241": true,
  200. "color242": true,
  201. "color243": true,
  202. "color244": true,
  203. "color245": true,
  204. "color246": true,
  205. "color247": true,
  206. "color248": true,
  207. "color249": true,
  208. "color25": true,
  209. "color250": true,
  210. "color251": true,
  211. "color252": true,
  212. "color253": true,
  213. "color254": true,
  214. "color255": true,
  215. "color26": true,
  216. "color27": true,
  217. "color28": true,
  218. "color29": true,
  219. "color3": true,
  220. "color30": true,
  221. "color31": true,
  222. "color32": true,
  223. "color33": true,
  224. "color34": true,
  225. "color35": true,
  226. "color36": true,
  227. "color37": true,
  228. "color38": true,
  229. "color39": true,
  230. "color4": true,
  231. "color40": true,
  232. "color41": true,
  233. "color42": true,
  234. "color43": true,
  235. "color44": true,
  236. "color45": true,
  237. "color46": true,
  238. "color47": true,
  239. "color48": true,
  240. "color49": true,
  241. "color5": true,
  242. "color50": true,
  243. "color51": true,
  244. "color52": true,
  245. "color53": true,
  246. "color54": true,
  247. "color55": true,
  248. "color56": true,
  249. "color57": true,
  250. "color58": true,
  251. "color59": true,
  252. "color6": true,
  253. "color60": true,
  254. "color61": true,
  255. "color62": true,
  256. "color63": true,
  257. "color64": true,
  258. "color65": true,
  259. "color66": true,
  260. "color67": true,
  261. "color68": true,
  262. "color69": true,
  263. "color7": true,
  264. "color70": true,
  265. "color71": true,
  266. "color72": true,
  267. "color73": true,
  268. "color74": true,
  269. "color75": true,
  270. "color76": true,
  271. "color77": true,
  272. "color78": true,
  273. "color79": true,
  274. "color8": true,
  275. "color80": true,
  276. "color81": true,
  277. "color82": true,
  278. "color83": true,
  279. "color84": true,
  280. "color85": true,
  281. "color86": true,
  282. "color87": true,
  283. "color88": true,
  284. "color89": true,
  285. "color9": true,
  286. "color90": true,
  287. "color91": true,
  288. "color92": true,
  289. "color93": true,
  290. "color94": true,
  291. "color95": true,
  292. "color96": true,
  293. "color97": true,
  294. "color98": true,
  295. "color99": true,
  296. "cursor": true,
  297. "cursor_text_color": true,
  298. "foreground": true,
  299. "inactive_border_color": true,
  300. "inactive_tab_background": true,
  301. "inactive_tab_foreground": true,
  302. "macos_titlebar_color": true,
  303. "mark1_background": true,
  304. "mark1_foreground": true,
  305. "mark2_background": true,
  306. "mark2_foreground": true,
  307. "mark3_background": true,
  308. "mark3_foreground": true,
  309. "selection_background": true,
  310. "selection_foreground": true,
  311. "tab_bar_background": true,
  312. "tab_bar_margin_color": true,
  313. "url_color": true,
  314. "visual_bell_color": true,
  315. "wayland_titlebar_color": true, // ALL_COLORS_END
  316. } // }}}
  317. type JSONMetadata struct {
  318. Etag string `json:"etag"`
  319. Timestamp string `json:"timestamp"`
  320. }
  321. var ErrNoCacheFound = errors.New("No cache found and max cache age is negative")
  322. func set_comment_in_zip_file(path string, comment string) error {
  323. src, err := zip.OpenReader(path)
  324. if err != nil {
  325. return err
  326. }
  327. defer src.Close()
  328. buf := bytes.Buffer{}
  329. dest := zip.NewWriter(&buf)
  330. if err = dest.SetComment(comment); err != nil {
  331. return err
  332. }
  333. for _, sf := range src.File {
  334. err = dest.Copy(sf)
  335. if err != nil {
  336. return err
  337. }
  338. }
  339. dest.Close()
  340. return utils.AtomicUpdateFile(path, buf.Bytes(), 0o644)
  341. }
  342. func fetch_cached(name, url, cache_path string, max_cache_age time.Duration) (string, error) {
  343. cache_path = filepath.Join(cache_path, name+".zip")
  344. zf, err := zip.OpenReader(cache_path)
  345. if err != nil && !errors.Is(err, fs.ErrNotExist) {
  346. return "", err
  347. }
  348. var jm JSONMetadata
  349. if err == nil {
  350. defer zf.Close()
  351. if err = json.Unmarshal(utils.UnsafeStringToBytes(zf.Comment), &jm); err == nil {
  352. if max_cache_age < 0 {
  353. return cache_path, nil
  354. }
  355. cache_age, err := utils.ISO8601Parse(jm.Timestamp)
  356. if err == nil {
  357. if time.Now().Before(cache_age.Add(max_cache_age)) {
  358. return cache_path, nil
  359. }
  360. }
  361. }
  362. }
  363. if max_cache_age < 0 {
  364. return "", ErrNoCacheFound
  365. }
  366. req, err := http.NewRequest(http.MethodGet, url, nil)
  367. if err != nil {
  368. return "", err
  369. }
  370. if jm.Etag != "" {
  371. req.Header.Add("If-None-Match", jm.Etag)
  372. }
  373. resp, err := http.DefaultClient.Do(req)
  374. if err != nil {
  375. return "", fmt.Errorf("Failed to download %s with error: %w", url, err)
  376. }
  377. defer resp.Body.Close()
  378. if resp.StatusCode != http.StatusOK {
  379. if resp.StatusCode == http.StatusNotModified {
  380. jm.Timestamp = utils.ISO8601Format(time.Now())
  381. comment, _ := json.Marshal(jm)
  382. err = set_comment_in_zip_file(cache_path, utils.UnsafeBytesToString(comment))
  383. if err != nil {
  384. return "", err
  385. }
  386. return cache_path, nil
  387. }
  388. return "", fmt.Errorf("Failed to download %s with HTTP error: %s", url, resp.Status)
  389. }
  390. var tf, tf2 *os.File
  391. tf, err = os.CreateTemp(filepath.Dir(cache_path), name+".temp-*")
  392. if err == nil {
  393. tf2, err = os.CreateTemp(filepath.Dir(cache_path), name+".temp-*")
  394. }
  395. defer func() {
  396. if tf != nil {
  397. tf.Close()
  398. os.Remove(tf.Name())
  399. tf = nil
  400. }
  401. if tf2 != nil {
  402. tf2.Close()
  403. os.Remove(tf2.Name())
  404. tf2 = nil
  405. }
  406. }()
  407. if err != nil {
  408. return "", fmt.Errorf("Failed to create temp file in %s with error: %w", filepath.Dir(cache_path), err)
  409. }
  410. _, err = io.Copy(tf, resp.Body)
  411. if err != nil {
  412. return "", fmt.Errorf("Failed to download %s with error: %w", url, err)
  413. }
  414. r, err := zip.OpenReader(tf.Name())
  415. if err != nil {
  416. return "", fmt.Errorf("Failed to open downloaded zip file with error: %w", err)
  417. }
  418. defer r.Close()
  419. w := zip.NewWriter(tf2)
  420. jm.Etag = resp.Header.Get("ETag")
  421. jm.Timestamp = utils.ISO8601Format(time.Now())
  422. comment, _ := json.Marshal(jm)
  423. if err = w.SetComment(utils.UnsafeBytesToString(comment)); err != nil {
  424. return "", err
  425. }
  426. for _, file := range r.File {
  427. err = w.Copy(file)
  428. if err != nil {
  429. return "", fmt.Errorf("Failed to copy zip file from source to destination archive")
  430. }
  431. }
  432. err = w.Close()
  433. if err != nil {
  434. return "", err
  435. }
  436. tf2.Close()
  437. err = os.Rename(tf2.Name(), cache_path)
  438. if err != nil {
  439. return "", fmt.Errorf("Failed to atomic rename temp file to %s with error: %w", cache_path, err)
  440. }
  441. tf2 = nil
  442. return cache_path, nil
  443. }
  444. func FetchCached(max_cache_age time.Duration) (string, error) {
  445. return fetch_cached("kitty-themes", "https://codeload.github.com/kovidgoyal/kitty-themes/zip/master", utils.CacheDir(), max_cache_age)
  446. }
  447. type ThemeMetadata struct {
  448. Name string `json:"name"`
  449. Filepath string `json:"file"`
  450. Is_dark bool `json:"is_dark"`
  451. Num_settings int `json:"num_settings"`
  452. Blurb string `json:"blurb"`
  453. License string `json:"license"`
  454. Upstream string `json:"upstream"`
  455. Author string `json:"author"`
  456. }
  457. func ParseThemeMetadata(path string) (*ThemeMetadata, map[string]string, error) {
  458. var in_metadata, in_blurb, finished_metadata bool
  459. ans := ThemeMetadata{Is_dark: true} // the default background in kitty is dark
  460. settings := map[string]string{}
  461. read_is_dark := func(key, val string) (err error) {
  462. settings[key] = val
  463. if key == "background" {
  464. if val != "" {
  465. bg, err := style.ParseColor(val)
  466. if err == nil {
  467. ans.Is_dark = utils.Max(bg.Red, bg.Green, bg.Blue) < 115
  468. }
  469. }
  470. }
  471. return
  472. }
  473. read_metadata := func(line string) (err error) {
  474. is_block := strings.HasPrefix(line, "## ")
  475. if in_metadata && !is_block {
  476. finished_metadata = true
  477. }
  478. if finished_metadata {
  479. return
  480. }
  481. if !in_metadata && is_block {
  482. in_metadata = true
  483. }
  484. if !in_metadata {
  485. return
  486. }
  487. line = line[3:]
  488. if in_blurb {
  489. ans.Blurb += " " + line
  490. return
  491. }
  492. key, val, found := strings.Cut(line, ":")
  493. if !found {
  494. return
  495. }
  496. key = strings.TrimSpace(strings.ToLower(key))
  497. val = strings.TrimSpace(val)
  498. switch key {
  499. case "name":
  500. if val != "The name of the theme (if not present, derived from filename)" {
  501. ans.Name = val
  502. }
  503. case "author":
  504. ans.Author = val
  505. case "upstream":
  506. ans.Upstream = val
  507. case "blurb":
  508. ans.Blurb = val
  509. in_blurb = true
  510. case "license":
  511. ans.License = val
  512. }
  513. return
  514. }
  515. cp := config.ConfigParser{LineHandler: read_is_dark, CommentsHandler: read_metadata}
  516. err := cp.ParseFiles(path)
  517. if err != nil {
  518. return nil, nil, err
  519. }
  520. ans.Num_settings = len(settings)
  521. return &ans, settings, nil
  522. }
  523. type Theme struct {
  524. metadata *ThemeMetadata
  525. code string
  526. settings map[string]string
  527. zip_reader *zip.File
  528. is_user_defined bool
  529. path_for_user_defined_theme string
  530. }
  531. func (self *Theme) Name() string { return self.metadata.Name }
  532. func (self *Theme) Author() string { return self.metadata.Author }
  533. func (self *Theme) Blurb() string { return self.metadata.Blurb }
  534. func (self *Theme) IsDark() bool { return self.metadata.Is_dark }
  535. func (self *Theme) IsUserDefined() bool { return self.is_user_defined }
  536. func (self *Theme) load_code() (string, error) {
  537. if self.zip_reader != nil {
  538. f, err := self.zip_reader.Open()
  539. self.zip_reader = nil
  540. if err != nil {
  541. return "", err
  542. }
  543. defer f.Close()
  544. data, err := io.ReadAll(f)
  545. if err != nil {
  546. return "", err
  547. }
  548. self.code = utils.UnsafeBytesToString(data)
  549. }
  550. if self.is_user_defined && self.path_for_user_defined_theme != "" && self.code == "" {
  551. raw, err := os.ReadFile(self.path_for_user_defined_theme)
  552. if err != nil {
  553. return "", err
  554. }
  555. self.code = utils.UnsafeBytesToString(raw)
  556. }
  557. return self.code, nil
  558. }
  559. func (self *Theme) Code() (string, error) {
  560. return self.load_code()
  561. }
  562. func patch_conf(text, theme_name string) string {
  563. addition := fmt.Sprintf("# BEGIN_KITTY_THEME\n# %s\ninclude current-theme.conf\n# END_KITTY_THEME", theme_name)
  564. pat := utils.MustCompile(`(?ms)^# BEGIN_KITTY_THEME.+?# END_KITTY_THEME`)
  565. replaced := false
  566. ntext := pat.ReplaceAllStringFunc(text, func(string) string {
  567. replaced = true
  568. return addition
  569. })
  570. if !replaced {
  571. if text != "" {
  572. text += "\n\n"
  573. }
  574. ntext = text + addition
  575. }
  576. pat = utils.MustCompile(fmt.Sprintf(`(?m)^\s*(%s)\b`, strings.Join(maps.Keys(AllColorSettingNames), "|")))
  577. return pat.ReplaceAllString(ntext, `# $1`)
  578. }
  579. func is_kitty_gui_cmdline(cmd ...string) bool {
  580. if len(cmd) == 0 {
  581. return false
  582. }
  583. if filepath.Base(cmd[0]) != "kitty" {
  584. return false
  585. }
  586. if len(cmd) == 1 {
  587. return true
  588. }
  589. s := cmd[1][:1]
  590. switch s {
  591. case `@`:
  592. return false
  593. case `+`:
  594. if cmd[1] == `+` {
  595. return len(cmd) > 2 && cmd[2] == `open`
  596. }
  597. return cmd[1] == `+open`
  598. }
  599. return true
  600. }
  601. type ReloadDestination string
  602. const (
  603. RELOAD_IN_PARENT ReloadDestination = "parent"
  604. RELOAD_IN_ALL ReloadDestination = "all"
  605. )
  606. func reload_config(reload_in ReloadDestination) bool {
  607. switch reload_in {
  608. case RELOAD_IN_PARENT:
  609. if pid, err := strconv.Atoi(os.Getenv("KITTY_PID")); err == nil {
  610. if p, err := process.NewProcess(int32(pid)); err == nil {
  611. if c, err := p.CmdlineSlice(); err == nil && is_kitty_gui_cmdline(c...) {
  612. return p.SendSignal(unix.SIGUSR1) == nil
  613. }
  614. }
  615. }
  616. case RELOAD_IN_ALL:
  617. if all, err := process.Processes(); err == nil {
  618. for _, p := range all {
  619. if c, err := p.CmdlineSlice(); err == nil && is_kitty_gui_cmdline(c...) {
  620. _ = p.SendSignal(unix.SIGUSR1)
  621. }
  622. }
  623. return true
  624. }
  625. }
  626. return false
  627. }
  628. func (self *Theme) SaveInDir(dirpath string) (err error) {
  629. path := filepath.Join(dirpath, self.Name()+".conf")
  630. code, err := self.Code()
  631. if err != nil {
  632. return err
  633. }
  634. return utils.AtomicUpdateFile(path, utils.UnsafeStringToBytes(code), 0o644)
  635. }
  636. func (self *Theme) SaveInConf(config_dir, reload_in, config_file_name string) (err error) {
  637. _ = os.MkdirAll(config_dir, 0o755)
  638. path := filepath.Join(config_dir, `current-theme.conf`)
  639. code, err := self.Code()
  640. if err != nil {
  641. return err
  642. }
  643. err = utils.AtomicUpdateFile(path, utils.UnsafeStringToBytes(code), 0o644)
  644. if err != nil {
  645. return err
  646. }
  647. confpath := config_file_name
  648. if !filepath.IsAbs(config_file_name) {
  649. confpath = filepath.Join(config_dir, config_file_name)
  650. }
  651. if q, err := filepath.EvalSymlinks(confpath); err == nil {
  652. confpath = q
  653. }
  654. raw, err := os.ReadFile(confpath)
  655. if err != nil && !errors.Is(err, fs.ErrNotExist) {
  656. return err
  657. }
  658. nraw := patch_conf(utils.UnsafeBytesToString(raw), self.metadata.Name)
  659. if len(raw) > 0 {
  660. _ = os.WriteFile(confpath+".bak", raw, 0o600)
  661. }
  662. err = utils.AtomicUpdateFile(confpath, utils.UnsafeStringToBytes(nraw), 0o600)
  663. if err != nil {
  664. return err
  665. }
  666. reload_config(ReloadDestination(reload_in))
  667. return
  668. }
  669. func (self *Theme) Settings() (map[string]string, error) {
  670. if self.zip_reader != nil {
  671. code, err := self.load_code()
  672. if err != nil {
  673. return nil, err
  674. }
  675. self.settings = make(map[string]string, 64)
  676. scanner := utils.NewLineScanner(code)
  677. for scanner.Scan() {
  678. line := strings.TrimSpace(scanner.Text())
  679. if line != "" && line[0] != '#' {
  680. key, val, found := strings.Cut(line, " ")
  681. if found {
  682. self.settings[key] = val
  683. }
  684. }
  685. }
  686. }
  687. return self.settings, nil
  688. }
  689. func (self *Theme) AsEscapeCodes() (string, error) {
  690. settings, err := self.Settings()
  691. if err != nil {
  692. return "", err
  693. }
  694. return ColorSettingsAsEscapeCodes(settings), nil
  695. }
  696. func ColorSettingsAsEscapeCodes(settings map[string]string) string {
  697. w := strings.Builder{}
  698. w.Grow(4096)
  699. set_color := func(i int, sharp string) {
  700. w.WriteByte(';')
  701. w.WriteString(strconv.Itoa(i))
  702. w.WriteByte(';')
  703. w.WriteString(sharp)
  704. }
  705. set_default_color := func(name, defval string, num loop.DefaultColor) {
  706. w.WriteString("\033]")
  707. defer func() { w.WriteString("\033\\") }()
  708. val, found := settings[name]
  709. if !found {
  710. val = defval
  711. }
  712. if val != "" {
  713. rgba, err := style.ParseColor(val)
  714. if err == nil {
  715. w.WriteString(strconv.Itoa(int(num)))
  716. w.WriteByte(';')
  717. w.WriteString(rgba.AsRGBSharp())
  718. return
  719. }
  720. }
  721. w.WriteByte('1')
  722. w.WriteString(strconv.Itoa(int(num)))
  723. }
  724. set_default_color("foreground", style.DefaultColors.Foreground, loop.FOREGROUND)
  725. set_default_color("background", style.DefaultColors.Background, loop.BACKGROUND)
  726. set_default_color("cursor", style.DefaultColors.Cursor, loop.CURSOR)
  727. set_default_color("selection_background", style.DefaultColors.SelectionBg, loop.SELECTION_BG)
  728. set_default_color("selection_foreground", style.DefaultColors.SelectionFg, loop.SELECTION_FG)
  729. w.WriteString("\033]4")
  730. for i := 0; i < 256; i++ {
  731. key := "color" + strconv.Itoa(i)
  732. val := settings[key]
  733. if val != "" {
  734. rgba, err := style.ParseColor(val)
  735. if err == nil {
  736. set_color(i, rgba.AsRGBSharp())
  737. continue
  738. }
  739. }
  740. rgba := style.RGBA{}
  741. rgba.FromRGB(style.ColorTable[i])
  742. set_color(i, rgba.AsRGBSharp())
  743. }
  744. w.WriteString("\033\\")
  745. return w.String()
  746. }
  747. type Themes struct {
  748. name_map map[string]*Theme
  749. index_map []string
  750. }
  751. func (self *Themes) Copy() *Themes {
  752. ans := &Themes{name_map: make(map[string]*Theme, len(self.name_map)), index_map: slices.Clone(self.index_map)}
  753. maps.Copy(ans.name_map, self.name_map)
  754. return ans
  755. }
  756. var camel_case_pat = sync.OnceValue(func() *regexp.Regexp {
  757. return regexp.MustCompile(`([a-z])([A-Z])`)
  758. })
  759. func ThemeNameFromFileName(fname string) string {
  760. fname = fname[:len(fname)-len(path.Ext(fname))]
  761. fname = strings.ReplaceAll(fname, "_", " ")
  762. fname = camel_case_pat().ReplaceAllString(fname, "$1 $2")
  763. return strings.Join(utils.Map(strings.Title, strings.Split(fname, " ")), " ")
  764. }
  765. func (self *Themes) Len() int { return len(self.name_map) }
  766. func (self *Themes) At(x int) *Theme {
  767. if x >= len(self.index_map) || x < 0 {
  768. return nil
  769. }
  770. return self.name_map[self.index_map[x]]
  771. }
  772. func (self *Themes) Names() []string { return self.index_map }
  773. func (self *Themes) create_index_map() {
  774. self.index_map = maps.Keys(self.name_map)
  775. self.index_map = utils.StableSortWithKey(self.index_map, strings.ToLower)
  776. }
  777. func (self *Themes) Filtered(is_ok func(*Theme) bool) *Themes {
  778. themes := utils.Filter(maps.Values(self.name_map), is_ok)
  779. ans := Themes{name_map: make(map[string]*Theme, len(themes))}
  780. for _, theme := range themes {
  781. ans.name_map[theme.metadata.Name] = theme
  782. }
  783. ans.create_index_map()
  784. return &ans
  785. }
  786. func (self *Themes) AddFromFile(path string) (*Theme, error) {
  787. m, conf, err := ParseThemeMetadata(path)
  788. if err != nil {
  789. return nil, err
  790. }
  791. if m.Name == "" {
  792. m.Name = ThemeNameFromFileName(filepath.Base(path))
  793. }
  794. t := Theme{metadata: m, is_user_defined: true, settings: conf, path_for_user_defined_theme: path}
  795. self.name_map[m.Name] = &t
  796. return &t, nil
  797. }
  798. func (self *Themes) add_from_dir(dirpath string) error {
  799. entries, err := os.ReadDir(dirpath)
  800. if err != nil {
  801. if errors.Is(err, fs.ErrNotExist) {
  802. err = nil
  803. }
  804. return err
  805. }
  806. for _, e := range entries {
  807. if !e.IsDir() && strings.HasSuffix(e.Name(), ".conf") {
  808. path := filepath.Join(dirpath, e.Name())
  809. // ignore files if they are the STDOUT of the current processes
  810. // allows using kitten theme --dump-theme name > ~/.config/kitty/themes/name.conf
  811. if utils.Samefile(path, os.Stdout) {
  812. continue
  813. }
  814. if _, err = self.AddFromFile(path); err != nil {
  815. return err
  816. }
  817. }
  818. }
  819. return nil
  820. }
  821. func (self *Themes) add_from_zip_file(zippath string) (io.Closer, error) {
  822. r, err := zip.OpenReader(zippath)
  823. if err != nil {
  824. return nil, err
  825. }
  826. name_map := make(map[string]*zip.File, len(r.File))
  827. var themes []*ThemeMetadata
  828. theme_dir := ""
  829. for _, file := range r.File {
  830. name_map[file.Name] = file
  831. if path.Base(file.Name) == "themes.json" {
  832. theme_dir = path.Dir(file.Name)
  833. fr, err := file.Open()
  834. if err != nil {
  835. return nil, fmt.Errorf("Error while opening %s from the ZIP file: %w", file.Name, err)
  836. }
  837. defer fr.Close()
  838. raw, err := io.ReadAll(fr)
  839. if err != nil {
  840. return nil, fmt.Errorf("Error while reading %s from the ZIP file: %w", file.Name, err)
  841. }
  842. err = json.Unmarshal(raw, &themes)
  843. if err != nil {
  844. return nil, fmt.Errorf("Error while decoding %s: %w", file.Name, err)
  845. }
  846. }
  847. }
  848. if theme_dir == "" {
  849. return nil, fmt.Errorf("No themes.json found in ZIP file")
  850. }
  851. for _, theme := range themes {
  852. key := path.Join(theme_dir, theme.Filepath)
  853. f := name_map[key]
  854. if f != nil {
  855. t := Theme{metadata: theme, zip_reader: f}
  856. self.name_map[theme.Name] = &t
  857. }
  858. }
  859. return r, nil
  860. }
  861. func (self *Themes) ThemeByName(name string) *Theme {
  862. ans := self.name_map[name]
  863. if ans == nil {
  864. q := strings.ToLower(name)
  865. for k, t := range self.name_map {
  866. if strings.ToLower(k) == q {
  867. return t
  868. }
  869. }
  870. }
  871. return ans
  872. }
  873. func match(expression string, items []string) []*subseq.Match {
  874. matches := subseq.ScoreItems(expression, items, subseq.Options{Level1: " "})
  875. matches = utils.StableSort(matches, func(a, b *subseq.Match) int {
  876. if b.Score < a.Score {
  877. return -1
  878. }
  879. if b.Score > a.Score {
  880. return 1
  881. }
  882. return 0
  883. })
  884. return matches
  885. }
  886. const (
  887. MARK_BEFORE = "\033[33m"
  888. MARK_AFTER = "\033[39m"
  889. )
  890. func (self *Themes) ApplySearch(expression string, marks ...string) []string {
  891. mark_before, mark_after := MARK_BEFORE, MARK_AFTER
  892. if len(marks) == 2 {
  893. mark_before, mark_after = marks[0], marks[1]
  894. }
  895. results := utils.Filter(match(expression, self.index_map), func(x *subseq.Match) bool { return x.Score > 0 })
  896. name_map := make(map[string]*Theme, len(results))
  897. for _, m := range results {
  898. name_map[m.Text] = self.name_map[m.Text]
  899. }
  900. self.name_map = name_map
  901. self.index_map = self.index_map[:0]
  902. ans := make([]string, 0, len(results))
  903. for _, m := range results {
  904. text := m.Text
  905. positions := m.Positions
  906. for i := len(positions) - 1; i >= 0; i-- {
  907. p := positions[i]
  908. text = text[:p] + mark_before + text[p:p+1] + mark_after + text[p+1:]
  909. }
  910. ans = append(ans, text)
  911. self.index_map = append(self.index_map, m.Text)
  912. }
  913. return ans
  914. }
  915. func LoadThemes(cache_age time.Duration) (ans *Themes, closer io.Closer, err error) {
  916. zip_path, err := FetchCached(cache_age)
  917. ans = &Themes{name_map: make(map[string]*Theme)}
  918. if err != nil {
  919. return nil, nil, err
  920. }
  921. if closer, err = ans.add_from_zip_file(zip_path); err != nil {
  922. return nil, nil, err
  923. }
  924. if err = ans.add_from_dir(filepath.Join(utils.ConfigDir(), "themes")); err != nil {
  925. return nil, nil, err
  926. }
  927. ans.create_index_map()
  928. return ans, closer, nil
  929. }
  930. func ThemeFromFile(path string) (*Theme, error) {
  931. ans := &Themes{name_map: make(map[string]*Theme)}
  932. return ans.AddFromFile(path)
  933. }
  934. func GetThemeNames(cache_age time.Duration) (ans []string, err error) {
  935. themes, closer, err := LoadThemes(cache_age)
  936. if err != nil {
  937. if errors.Is(err, ErrNoCacheFound) {
  938. return []string{"Default"}, nil
  939. }
  940. return nil, err
  941. }
  942. defer closer.Close()
  943. for name := range themes.name_map {
  944. ans = append(ans, name)
  945. }
  946. return
  947. }
  948. func CompleteThemes(completions *cli.Completions, word string, arg_num int) {
  949. names, err := GetThemeNames(-1)
  950. if err == nil {
  951. mg := completions.AddMatchGroup("Themes")
  952. for _, theme_name := range names {
  953. theme_name = strings.TrimSpace(theme_name)
  954. if theme_name != "" && strings.HasPrefix(theme_name, word) {
  955. mg.AddMatch(theme_name)
  956. }
  957. }
  958. }
  959. }