diff --git a/catppuccin/colour.py b/catppuccin/colour.py index 820c90e..193eea7 100644 --- a/catppuccin/colour.py +++ b/catppuccin/colour.py @@ -15,29 +15,48 @@ class Colour: red: int green: int blue: int + alpha: int = 255 @property def rgb(self) -> Tuple[int, int, int]: """Get the colour as a 3-tuple of red, green, and blue.""" return self.red, self.green, self.blue + @property + def rgba(self) -> Tuple[int, int, int, int]: + """Get the colour as a 4-tuple of red, green, blue, and alpha.""" + return self.red, self.green, self.blue, self.alpha + @property def hex(self) -> str: """Get the colour as a lowercase hex string.""" + if self.alpha < 255: + return f"{self.red:02x}{self.green:02x}{self.blue:02x}{self.alpha:02x}" return f"{self.red:02x}{self.green:02x}{self.blue:02x}" def __eq__(self, other: Any) -> bool: if not isinstance(other, Colour): raise ValueError("Cannot check equality with non-colour types.") + return self.hex == other.hex @classmethod def from_hex(cls, hex_string: str) -> Colour: - """Create a color from hex string.""" - if len(hex_string) != 6: - raise ValueError("Hex string must be 6 characters long.") - match = re.match(r"([\da-fA-F]{2})" * 3, hex_string) + """Create a colour from hex string.""" + if len(hex_string) not in (6, 8): + raise ValueError("Hex string must be 6 or 8 characters long.") + + num_groups = 3 if len(hex_string) == 6 else 4 + match = re.match(r"([\da-fA-F]{2})" * num_groups, hex_string) if match is None: - raise ValueError("Hex string have an invalid format.") - hex_r, hex_g, hex_b = match.groups() - return Colour(*(int(col, 16) for col in (hex_r, hex_g, hex_b))) + raise ValueError("Hex string has an invalid format.") + + components = (int(col, 16) for col in match.groups()) + return Colour(*components) + + def opacity(self, opacity: float) -> Colour: + """Return a new colour with the given opacity.""" + if not 0 <= opacity <= 1: + raise ValueError("Opacity must be between 0 and 1.") + + return Colour(self.red, self.green, self.blue, int(opacity * 255)) diff --git a/tests/test_colour.py b/tests/test_colour.py index 4c4ab61..3c47806 100644 --- a/tests/test_colour.py +++ b/tests/test_colour.py @@ -7,14 +7,30 @@ def test_colour_to_rgb(): assert Colour(12, 123, 234).rgb == (12, 123, 234) -def test_colour_to_hex(): +def test_colour_to_rgba(): + assert Colour(12, 123, 234, 35).rgba == (12, 123, 234, 35) + + +def test_colour_to_rgba_default(): + assert Colour(12, 123, 234).rgba == (12, 123, 234, 255) + + +def test_rgb_colour_to_hex(): assert Colour(0x12, 0xEB, 0x77).hex == "12eb77" -def test_hex_to_color(): +def test_rgba_colour_to_hex(): + assert Colour(0x12, 0xEB, 0x77, 0x35).hex == "12eb7735" + + +def test_hex_to_colour(): assert Colour.from_hex("12eb77") == Colour(0x12, 0xEB, 0x77) +def test_hex_to_colour_with_alpha(): + assert Colour.from_hex("12eb7735") == Colour(0x12, 0xEB, 0x77, 0x35) + + def test_invalid_hex(): for invalid_value in ("1234567", "12345", "Z00000", "ABCDEG", "0F7CBJ"): with pytest.raises(ValueError): @@ -27,3 +43,13 @@ def test_equality(): with pytest.raises(ValueError): assert Colour(0x12, 0xEB, 0x77) == 42 + + +def test_opacity(): + colour = Colour(0x12, 0xEB, 0x77).opacity(0.5) + assert colour == Colour(0x12, 0xEB, 0x77, 0x7F) + + +def test_opacity_invalid(): + with pytest.raises(ValueError): + Colour(0x12, 0xEB, 0x77).opacity(1.5)