diff --git a/projects/khora/src/khora/colors.py b/projects/khora/src/khora/colors.py new file mode 100644 index 00000000..176c4ae8 --- /dev/null +++ b/projects/khora/src/khora/colors.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +import re + +_DEFAULT = "#3584e4" +_ANSI = ( + "#241f31", + "#c01c28", + "#26a269", + "#a2734c", + "#1c71d8", + "#a347ba", + "#2aa1b3", + "#deddda", + "#5e5c64", + "#ed333b", + "#57e389", + "#f8e45c", + "#62a0ea", + "#c061cb", + "#5bc8d7", + "#ffffff", +) +_NAMED = dict( + zip( + ( + "black", + "dark red", + "dark green", + "brown", + "dark blue", + "dark magenta", + "dark cyan", + "white", + "dark gray", + "light red", + "light green", + "yellow", + "light blue", + "light magenta", + "light cyan", + "light gray", + ), + _ANSI, + strict=True, + ) +) +_HEX_COLOR = re.compile(r"#[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3})?(?:[0-9a-fA-F]{2})?") + + +def display_color(value: str | None) -> str: + """Turn khal's terminal-oriented color values into GTK-friendly hex.""" + if not value or value == "auto": + return _DEFAULT + if _HEX_COLOR.fullmatch(value): + return value + if value in _NAMED: + return _NAMED[value] + if value.isdigit() and 0 <= (index := int(value)) <= 255: + return _xterm_color(index) + return _DEFAULT + + +def _xterm_color(index: int) -> str: + if index < 16: + return _ANSI[index] + if index < 232: + index -= 16 + levels = (0, 95, 135, 175, 215, 255) + red, remainder = divmod(index, 36) + green, blue = divmod(remainder, 6) + return f"#{levels[red]:02x}{levels[green]:02x}{levels[blue]:02x}" + gray = 8 + (index - 232) * 10 + return f"#{gray:02x}{gray:02x}{gray:02x}" diff --git a/projects/khora/src/khora/window.py b/projects/khora/src/khora/window.py index c98dbc26..363d8500 100644 --- a/projects/khora/src/khora/window.py +++ b/projects/khora/src/khora/window.py @@ -8,6 +8,7 @@ gi.require_version("Adw", "1") gi.require_version("Gtk", "4.0") from gi.repository import Adw, Gio, GLib, Gtk +from .colors import display_color from .khal_adapter import KhalRepository from .model import Event @@ -83,6 +84,7 @@ class KhoraWindow(Adw.ApplicationWindow): toggle = Gtk.Switch(active=True, valign=Gtk.Align.CENTER) toggle.connect("notify::active", self._on_calendar_toggled, calendar.name) row = Adw.ActionRow(title=calendar.name, activatable=True) + row.add_prefix(self._color_dot(calendar.color)) row.add_suffix(toggle) row.set_activatable_widget(toggle) calendars.append(row) @@ -136,7 +138,15 @@ class KhoraWindow(Adw.ApplicationWindow): details = f"{event.time_label} · {event.calendar}" if event.location: details += f" · {event.location}" - return Adw.ActionRow(title=event.summary, subtitle=details) + row = Adw.ActionRow(title=event.summary, subtitle=details) + row.add_prefix(KhoraWindow._color_dot(event.color)) + return row + + @staticmethod + def _color_dot(color: str | None) -> Gtk.Label: + dot = Gtk.Label(valign=Gtk.Align.CENTER) + dot.set_markup(f'') + return dot def _on_calendar_toggled(self, button: Gtk.Switch, _property, name: str) -> None: if button.get_active(): diff --git a/projects/khora/tests/test_colors.py b/projects/khora/tests/test_colors.py new file mode 100644 index 00000000..49f4f1ef --- /dev/null +++ b/projects/khora/tests/test_colors.py @@ -0,0 +1,18 @@ +from khora.colors import display_color + + +def test_preserves_html_colors() -> None: + assert display_color("#c01c28") == "#c01c28" + + +def test_converts_khal_named_colors() -> None: + assert display_color("light blue") == "#62a0ea" + + +def test_converts_xterm_color_cube() -> None: + assert display_color("196") == "#ff0000" + + +def test_unknown_colors_use_accent_fallback() -> None: + assert display_color("auto") == "#3584e4" + assert display_color("chartreuse, probably") == "#3584e4"