8 Commits
1.5.1 ... 1.5.5

Author SHA1 Message Date
6e684a9c4e fix scheme mapping for magenta 2026-03-05 13:40:17 -08:00
Kevin Hovsäter
1dcf75a360 Add support for Ghostty 2026-03-04 20:18:25 -08:00
b580f47679 sync version with pypi 2026-02-03 21:26:52 -08:00
b2e80a9f35 bump generated config versions 2026-02-02 19:24:17 -08:00
534f2423e9 track missing parameter file in package data 2026-01-28 01:29:53 -08:00
b5b9b12174 update figure references to latest in README 2026-01-22 20:00:00 -08:00
4ee9e23c5c implement consistent quantization of srgb8 for palette figure 2026-01-21 18:22:30 -08:00
a5256aaa62 clean up plotting method signatures 2026-01-16 16:34:00 -08:00
53 changed files with 161 additions and 48 deletions

1
.gitignore vendored
View File

@@ -17,7 +17,6 @@ docs/_build/
# local
/Makefile
data/
archive/
notebooks/color_spaces_manyview.ipynb

View File

@@ -32,11 +32,11 @@ smoothly as a function of lightness within sRGB gamut bounds.
| Chroma curves | Color trajectories |
|----------------------------------------------------------|------------------------------------------|
| ![Chroma curves](images/release/1.5.1/chroma-curves.png) | ![Trajectories](images/trajectories.gif) |
| ![Chroma curves](images/release/1.5.4/chroma-curves.png) | ![Trajectories](images/trajectories.gif) |
| Palette |
|----------------------------------------------|
| ![Palette](images/release/1.5.1/palette.png) |
| ![Palette](images/release/1.5.4/palette.png) |
Chroma curves are designed specifically to establish a distinct role for each
accent and are non-intersecting over the lightness domain (hence the distinct
@@ -143,16 +143,23 @@ using the `monobiome` CLI:
(`grassland`). Every part of this process can be customized: the scheme
parameters, the scheme definitions/file, the app template.
The separation of duties here facilitates robustness: the palette colors can be
tweaked (across versions, say) without changing how those colors get used in
app themes. Scheme files don't hardcode color values, and app templates don't
refer to palette variables at all. Scheme files bridge palette colors with
*intended uses*, and templates need only align those uses under an app's
expected config syntax.
Running these commands in sequence from the repo root should work
out-of-the-box after having installed the CLI tool.
## Applications
This repo provides palette-agnostic theme templates for `kitty`,
`vim`/`neovim`, and `fzf` in the `templates/` directory. Pre-generated
*concrete* themes can be found in `app-config/`, if you'd like to try an
example out-of-the-box without using the `monobiome` CLI. Raw
palette colors can be found in `colors/` if you want to use them to define
static themes for other applications.
This repo provides palette-agnostic theme templates for `ghostty`,
`kitty`, `vim`/`neovim`, and `fzf` in the `templates/` directory.
Pre-generated *concrete* themes can be found in `app-config/`, if you'd
like to try an example out-of-the-box without using the `monobiome` CLI.
Raw palette colors can be found in `colors/` if you want to use them to
define static themes for other applications.
Themes files in the `app-config/` directory are generated for light and dark
modes of each biome, and named according to the following pattern:

View File

@@ -1,4 +1,4 @@
version = "1.5.1"
version = "1.5.4"
[alpine]
l10 = "#030303"

View File

@@ -1,4 +1,4 @@
version = "1.5.1"
version = "1.5.4"
[alpine]
l10 = "oklch(10.0% 0.0000 0.0)"

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -111,7 +111,7 @@ def handle_scheme(args: Namespace, parser: ArgumentParser) -> None:
"cyan": "cyan",
"blue": "blue",
"violet": "violet",
"magenta": "orange",
"magenta": "magenta",
}
term_color_map = {
"red": "red",

View File

@@ -0,0 +1,65 @@
L_min = 10
L_max = 98
L_step = 5
[monotone_C_map]
alpine = 0
badlands = 0.011
chaparral = 0.011
savanna = 0.011
grassland = 0.011
reef = 0.011
tundra = 0.011
heathland = 0.011
moorland = 0.011
[h_weights]
red = 3.0
orange = 3.8
yellow = 3.8
green = 3.8
cyan = 3.8
blue = 3.6
violet = 3.0
magenta = 3.6
[h_L_offsets]
red = 0
orange = -5.5
yellow = -13.5
green = -12.5
cyan = -10.5
blue = 9
violet = 7
magenta = 2
[h_C_offsets]
red = 0
orange = -0.015
yellow = -0.052
green = -0.08
cyan = -0.009
blue = -0.01
violet = -0.047
magenta = -0.1
[monotone_h_map]
alpine = 0
badlands = 29
chaparral = 62.5
savanna = 104
grassland = 148
reef = 205
tundra = 262
heathland = 306
moorland = 350
[accent_h_map]
red = 29
orange = 62.5
yellow = 104
green = 148
cyan = 205
blue = 262
violet = 306
magenta = 350

View File

@@ -5,6 +5,10 @@ from importlib.metadata import version
from coloraide import Color
from monobiome.util import (
hex_from_rgb8,
srgb8_from_color,
)
from monobiome.constants import (
h_map,
L_points,
@@ -24,8 +28,8 @@ def compute_hlc_map(notation: str) -> dict[str, Any]:
oklch = Color('oklch', [_l/100, _c, _h])
if notation == "hex":
srgb = oklch.convert('srgb')
c_str = srgb.to_string(hex=True)
rgb8 = srgb8_from_color(oklch)
c_str = hex_from_rgb8(rgb8)
elif notation == "oklch":
ol, oc, oh = oklch.convert('oklch').coords()
c_str = f"oklch({ol*100:.1f}% {oc:.4f} {oh:.1f})"

View File

@@ -5,6 +5,7 @@ import matplotlib.pyplot as plt
from coloraide import Color
from matplotlib.collections import LineCollection
from monobiome.util import srgb8_from_color
from monobiome.palette import compute_hlc_map
from monobiome.constants import (
h_map,
@@ -72,13 +73,12 @@ def plot_hue_chroma_bounds() -> tuple[plt.Figure, plt.Axes]:
return fig, axes
def plot_hue_chroma_star() -> tuple[plt.Figure, plt.Axes]:
fig, ax = plt.subplots(1, 1, figsize=(8, 6))
# uncomment to preview 5 core term colors
colors = accent_h_map.keys()
#colors = set(["red", "orange", "yellow", "green", "blue"])
# uncomment to preview just the 5 core term colors
# colors = set(["red", "orange", "yellow", "green", "blue"])
for h_str, _ in max_Cstar_Horder:
Lpoints_Cstar = Lpoints_Cstar_Hmap[h_str]
@@ -88,23 +88,11 @@ def plot_hue_chroma_star() -> tuple[plt.Figure, plt.Axes]:
_h = h_map[h_str]
h_colors = [
Color('oklch', [_l/100, _c, _h]).convert("srgb")
Color(
'oklch', [_l/100, _c, _h]
).convert("srgb").fit(method="oklch-chroma")
for _l, _c in zip(L_points, Lpoints_Cstar, strict=True)
]
# # ax.fill_between(
# ax.scatter(
# L_points,
# Lpoints_Cstar,
# alpha=0.7,
# c=h_colors,
# #alpha=0.2,
# #color='grey',
# label=h_str
# )
# x, y = L_points, Lpoints_Cstar_Hmap[h_str]
# n = int(0.45*len(x))
# ax.text(x[n], y[n]-0.01, h_str, rotation=10, va='center', ha='left')
x = np.asarray(L_points)
y = np.asarray(Lpoints_Cstar)
@@ -112,10 +100,10 @@ def plot_hue_chroma_star() -> tuple[plt.Figure, plt.Axes]:
segs = np.concatenate([pts[:-1], pts[1:]], axis=1)
rgb = np.asarray(h_colors)
seg_colors = (rgb[:-1] + rgb[1:]) / 2
lc = LineCollection(segs, colors=seg_colors, linewidth=3,
capstyle="round", joinstyle="round",
label=h_str)
ax.add_collection(lc)
ax.autoscale_view()
@@ -126,12 +114,11 @@ def plot_hue_chroma_star() -> tuple[plt.Figure, plt.Axes]:
return fig, ax
def palette_image(
palette: dict[str, dict[int, str]],
cell_size: int = 40,
keys: list[str] | None = None
) -> tuple[np.ndarray, list[str], list[list[int]], int, int]:
) -> tuple[np.ndarray, list[str], list[list[int]]]:
names = list(palette.keys()) if keys is None else keys
row_count = len(names)
@@ -140,7 +127,7 @@ def palette_image(
h = row_count * cell_size
w = max_cols * cell_size
img = np.ones((h, w, 3), float)
img = np.ones((h, w, 3), int)
lightness_keys_per_row = []
@@ -149,14 +136,12 @@ def palette_image(
lkeys = sorted(shades.keys())
lightness_keys_per_row.append(lkeys)
for c, k in enumerate(lkeys):
col = Color(shades[k]).convert("srgb").fit(method="clip")
rgb = [col["r"], col["g"], col["b"]]
rgb = srgb8_from_color(shades[k])
r0, r1 = r * cell_size, (r + 1) * cell_size
c0, c1 = c * cell_size, (c + 1) * cell_size
img[r0:r1, c0:c1, :] = rgb
return img, names, lightness_keys_per_row, cell_size, max_cols
return img, names, lightness_keys_per_row
def show_palette(
palette: dict[str, dict[int, str]],
@@ -165,9 +150,7 @@ def show_palette(
show_labels: bool = True,
dpi: int = 100,
) -> tuple[plt.Figure, plt.Axes]:
img, names, keys, cell_size, max_cols = palette_image(
palette, cell_size, keys=keys
)
img, names, _ = palette_image(palette, cell_size, keys=keys)
fig_w = img.shape[1] / 100
fig_h = img.shape[0] / 100
@@ -175,7 +158,7 @@ def show_palette(
if show_labels:
fig, ax = plt.subplots(figsize=(fig_w, fig_h), dpi=dpi)
ax.imshow(img, interpolation="none", origin="upper")
ax.imshow(img, interpolation="nearest", origin="upper")
ax.set_xticks([])
ytick_pos = [(i + 0.5) * cell_size for i in range(len(names))]

View File

@@ -2,6 +2,7 @@ import math
from types import GenericAlias
from argparse import ArgumentParser, _SubParsersAction
import numpy as np
from coloraide import Color
_SubParsersAction.__class_getitem__ = classmethod(GenericAlias)
@@ -33,3 +34,13 @@ def oklch_distance(xc: Color, yc: Color) -> float:
dz = l1 - l2
return (dx**2 + dy**2 + dz**2)**0.5
def srgb8_from_color(c: str | Color) -> np.ndarray:
c = Color(c).convert("srgb").fit(method="oklch-chroma")
rgb = np.array([c["r"], c["g"], c["b"]], dtype=float)
rgb8 = np.clip(np.round(rgb * 255), 0, 255).astype(np.uint8)
return rgb8
def hex_from_rgb8(rgb8: np.ndarray) -> str:
return f"#{int(rgb8[0]):02x}{int(rgb8[1]):02x}{int(rgb8[2]):02x}"

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "monobiome"
version = "1.5.1"
version = "1.5.5"
description = "Monobiome color palette"
requires-python = ">=3.12"
authors = [

View File

@@ -17,8 +17,8 @@ fig.savefig(Path(figure_dir, "chroma-curves.png"))
# "oklch" causes some slight hex drift when later using an eyedropper
hlc_map = compute_hlc_map("hex") # ("oklch")
fig, ax = plotting.show_palette(hlc_map)
fig.savefig(Path(figure_dir, "palette.png"))
fig, ax = plotting.show_palette(hlc_map, cell_size=25)
fig.savefig(Path(figure_dir, "palette.png"), pad_inches=0)
fig, ax = plotting.show_palette(hlc_map, show_labels=False)
fig.savefig(Path(figure_dir, "palette-bare.png"), pad_inches=0)

View File

@@ -11,3 +11,6 @@ uv run monobiome palette -n oklch -f toml -o colors/oklch-palette.toml
# generate provided app config
"$script_dir/generate.sh"
# generate release plots
uv run "$script_dir/plots.py"

41
templates/ghostty/config Normal file
View File

@@ -0,0 +1,41 @@
# base settings
background = f{{term.background}}
foreground = f{{term.foreground}}
selection-background = f{{term.selection_bg}}
selection-foreground = f{{term.selection_fg}}
cursor-color = f{{term.cursor}}
cursor-text = f{{term.cursor_text}}
# black
palette = 0=f{{term.normal.black}}
palette = 8=f{{term.bright.black}}
# red
palette = 1=f{{term.normal.red}}
palette = 9=f{{term.bright.red}}
# green
palette = 2=f{{term.normal.green}}
palette = 10=f{{term.bright.green}}
# yellow
palette = 3=f{{term.normal.yellow}}
palette = 11=f{{term.bright.yellow}}
# blue
palette = 4=f{{term.normal.blue}}
palette = 12=f{{term.bright.blue}}
# magenta (red)
palette = 5=f{{term.normal.magenta}}
palette = 13=f{{term.bright.magenta}}
# cyan (blue)
palette = 6=f{{term.normal.cyan}}
palette = 14=f{{term.bright.cyan}}
# white
palette = 7=f{{term.normal.white}}
palette = 15=f{{term.bright.white}}

2
uv.lock generated
View File

@@ -882,7 +882,7 @@ wheels = [
[[package]]
name = "monobiome"
version = "1.5.1"
version = "1.5.5"
source = { editable = "." }
dependencies = [
{ name = "coloraide" },