feat: implement catppuccin in nim

This commit is contained in:
Daylin Morgan 2023-04-26 20:03:36 -05:00
parent 1f1e03f873
commit b8abe92f91
Signed by: daylin
GPG key ID: C1E52E7DD81DF79F
12 changed files with 609 additions and 27 deletions

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
/public/
/tools/palette-porcelain.json
/tests/**
!/tests/*.nim
!/tests/*.nims

View file

@ -11,41 +11,30 @@
<a href="https://github.com/catppuccin/nim/contributors"><img src="https://img.shields.io/github/contributors/catppuccin/template?colorA=363a4f&colorB=a6da95&style=for-the-badge"></a>
</p>
<p align="center">
<img src="https://raw.githubusercontent.com/catppuccin/catppuccin/main/assets/previews/preview.webp"/>
</p>
## Previews
<details>
<summary>🌻 Latte</summary>
<img src="https://raw.githubusercontent.com/catppuccin/catppuccin/main/assets/previews/latte.webp"/>
</details>
<details>
<summary>🪴 Frappé</summary>
<img src="https://raw.githubusercontent.com/catppuccin/catppuccin/main/assets/previews/frappe.webp"/>
</details>
<details>
<summary>🌺 Macchiato</summary>
<img src="https://raw.githubusercontent.com/catppuccin/catppuccin/main/assets/previews/macchiato.webp"/>
</details>
<details>
<summary>🌿 Mocha</summary>
<img src="https://raw.githubusercontent.com/catppuccin/catppuccin/main/assets/previews/mocha.webp"/>
</details>
## Usage
1. Clone this repository locally
2. Open the app's settings
3. Select `import theme` and browse to where you cloned Catppuccin
4. Select it
```sh
nimble install https://github.com/catppuccin/nim
```
The `catppuccin` nim library was designed to interface with [`treeform/chroma`](https://github.com/treeform/chroma), however it is not required for basic usage. Some of the basic color types and transformations have been ported from `chroma`
If you do wish to access `catppuccin` colors alongside `chroma` compile with `-d:inheritChroma` (see `./examples/use_chroma.nim`) to use the color types defined by `chroma`.
### Example
```nim
import catppuccin
echo mocha.rosewater.color().toHex()
```
<!-- this section is optional -->
## 🙋 FAQ
- Q: **_"Where can I find the doc?"_**\
A: Run `nim mkDocs`
A: Run `nimble docs`
## 💝 Thanks to

35
catppuccin.nimble Normal file
View file

@ -0,0 +1,35 @@
import std/[os,strformat]
# Package
version = "0.1.0"
author = "Daylin Morgan"
description = "Soothing pastel theme for nim"
license = "MIT"
srcDir = "src"
# Dependencies
requires "nim >= 1.6.12"
task gen, "generate src/catppuccin/palette.nim":
let paletteJson = "./tools/palette-porcelain.json"
let srcUrl = "https://raw.githubusercontent.com/catppuccin/palette/main/palette-porcelain.json"
if not fileExists(paletteJson): exec &"wget -O {paletteJson} {srcUrl}"
exec "nim r ./tools/generate.nim"
task docs, "Deploy doc html + search index to public/ directory":
let
deployDir = getCurrentDir() / "public"
pkgName = "catppuccin"
srcFile = getCurrentDir() / "src" / (pkgName & ".nim")
gitUrl = "https://github.com/daylinmorgan/catppuccin-nim"
selfExec &"doc --index:on --git.url:{gitUrl} --outdir:{deployDir} --project {srcFile}"
withDir deployDir:
mvFile(pkgName & ".html", "index.html")
for file in walkDirRec(".", {pcFile}):
# As we renamed the file, we need to rename that in hyperlinks
exec(r"sed -i -r 's|$1\.html|index.html|g' $2" % [pkgName, file])
# drop 'src/' from titles
exec(r"sed -i -r 's/<(.*)>src\//<\1>/' $1" % file)

2
examples/config.nims Normal file
View file

@ -0,0 +1,2 @@
switch("path", "$projectDir/../src")
switch("hints", "off")

28
examples/term.nim Normal file
View file

@ -0,0 +1,28 @@
import std/[strformat]
import catppuccin
const ansiReset = "\e[0m"
proc ansi(s: string, c: ColorRGB): string =
let code = &"\e[48;2;{c.r};{c.g};{c.b}m"
result.add(code)
result.add(s)
result.add(ansiReset)
when isMainModule:
let flavors = @[
("latte", latte),
("frappe", frappe),
("macchiato", macchiato),
("mocha", mocha)
]
for (name, flavor) in flavors:
echo name
for name, color in flavor.fieldPairs():
write(stdout, " ".ansi(color))
write(stdout, "\n\n")

11
examples/use_chroma.nim Normal file
View file

@ -0,0 +1,11 @@
{.define: inheritChroma.}
import std/[strutils]
import catppuccin
when isMainModule:
echo "Mocha colors as CMYK"
for n, c in mocha.fieldPairs():
echo alignLeft(n, 9) & " -> " & $c.color().asCmyk()

18
src/catppuccin.nim Normal file
View file

@ -0,0 +1,18 @@
when not defined(inheritChroma):
import catppuccin/chroma
else:
import chroma
type
Flavor = object
rosewater*, flamingo*, pink*, mauve*, red*, maroon*, peach*, yellow*,
green*, teal*, sky*, sapphire*, blue*, lavender*, text*, subtext1*,
subtext0*, overlay2*,
overlay1*, surface2*, surface1*, surface0*, base*, mantle*,
crust*: ColorRGB
include catppuccin/palette
export mocha, latte, macchiato, frappe
export chroma

325
src/catppuccin/chroma.nim Normal file
View file

@ -0,0 +1,325 @@
import std/[hashes, math, strutils]
## standalone types/methods ported from treeform/chroma
##
# chroma/colortypes ---------------
type
Color* = object
## Main color type, float32 points
r*: float32 ## red (0-1)
g*: float32 ## green (0-1)
b*: float32 ## blue (0-1)
a*: float32 ## alpha (0-1, 0 is fully transparent)
# Color Space: rgb
ColorRGB* = object
## Color stored as 3 uint8s
r*: uint8 ## Red 0-255
g*: uint8 ## Green 0-255
b*: uint8 ## Blue 0-255
# Color Space: rgba
ColorRGBA* = object
## Color stored as 4 uint8s
r*: uint8 ## Red 0-255
g*: uint8 ## Green 0-255
b*: uint8 ## Blue 0-255
a*: uint8 ## Alpha 0-255
ColorRGBX* = object
## Premultiplied alpha RGBA color stored as 4 uint8s
r*: uint8 ## Red 0-a
g*: uint8 ## Green 0-a
b*: uint8 ## Blue 0-a
a*: uint8 ## Alpha 0-255
# Color Space: HSL
ColorHSL* = object
## HSL attempts to resemble more perceptual color models
h*: float32 ## hue 0 to 360
s*: float32 ## saturation 0 to 100
l*: float32 ## lightness 0 to 100
SomeColor* = Color|ColorRGB|ColorRGBA|ColorHSL
InvalidColor* = object of ValueError
proc color*(r, g, b: float32, a: float32 = 1.0): Color {.inline.} =
## Creates from floats like:
## * color(1,0,0) -> red
## * color(0,1,0) -> green
## * color(0,0,1) -> blue
## * color(0,0,0,1) -> opaque black
## * color(0,0,0,0) -> transparent black
Color(r: r, g: g, b: b, a: a)
proc rgb*(r, g, b: uint8): ColorRGB {.inline.} =
## Creates from uint8s like:
## * rgba(255,0,0) -> red
## * rgba(0,255,0) -> green
## * rgba(0,0,255) -> blue
ColorRGB(r: r, g: g, b: b)
proc rgba*(r, g, b, a: uint8): ColorRGBA {.inline.} =
## Creates from uint8s like:
## * rgba(255,0,0,255) -> red
## * rgba(0,255,0,255) -> green
## * rgba(0,0,255,255) -> blue
## * rgba(0,0,0,255) -> opaque black
## * rgba(0,0,0,0) -> transparent black
ColorRGBA(r: r, g: g, b: b, a: a)
proc hsl*(h, s, l: float32): ColorHSL {.inline.} =
ColorHSL(h: h, s: s, l: l)
# chroma/colortypes ---------------
# chroma/transformations ----------
proc rgba*(c: ColorRGBX): ColorRGBA {.inline.} =
## Convert a premultiplied alpha RGBA to a straight alpha RGBA.
result.r = c.r
result.g = c.g
result.b = c.b
result.a = c.a
if result.a != 0 and result.a != 255:
let multiplier = round((255 / c.a.float32) * 255).uint32
result.r = ((result.r.uint32 * multiplier + 127) div 255).uint8
result.g = ((result.g.uint32 * multiplier + 127) div 255).uint8
result.b = ((result.b.uint32 * multiplier + 127) div 255).uint8
proc rgb*(c: Color): ColorRGB {.inline.} =
## Convert Color to ColorRGB
result.r = round(c.r * 255).uint8
result.g = round(c.g * 255).uint8
result.b = round(c.b * 255).uint8
proc color*(c: ColorRGB): Color {.inline.} =
## Convert ColorRGB to Color
result.r = float32(c.r) / 255
result.g = float32(c.g) / 255
result.b = float32(c.b) / 255
result.a = 1.0
proc rgba*(c: Color): ColorRGBA {.inline.} =
## Convert Color to ColorRGBA
result.r = round(c.r * 255).uint8
result.g = round(c.g * 255).uint8
result.b = round(c.b * 255).uint8
result.a = round(c.a * 255).uint8
proc color*(c: ColorRGBA): Color {.inline.} =
## Convert ColorRGBA to Color
result.r = float32(c.r) / 255
result.g = float32(c.g) / 255
result.b = float32(c.b) / 255
result.a = float32(c.a) / 255
proc min3(a, b, c: float32): float32 {.inline.} = min(a, min(b, c))
proc max3(a, b, c: float32): float32 {.inline.} = max(a, max(b, c))
proc hsl*(c: Color): ColorHSL =
## convert Color to ColorHSL
let
min = min3(c.r, c.g, c.b)
max = max3(c.r, c.g, c.b)
delta = max - min
if max == min:
result.h = 0.0
elif c.r == max:
result.h = (c.g - c.b) / delta
elif c.g == max:
result.h = 2 + (c.b - c.r) / delta
elif c.b == max:
result.h = 4 + (c.r - c.g) / delta
result.h = min(result.h * 60, 360)
if result.h < 0:
result.h += 360
result.l = (min + max) / 2
if max == min:
result.s = 0
elif result.l <= 0.5:
result.s = delta / (max + min)
else:
result.s = delta / (2 - max - min)
result.s *= 100
result.l *= 100
func fixupColor[T: int | float32](r, g, b: var T): bool =
## performs a fixup of the given r, g, b values and returnes whether
## any of the values was modified.
## This func works on integers or floats. It is only used within the
## conversion of `Color -> ColorHCL` (on integers) and `ColorHCL -> Color`
## (on floats).
template fixC(c: untyped): untyped =
if c < T(0):
c = T(0)
result = true
when T is int:
if c > 255:
c = 255
result = true
else:
if c > 1.0:
c = 1.0
result = true
fixC(r)
fixC(g)
fixC(b)
# overload working on `var Color`. It's `discardable`, because in our usage
# here we do not really care whether a value was modified.
func fixupColor(c: var Color): bool {.inline, discardable.} =
fixupColor(c.r, c.g, c.b)
proc color*(c: ColorHSL): Color =
## convert ColorHSL to Color
let
h = c.h / 360
s = c.s / 100
l = c.l / 100
var t1, t2, t3: float32
if s == 0.0:
return color(l, l, l)
if l < 0.5:
t2 = l * (1 + s)
else:
t2 = l + s - l * s
t1 = 2 * l - t2
var rgb: array[3, float32]
for i in 0..2:
t3 = h + 1.0 / 3.0 * - (float32(i) - 1.0)
if t3 < 0:
t3 += 1
elif t3 > 1:
t3 -= 1
var val: float32
if 6 * t3 < 1:
val = t1 + (t2 - t1) * 6 * t3
elif 2 * t3 < 1:
val = t2
elif 3 * t3 < 2:
val = t1 + (t2 - t1) * (2 / 3 - t3) * 6
else:
val = t1
rgb[i] = val
result.r = rgb[0]
result.g = rgb[1]
result.b = rgb[2]
result.a = 1.0
fixupColor(result)
result
proc color*(c: Color): Color {.inline.} =
c
proc to*[T: SomeColor](c: SomeColor, toColor: typedesc[T]): T {.inline.} =
## Allows conversion of transformation of a color in any color space into any
## other color space.
when type(c) is T:
c
else:
when toColor is Color:
c.color
elif toColor is ColorRGB:
c.color.rgb
elif toColor is ColorRGBA:
c.color.rgba
elif toColor is ColorHSL:
c.color.hsl
proc asColor*(c: SomeColor): Color {.inline.} = c.to(Color)
proc asRgb*(c: SomeColor): ColorRGB {.inline.} = c.to(ColorRGB)
proc asHsl*(c: SomeColor): ColorHSL {.inline.} = c.to(ColorHSL)
# chroma/transformations ----------
# chroma --------------------------
proc toHex(a: float32): string {.inline.} = toHex(int(a))
proc `$`*(c: Color): string =
## Returns colors as "(r, g, b, a)".
"(" & $c.r & ", " & $c.g & ", " & $c.b & ", " & $c.a & ")"
func hash*(c: Color): Hash =
## Hashes a Color - used in tables.
hash((c.r, c.g, c.b, c.a))
func hash*(c: ColorRGB): Hash =
## Hashes a ColorRGB - used in tables.
hash((c.r, c.g, c.b))
func hash*(c: ColorRGBA): Hash =
## Hashes a ColorRGB - used in tables.
hash((c.r, c.g, c.b, c.a))
func hash*(c: ColorHSL): Hash =
## Hashes a ColorHSL - used in tables.
hash((c.h, c.s, c.l))
proc toHex*(c: Color): string =
## Formats color as hex (upper case):
## * red -> FF0000
## * blue -> 0000FF
## * white -> FFFFFF
template pair(n: float32): string =
toHex(n*255)[^2..^1]
pair(c.r) & pair(c.g) & pair(c.b)
proc toHexAlpha*(c: Color): string =
## Formats color as hex (upper case):
## * red -> FF0000FF
## * blue -> 0000FFFF
## * white -> FFFFFFFF
## * opaque black -> 000000FF
## * transparent black -> 00000000
template pair(n: float32): string =
toHex(n*255)[^2..^1]
pair(c.r) & pair(c.g) & pair(c.b) & pair(c.a)
proc toHtmlHex*(c: Color): string =
## Formats color as HTML hex (upper case):
## * red -> #FF0000
## * blue -> #0000FF
## * white -> #FFFFFF
'#' & c.toHex()
proc toHtmlHexTiny*(c: Color): string =
## Formats color as HTML 3 hex numbers (upper case):
## * red -> #F00
## * blue -> #00F
## * white -> #FFF
proc pair(n: float32): string =
toHex(n*15)[^1..^1]
return '#' & pair(c.r) & pair(c.g) & pair(c.b)
proc toHtmlRgb*(c: Color): string =
## Parses colors in html's rgb format:
## * red -> rgb(255, 0, 0)
## * blue -> rgb(0,0,255)
## * white -> rgb(255,255,255)
"rgb(" &
$round(c.r * 255).int & ", " &
$round(c.g * 255).int & ", " &
$round(c.b * 255).int &
")"
proc toHtmlRgba*(c: Color): string =
## Parses colors in html's rgb format:
## * red -> rgb(255, 0, 0)
## * blue -> rgb(0,0,255)
## * white -> rgb(255,255,255)
"rgba(" &
$round(c.r * 255).int & ", " &
$round(c.g * 255).int & ", " &
$round(c.b * 255).int & ", " &
$c.a &
")"

112
src/catppuccin/palette.nim Normal file
View file

@ -0,0 +1,112 @@
# DO NOT EDIT this file is autogenerated by tools/generate.nim!
const
latte* = Flavor(
rosewater: ColorRGB(r: 220, g: 138, b: 120),
flamingo: ColorRGB(r: 221, g: 120, b: 120),
pink: ColorRGB(r: 234, g: 118, b: 203),
mauve: ColorRGB(r: 136, g: 57, b: 239),
red: ColorRGB(r: 210, g: 15, b: 57),
maroon: ColorRGB(r: 230, g: 69, b: 83),
peach: ColorRGB(r: 254, g: 100, b: 11),
yellow: ColorRGB(r: 223, g: 142, b: 29),
green: ColorRGB(r: 64, g: 160, b: 43),
teal: ColorRGB(r: 23, g: 146, b: 153),
sky: ColorRGB(r: 4, g: 165, b: 229),
sapphire: ColorRGB(r: 32, g: 159, b: 181),
blue: ColorRGB(r: 30, g: 102, b: 245),
lavender: ColorRGB(r: 114, g: 135, b: 253),
text: ColorRGB(r: 76, g: 79, b: 105),
subtext1: ColorRGB(r: 92, g: 95, b: 119),
subtext0: ColorRGB(r: 108, g: 111, b: 133),
overlay2: ColorRGB(r: 124, g: 127, b: 147),
overlay1: ColorRGB(r: 140, g: 143, b: 161),
surface2: ColorRGB(r: 172, g: 176, b: 190),
surface1: ColorRGB(r: 188, g: 192, b: 204),
surface0: ColorRGB(r: 204, g: 208, b: 218),
base: ColorRGB(r: 239, g: 241, b: 245),
mantle: ColorRGB(r: 230, g: 233, b: 239),
crust: ColorRGB(r: 220, g: 224, b: 232)
)
frappe* = Flavor(
rosewater: ColorRGB(r: 242, g: 213, b: 207),
flamingo: ColorRGB(r: 238, g: 190, b: 190),
pink: ColorRGB(r: 244, g: 184, b: 228),
mauve: ColorRGB(r: 202, g: 158, b: 230),
red: ColorRGB(r: 231, g: 130, b: 132),
maroon: ColorRGB(r: 234, g: 153, b: 156),
peach: ColorRGB(r: 239, g: 159, b: 118),
yellow: ColorRGB(r: 229, g: 200, b: 144),
green: ColorRGB(r: 166, g: 209, b: 137),
teal: ColorRGB(r: 129, g: 200, b: 190),
sky: ColorRGB(r: 153, g: 209, b: 219),
sapphire: ColorRGB(r: 133, g: 193, b: 220),
blue: ColorRGB(r: 140, g: 170, b: 238),
lavender: ColorRGB(r: 186, g: 187, b: 241),
text: ColorRGB(r: 198, g: 208, b: 245),
subtext1: ColorRGB(r: 181, g: 191, b: 226),
subtext0: ColorRGB(r: 165, g: 173, b: 206),
overlay2: ColorRGB(r: 148, g: 156, b: 187),
overlay1: ColorRGB(r: 131, g: 139, b: 167),
surface2: ColorRGB(r: 98, g: 104, b: 128),
surface1: ColorRGB(r: 81, g: 87, b: 109),
surface0: ColorRGB(r: 65, g: 69, b: 89),
base: ColorRGB(r: 48, g: 52, b: 70),
mantle: ColorRGB(r: 41, g: 44, b: 60),
crust: ColorRGB(r: 35, g: 38, b: 52)
)
macchiato* = Flavor(
rosewater: ColorRGB(r: 244, g: 219, b: 214),
flamingo: ColorRGB(r: 240, g: 198, b: 198),
pink: ColorRGB(r: 245, g: 189, b: 230),
mauve: ColorRGB(r: 198, g: 160, b: 246),
red: ColorRGB(r: 237, g: 135, b: 150),
maroon: ColorRGB(r: 238, g: 153, b: 160),
peach: ColorRGB(r: 245, g: 169, b: 127),
yellow: ColorRGB(r: 238, g: 212, b: 159),
green: ColorRGB(r: 166, g: 218, b: 149),
teal: ColorRGB(r: 139, g: 213, b: 202),
sky: ColorRGB(r: 145, g: 215, b: 227),
sapphire: ColorRGB(r: 125, g: 196, b: 228),
blue: ColorRGB(r: 138, g: 173, b: 244),
lavender: ColorRGB(r: 183, g: 189, b: 248),
text: ColorRGB(r: 202, g: 211, b: 245),
subtext1: ColorRGB(r: 184, g: 192, b: 224),
subtext0: ColorRGB(r: 165, g: 173, b: 203),
overlay2: ColorRGB(r: 147, g: 154, b: 183),
overlay1: ColorRGB(r: 128, g: 135, b: 162),
surface2: ColorRGB(r: 91, g: 96, b: 120),
surface1: ColorRGB(r: 73, g: 77, b: 100),
surface0: ColorRGB(r: 54, g: 58, b: 79),
base: ColorRGB(r: 36, g: 39, b: 58),
mantle: ColorRGB(r: 30, g: 32, b: 48),
crust: ColorRGB(r: 24, g: 25, b: 38)
)
mocha* = Flavor(
rosewater: ColorRGB(r: 245, g: 224, b: 220),
flamingo: ColorRGB(r: 242, g: 205, b: 205),
pink: ColorRGB(r: 245, g: 194, b: 231),
mauve: ColorRGB(r: 203, g: 166, b: 247),
red: ColorRGB(r: 243, g: 139, b: 168),
maroon: ColorRGB(r: 235, g: 160, b: 172),
peach: ColorRGB(r: 250, g: 179, b: 135),
yellow: ColorRGB(r: 249, g: 226, b: 175),
green: ColorRGB(r: 166, g: 227, b: 161),
teal: ColorRGB(r: 148, g: 226, b: 213),
sky: ColorRGB(r: 137, g: 220, b: 235),
sapphire: ColorRGB(r: 116, g: 199, b: 236),
blue: ColorRGB(r: 137, g: 180, b: 250),
lavender: ColorRGB(r: 180, g: 190, b: 254),
text: ColorRGB(r: 205, g: 214, b: 244),
subtext1: ColorRGB(r: 186, g: 194, b: 222),
subtext0: ColorRGB(r: 166, g: 173, b: 200),
overlay2: ColorRGB(r: 147, g: 153, b: 178),
overlay1: ColorRGB(r: 127, g: 132, b: 156),
surface2: ColorRGB(r: 88, g: 91, b: 112),
surface1: ColorRGB(r: 69, g: 71, b: 90),
surface0: ColorRGB(r: 49, g: 50, b: 68),
base: ColorRGB(r: 30, g: 30, b: 46),
mantle: ColorRGB(r: 24, g: 24, b: 37),
crust: ColorRGB(r: 17, g: 17, b: 27)
)

1
tests/config.nims Normal file
View file

@ -0,0 +1 @@
switch("path", "$projectDir/../src")

9
tests/test1.nim Normal file
View file

@ -0,0 +1,9 @@
import unittest
import catppuccin
test "color":
check mocha.rosewater == ColorRGB(r: 245, g: 224, b: 220)
test "convert":
check mocha.rosewater.color().toHex() == "F5E0DC"

46
tools/generate.nim Normal file
View file

@ -0,0 +1,46 @@
import std/[json, strformat, strutils]
type
Color = object
hex: string
rgb: array[3, int]
hsl: array[3, float]
Flavor = object
rosewater, flamingo, pink, mauve, red, maroon, peach, yellow, green, teal,
sky, sapphire, blue, lavender, text, subtext1, subtext0, overlay2,
overlay1, surface2, surface1, surface0, base, mantle, crust: Color
Palette = object
latte, frappe, macchiato, mocha: Flavor
proc createColor(c: Color, name: string): string =
result = &"""
{name}: ColorRGB(r: {c.rgb[0]}, g: {c.rgb[1]}, b: {c.rgb[2]})"""
proc createFlavor(f: Flavor, name: string): string =
var colorsDef: seq[string]
for name, color in f.fieldPairs():
colorsDef.add color.createColor(name)
result = &"""
{name}* = Flavor(
{colorsDef.join(",\n")}
)"""
when isMainModule:
const paletteJsonStr = slurp "./palette-porcelain.json"
const doNotEdit = "# DO NOT EDIT this file is autogenerated by tools/generate.nim!"
let palette = paletteJsonStr.parseJson().to(Palette)
var flavors: string
for name, flavor in palette.fieldPairs():
flavors &= flavor.createFlavor(name) & "\n"
let catppuccin = &"""
{doNotEdit}
const
{flavors}
"""
writeFile("./src/catppuccin/palette.nim", catppuccin)