collection.go 26 KB

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