collection.go 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937
  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. "selection_background": true,
  308. "selection_foreground": true,
  309. "tab_bar_background": true,
  310. "tab_bar_margin_color": true,
  311. "url_color": true,
  312. "visual_bell_color": true,
  313. "wayland_titlebar_color": true, // ALL_COLORS_END
  314. } // }}}
  315. type JSONMetadata struct {
  316. Etag string `json:"etag"`
  317. Timestamp string `json:"timestamp"`
  318. }
  319. var ErrNoCacheFound = errors.New("No cache found and max cache age is negative")
  320. func set_comment_in_zip_file(path string, comment string) error {
  321. src, err := zip.OpenReader(path)
  322. if err != nil {
  323. return err
  324. }
  325. defer src.Close()
  326. buf := bytes.Buffer{}
  327. dest := zip.NewWriter(&buf)
  328. if err = dest.SetComment(comment); err != nil {
  329. return err
  330. }
  331. for _, sf := range src.File {
  332. err = dest.Copy(sf)
  333. if err != nil {
  334. return err
  335. }
  336. }
  337. dest.Close()
  338. return utils.AtomicUpdateFile(path, bytes.NewReader(buf.Bytes()), 0o644)
  339. }
  340. func fetch_cached(name, url, cache_path string, max_cache_age time.Duration) (string, error) {
  341. cache_path = filepath.Join(cache_path, name+".zip")
  342. zf, err := zip.OpenReader(cache_path)
  343. if err != nil && !errors.Is(err, fs.ErrNotExist) {
  344. return "", err
  345. }
  346. var jm JSONMetadata
  347. if err == nil {
  348. defer zf.Close()
  349. if err = json.Unmarshal(utils.UnsafeStringToBytes(zf.Comment), &jm); err == nil {
  350. if max_cache_age < 0 {
  351. return cache_path, nil
  352. }
  353. cache_age, err := utils.ISO8601Parse(jm.Timestamp)
  354. if err == nil {
  355. if time.Now().Before(cache_age.Add(max_cache_age)) {
  356. return cache_path, nil
  357. }
  358. }
  359. }
  360. }
  361. if max_cache_age < 0 {
  362. return "", ErrNoCacheFound
  363. }
  364. req, err := http.NewRequest(http.MethodGet, url, nil)
  365. if err != nil {
  366. return "", err
  367. }
  368. if jm.Etag != "" {
  369. req.Header.Add("If-None-Match", jm.Etag)
  370. }
  371. resp, err := http.DefaultClient.Do(req)
  372. if err != nil {
  373. return "", fmt.Errorf("Failed to download %s with error: %w", url, err)
  374. }
  375. defer resp.Body.Close()
  376. if resp.StatusCode != http.StatusOK {
  377. if resp.StatusCode == http.StatusNotModified {
  378. jm.Timestamp = utils.ISO8601Format(time.Now())
  379. comment, _ := json.Marshal(jm)
  380. err = set_comment_in_zip_file(cache_path, utils.UnsafeBytesToString(comment))
  381. if err != nil {
  382. return "", err
  383. }
  384. return cache_path, nil
  385. }
  386. return "", fmt.Errorf("Failed to download %s with HTTP error: %s", url, resp.Status)
  387. }
  388. var tf, tf2 *os.File
  389. tf, err = os.CreateTemp(filepath.Dir(cache_path), name+".temp-*")
  390. if err == nil {
  391. tf2, err = os.CreateTemp(filepath.Dir(cache_path), name+".temp-*")
  392. }
  393. defer func() {
  394. if tf != nil {
  395. tf.Close()
  396. os.Remove(tf.Name())
  397. tf = nil
  398. }
  399. if tf2 != nil {
  400. tf2.Close()
  401. os.Remove(tf2.Name())
  402. tf2 = nil
  403. }
  404. }()
  405. if err != nil {
  406. return "", fmt.Errorf("Failed to create temp file in %s with error: %w", filepath.Dir(cache_path), err)
  407. }
  408. _, err = io.Copy(tf, resp.Body)
  409. if err != nil {
  410. return "", fmt.Errorf("Failed to download %s with error: %w", url, err)
  411. }
  412. r, err := zip.OpenReader(tf.Name())
  413. if err != nil {
  414. return "", fmt.Errorf("Failed to open downloaded zip file with error: %w", err)
  415. }
  416. defer r.Close()
  417. w := zip.NewWriter(tf2)
  418. jm.Etag = resp.Header.Get("ETag")
  419. jm.Timestamp = utils.ISO8601Format(time.Now())
  420. comment, _ := json.Marshal(jm)
  421. if err = w.SetComment(utils.UnsafeBytesToString(comment)); err != nil {
  422. return "", err
  423. }
  424. for _, file := range r.File {
  425. err = w.Copy(file)
  426. if err != nil {
  427. return "", fmt.Errorf("Failed to copy zip file from source to destination archive")
  428. }
  429. }
  430. err = w.Close()
  431. if err != nil {
  432. return "", err
  433. }
  434. tf2.Close()
  435. err = os.Rename(tf2.Name(), cache_path)
  436. if err != nil {
  437. return "", fmt.Errorf("Failed to atomic rename temp file to %s with error: %w", cache_path, err)
  438. }
  439. tf2 = nil
  440. return cache_path, nil
  441. }
  442. func FetchCached(max_cache_age time.Duration) (string, error) {
  443. return fetch_cached("kitty-themes", "https://codeload.github.com/kovidgoyal/kitty-themes/zip/master", utils.CacheDir(), max_cache_age)
  444. }
  445. type ThemeMetadata struct {
  446. Name string `json:"name"`
  447. Filepath string `json:"file"`
  448. Is_dark bool `json:"is_dark"`
  449. Num_settings int `json:"num_settings"`
  450. Blurb string `json:"blurb"`
  451. License string `json:"license"`
  452. Upstream string `json:"upstream"`
  453. Author string `json:"author"`
  454. }
  455. func ParseThemeMetadata(path string) (*ThemeMetadata, map[string]string, error) {
  456. var in_metadata, in_blurb, finished_metadata bool
  457. ans := ThemeMetadata{Is_dark: true} // the default background in kitty is dark
  458. settings := map[string]string{}
  459. read_is_dark := func(key, val string) (err error) {
  460. settings[key] = val
  461. if key == "background" {
  462. if val != "" {
  463. bg, err := style.ParseColor(val)
  464. if err == nil {
  465. ans.Is_dark = utils.Max(bg.Red, bg.Green, bg.Blue) < 115
  466. }
  467. }
  468. }
  469. return
  470. }
  471. read_metadata := func(line string) (err error) {
  472. is_block := strings.HasPrefix(line, "## ")
  473. if in_metadata && !is_block {
  474. finished_metadata = true
  475. }
  476. if finished_metadata {
  477. return
  478. }
  479. if !in_metadata && is_block {
  480. in_metadata = true
  481. }
  482. if !in_metadata {
  483. return
  484. }
  485. line = line[3:]
  486. if in_blurb {
  487. ans.Blurb += " " + line
  488. return
  489. }
  490. key, val, found := strings.Cut(line, ":")
  491. if !found {
  492. return
  493. }
  494. key = strings.TrimSpace(strings.ToLower(key))
  495. val = strings.TrimSpace(val)
  496. switch key {
  497. case "name":
  498. if val != "The name of the theme (if not present, derived from filename)" {
  499. ans.Name = val
  500. }
  501. case "author":
  502. ans.Author = val
  503. case "upstream":
  504. ans.Upstream = val
  505. case "blurb":
  506. ans.Blurb = val
  507. in_blurb = true
  508. case "license":
  509. ans.License = val
  510. }
  511. return
  512. }
  513. cp := config.ConfigParser{LineHandler: read_is_dark, CommentsHandler: read_metadata}
  514. err := cp.ParseFiles(path)
  515. if err != nil {
  516. return nil, nil, err
  517. }
  518. ans.Num_settings = len(settings)
  519. return &ans, settings, nil
  520. }
  521. type Theme struct {
  522. metadata *ThemeMetadata
  523. code string
  524. settings map[string]string
  525. zip_reader *zip.File
  526. is_user_defined bool
  527. path_for_user_defined_theme string
  528. }
  529. func (self *Theme) Name() string { return self.metadata.Name }
  530. func (self *Theme) Author() string { return self.metadata.Author }
  531. func (self *Theme) Blurb() string { return self.metadata.Blurb }
  532. func (self *Theme) IsDark() bool { return self.metadata.Is_dark }
  533. func (self *Theme) IsUserDefined() bool { return self.is_user_defined }
  534. func (self *Theme) load_code() (string, error) {
  535. if self.zip_reader != nil {
  536. f, err := self.zip_reader.Open()
  537. self.zip_reader = nil
  538. if err != nil {
  539. return "", err
  540. }
  541. defer f.Close()
  542. data, err := io.ReadAll(f)
  543. if err != nil {
  544. return "", err
  545. }
  546. self.code = utils.UnsafeBytesToString(data)
  547. }
  548. if self.is_user_defined && self.path_for_user_defined_theme != "" && self.code == "" {
  549. raw, err := os.ReadFile(self.path_for_user_defined_theme)
  550. if err != nil {
  551. return "", err
  552. }
  553. self.code = utils.UnsafeBytesToString(raw)
  554. }
  555. return self.code, nil
  556. }
  557. func (self *Theme) Code() (string, error) {
  558. return self.load_code()
  559. }
  560. func (self *Theme) SaveInDir(dirpath string) (err error) {
  561. path := filepath.Join(dirpath, self.Name()+".conf")
  562. code, err := self.Code()
  563. if err != nil {
  564. return err
  565. }
  566. return utils.AtomicUpdateFile(path, bytes.NewReader(utils.UnsafeStringToBytes(code)), 0o644)
  567. }
  568. func (self *Theme) SaveInFile(config_dir, config_file_name string) (err error) {
  569. _ = os.MkdirAll(config_dir, 0o755)
  570. path := filepath.Join(config_dir, config_file_name)
  571. code, err := self.Code()
  572. if err != nil {
  573. return err
  574. }
  575. return utils.AtomicUpdateFile(path, bytes.NewReader(utils.UnsafeStringToBytes(code)), 0o644)
  576. }
  577. func (self *Theme) SaveInConf(config_dir, reload_in, config_file_name string) (err error) {
  578. if err = self.SaveInFile(config_dir, `current-theme.conf`); err != nil {
  579. return err
  580. }
  581. confpath := config_file_name
  582. if !filepath.IsAbs(config_file_name) {
  583. confpath = filepath.Join(config_dir, config_file_name)
  584. }
  585. patcher := config.Patcher{Write_backup: true}
  586. if _, err = patcher.Patch(
  587. confpath, "KITTY_THEME", fmt.Sprintf("# %s\ninclude current-theme.conf", self.metadata.Name),
  588. utils.Keys(AllColorSettingNames)...); err != nil {
  589. return
  590. }
  591. switch reload_in {
  592. case "parent":
  593. config.ReloadConfigInKitty(true)
  594. case "all":
  595. config.ReloadConfigInKitty(false)
  596. }
  597. return
  598. }
  599. func (self *Theme) Settings() (map[string]string, error) {
  600. if self.zip_reader != nil {
  601. code, err := self.load_code()
  602. if err != nil {
  603. return nil, err
  604. }
  605. self.settings = make(map[string]string, 64)
  606. scanner := utils.NewLineScanner(code)
  607. for scanner.Scan() {
  608. line := strings.TrimSpace(scanner.Text())
  609. if line != "" && line[0] != '#' {
  610. key, val, found := strings.Cut(line, " ")
  611. if found {
  612. self.settings[key] = val
  613. }
  614. }
  615. }
  616. }
  617. return self.settings, nil
  618. }
  619. func (self *Theme) AsEscapeCodes() (string, error) {
  620. settings, err := self.Settings()
  621. if err != nil {
  622. return "", err
  623. }
  624. return ColorSettingsAsEscapeCodes(settings), nil
  625. }
  626. func ColorSettingsAsEscapeCodes(settings map[string]string) string {
  627. w := strings.Builder{}
  628. w.Grow(4096)
  629. set_color := func(i int, sharp string) {
  630. w.WriteByte(';')
  631. w.WriteString(strconv.Itoa(i))
  632. w.WriteByte(';')
  633. w.WriteString(sharp)
  634. }
  635. set_default_color := func(name, defval string, num loop.DefaultColor) {
  636. w.WriteString("\033]")
  637. defer func() { w.WriteString("\033\\") }()
  638. val, found := settings[name]
  639. if !found {
  640. val = defval
  641. }
  642. if val != "" {
  643. rgba, err := style.ParseColor(val)
  644. if err == nil {
  645. w.WriteString(strconv.Itoa(int(num)))
  646. w.WriteByte(';')
  647. w.WriteString(rgba.AsRGBSharp())
  648. return
  649. }
  650. }
  651. w.WriteByte('1')
  652. w.WriteString(strconv.Itoa(int(num)))
  653. }
  654. set_default_color("foreground", style.DefaultColors.Foreground, loop.FOREGROUND)
  655. set_default_color("background", style.DefaultColors.Background, loop.BACKGROUND)
  656. set_default_color("cursor", style.DefaultColors.Cursor, loop.CURSOR)
  657. set_default_color("selection_background", style.DefaultColors.SelectionBg, loop.SELECTION_BG)
  658. set_default_color("selection_foreground", style.DefaultColors.SelectionFg, loop.SELECTION_FG)
  659. w.WriteString("\033]4")
  660. for i := 0; i < 256; i++ {
  661. key := "color" + strconv.Itoa(i)
  662. val := settings[key]
  663. if val != "" {
  664. rgba, err := style.ParseColor(val)
  665. if err == nil {
  666. set_color(i, rgba.AsRGBSharp())
  667. continue
  668. }
  669. }
  670. rgba := style.RGBA{}
  671. rgba.FromRGB(style.ColorTable[i])
  672. set_color(i, rgba.AsRGBSharp())
  673. }
  674. w.WriteString("\033\\")
  675. return w.String()
  676. }
  677. type Themes struct {
  678. name_map map[string]*Theme
  679. index_map []string
  680. }
  681. func (self *Themes) Copy() *Themes {
  682. ans := &Themes{name_map: make(map[string]*Theme, len(self.name_map)), index_map: slices.Clone(self.index_map)}
  683. maps.Copy(ans.name_map, self.name_map)
  684. return ans
  685. }
  686. var camel_case_pat = sync.OnceValue(func() *regexp.Regexp {
  687. return regexp.MustCompile(`([a-z])([A-Z])`)
  688. })
  689. func ThemeNameFromFileName(fname string) string {
  690. fname = fname[:len(fname)-len(path.Ext(fname))]
  691. fname = strings.ReplaceAll(fname, "_", " ")
  692. fname = camel_case_pat().ReplaceAllString(fname, "$1 $2")
  693. return strings.Join(utils.Map(strings.Title, strings.Split(fname, " ")), " ")
  694. }
  695. func (self *Themes) Len() int { return len(self.name_map) }
  696. func (self *Themes) At(x int) *Theme {
  697. if x >= len(self.index_map) || x < 0 {
  698. return nil
  699. }
  700. return self.name_map[self.index_map[x]]
  701. }
  702. func (self *Themes) Names() []string { return self.index_map }
  703. func (self *Themes) create_index_map() {
  704. self.index_map = utils.Keys(self.name_map)
  705. self.index_map = utils.StableSortWithKey(self.index_map, strings.ToLower)
  706. }
  707. func (self *Themes) Filtered(is_ok func(*Theme) bool) *Themes {
  708. themes := utils.Filter(utils.Values(self.name_map), is_ok)
  709. ans := Themes{name_map: make(map[string]*Theme, len(themes))}
  710. for _, theme := range themes {
  711. ans.name_map[theme.metadata.Name] = theme
  712. }
  713. ans.create_index_map()
  714. return &ans
  715. }
  716. func (self *Themes) AddFromFile(path string) (*Theme, error) {
  717. m, conf, err := ParseThemeMetadata(path)
  718. if err != nil {
  719. return nil, err
  720. }
  721. if m.Name == "" {
  722. m.Name = ThemeNameFromFileName(filepath.Base(path))
  723. }
  724. t := Theme{metadata: m, is_user_defined: true, settings: conf, path_for_user_defined_theme: path}
  725. self.name_map[m.Name] = &t
  726. return &t, nil
  727. }
  728. func (self *Themes) add_from_dir(dirpath string) error {
  729. entries, err := os.ReadDir(dirpath)
  730. if err != nil {
  731. if errors.Is(err, fs.ErrNotExist) {
  732. err = nil
  733. }
  734. return err
  735. }
  736. for _, e := range entries {
  737. if !e.IsDir() && strings.HasSuffix(e.Name(), ".conf") {
  738. path := filepath.Join(dirpath, e.Name())
  739. // ignore files if they are the STDOUT of the current processes
  740. // allows using kitten theme --dump-theme name > ~/.config/kitty/themes/name.conf
  741. if utils.Samefile(path, os.Stdout) {
  742. continue
  743. }
  744. if _, err = self.AddFromFile(path); err != nil {
  745. return err
  746. }
  747. }
  748. }
  749. return nil
  750. }
  751. func (self *Themes) add_from_zip_file(zippath string) (io.Closer, error) {
  752. r, err := zip.OpenReader(zippath)
  753. if err != nil {
  754. return nil, err
  755. }
  756. name_map := make(map[string]*zip.File, len(r.File))
  757. var themes []*ThemeMetadata
  758. theme_dir := ""
  759. for _, file := range r.File {
  760. name_map[file.Name] = file
  761. if path.Base(file.Name) == "themes.json" {
  762. theme_dir = path.Dir(file.Name)
  763. fr, err := file.Open()
  764. if err != nil {
  765. return nil, fmt.Errorf("Error while opening %s from the ZIP file: %w", file.Name, err)
  766. }
  767. defer fr.Close()
  768. raw, err := io.ReadAll(fr)
  769. if err != nil {
  770. return nil, fmt.Errorf("Error while reading %s from the ZIP file: %w", file.Name, err)
  771. }
  772. err = json.Unmarshal(raw, &themes)
  773. if err != nil {
  774. return nil, fmt.Errorf("Error while decoding %s: %w", file.Name, err)
  775. }
  776. }
  777. }
  778. if theme_dir == "" {
  779. return nil, fmt.Errorf("No themes.json found in ZIP file")
  780. }
  781. for _, theme := range themes {
  782. key := path.Join(theme_dir, theme.Filepath)
  783. f := name_map[key]
  784. if f != nil {
  785. t := Theme{metadata: theme, zip_reader: f}
  786. self.name_map[theme.Name] = &t
  787. }
  788. }
  789. return r, nil
  790. }
  791. func (self *Themes) ThemeByName(name string) *Theme {
  792. ans := self.name_map[name]
  793. if ans == nil {
  794. q := strings.ToLower(name)
  795. for k, t := range self.name_map {
  796. if strings.ToLower(k) == q {
  797. return t
  798. }
  799. }
  800. }
  801. return ans
  802. }
  803. func match(expression string, items []string) []*subseq.Match {
  804. matches := subseq.ScoreItems(expression, items, subseq.Options{Level1: " "})
  805. matches = utils.StableSort(matches, func(a, b *subseq.Match) int {
  806. if b.Score < a.Score {
  807. return -1
  808. }
  809. if b.Score > a.Score {
  810. return 1
  811. }
  812. return 0
  813. })
  814. return matches
  815. }
  816. const (
  817. MARK_BEFORE = "\033[33m"
  818. MARK_AFTER = "\033[39m"
  819. )
  820. func (self *Themes) ApplySearch(expression string, marks ...string) []string {
  821. mark_before, mark_after := MARK_BEFORE, MARK_AFTER
  822. if len(marks) == 2 {
  823. mark_before, mark_after = marks[0], marks[1]
  824. }
  825. results := utils.Filter(match(expression, self.index_map), func(x *subseq.Match) bool { return x.Score > 0 })
  826. name_map := make(map[string]*Theme, len(results))
  827. for _, m := range results {
  828. name_map[m.Text] = self.name_map[m.Text]
  829. }
  830. self.name_map = name_map
  831. self.index_map = self.index_map[:0]
  832. ans := make([]string, 0, len(results))
  833. for _, m := range results {
  834. text := m.Text
  835. positions := m.Positions
  836. for i := len(positions) - 1; i >= 0; i-- {
  837. p := positions[i]
  838. text = text[:p] + mark_before + text[p:p+1] + mark_after + text[p+1:]
  839. }
  840. ans = append(ans, text)
  841. self.index_map = append(self.index_map, m.Text)
  842. }
  843. return ans
  844. }
  845. func LoadThemes(cache_age time.Duration) (ans *Themes, closer io.Closer, err error) {
  846. zip_path, err := FetchCached(cache_age)
  847. ans = &Themes{name_map: make(map[string]*Theme)}
  848. if err != nil {
  849. return nil, nil, err
  850. }
  851. if closer, err = ans.add_from_zip_file(zip_path); err != nil {
  852. return nil, nil, err
  853. }
  854. if err = ans.add_from_dir(filepath.Join(utils.ConfigDir(), "themes")); err != nil {
  855. return nil, nil, err
  856. }
  857. ans.create_index_map()
  858. return ans, closer, nil
  859. }
  860. func ThemeFromFile(path string) (*Theme, error) {
  861. ans := &Themes{name_map: make(map[string]*Theme)}
  862. return ans.AddFromFile(path)
  863. }
  864. func GetThemeNames(cache_age time.Duration) (ans []string, err error) {
  865. themes, closer, err := LoadThemes(cache_age)
  866. if err != nil {
  867. if errors.Is(err, ErrNoCacheFound) {
  868. return []string{"Default"}, nil
  869. }
  870. return nil, err
  871. }
  872. defer closer.Close()
  873. for name := range themes.name_map {
  874. ans = append(ans, name)
  875. }
  876. return
  877. }
  878. func CompleteThemes(completions *cli.Completions, word string, arg_num int) {
  879. names, err := GetThemeNames(-1)
  880. if err == nil {
  881. mg := completions.AddMatchGroup("Themes")
  882. for _, theme_name := range names {
  883. theme_name = strings.TrimSpace(theme_name)
  884. if theme_name != "" && strings.HasPrefix(theme_name, word) {
  885. mg.AddMatch(theme_name)
  886. }
  887. }
  888. }
  889. }