feat(projects/khora): color-code calendar events

Render calendar colors beside both event and calendar rows, including
conversion of khal named and xterm palette values.

Assisted-by: pi (gpt-5.6-sol)
This commit is contained in:
Gabriel Fontes
2026-08-15 12:28:07 -03:00
parent 5575c02521
commit c7392e5d6c
3 changed files with 103 additions and 1 deletions
+74
View File
@@ -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}"
+11 -1
View File
@@ -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'<span foreground="{display_color(color)}" size="18000">●</span>')
return dot
def _on_calendar_toggled(self, button: Gtk.Switch, _property, name: str) -> None:
if button.get_active():
+18
View File
@@ -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"