Skip to content

palettes

The module responsible for creating snazzy color palettes.

PaletteGeneratorStrategy = Callable[[Color], Tuple[Color, Color, Color, Color]] module-attribute

Returns 4 colors generated from the base color.

The first color will be used as the primary color. This should usually be the base color, but in some strategies (like analogous) it may not make sense.

The second and third colors will be the secondary and tertiary colors, respectively. The last color will be interpreted as the accent.

Palette dataclass

A harmonious color palette.

Running Palette.alias on a generated palette will create the following color aliases:

Main colors

These are the colors used by the majority of the application. Primary should make up around 50% percent of an average screen's colors, while secondary and tertiary should use the remaining 50% together (25% each).

Accents should be used sparingly to highlight specific details.

Items: primary, secondary, tertiary, accent

Semantic colors

These colors are all meant to convey some meaning. They shouldn't be used in situation where that meaning, e.g. success, isn't clearly related. When not given as an argument, they are generated by blending some default green, yellow and red with the primary color.

Items: success, warning, error

Neutral colors

These are colors meant to be used as a background to the main group. All of them are a blend of a default background color and one of the main colors: surface is generated from primary, surface2 comes from secondary and so on.

Items: surface, surface2, surface3, surface4

Source code in pytermgui/palettes.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
@dataclass(repr=False)
class Palette:
    """A harmonious color palette.

    Running `Palette.alias` on a generated palette will create the following color
    aliases:

    !!! cite "Main colors"

        These are the colors used by the majority of the application. Primary should
        make up around 50% percent of an average screen's colors, while secondary and
        tertiary should use the remaining 50% together (25% each).

        Accents should be used sparingly to highlight specific details.

        **Items:** primary, secondary, tertiary, accent

    !!! cite "Semantic colors"

        These colors are all meant to convey some meaning. They shouldn't be used in
        situation where that meaning, e.g. success, isn't clearly related. When not given
        as an argument, they are generated by blending some default green, yellow and red
        with the primary color.

        **Items:** success, warning, error

    !!! cite "Neutral colors"

        These are colors meant to be used as a background to the main group. All of them
        are a blend of a default background color and one of the main colors: `surface`
        is generated from `primary`, `surface2` comes from secondary and so on.

        **Items:** surface, surface2, surface3, surface4
    """

    data: dict[str, str]

    def __init__(  # pylint: disable=too-many-locals,too-many-arguments
        self,
        *,
        primary: str,
        secondary: str | None = None,
        tertiary: str | None = None,
        accent: str | None = None,
        success: str | None = None,
        warning: str | None = None,
        error: str | None = None,
        surface: str | None = None,
        surface2: str | None = None,
        surface3: str | None = None,
        strategy: PaletteGeneratorStrategy = triadic,
    ) -> None:
        """Generates a color palette from the given primary color.

        If any other color arguments are passed, they will be parsed as a color
        and used as-is. Otherwise, they will be derived from the primary.

        See the class documentation for info on all arguments.

        Args:
            strategy: A strategy that will be used to derive colors.
        """

        self.data = self._generate_map(
            primary=primary,
            secondary=secondary,
            tertiary=tertiary,
            accent=accent,
            success=success,
            warning=warning,
            error=error,
            surface=surface,
            surface2=surface2,
            surface3=surface3,
            strategy=strategy,
        )

    def _generate_map(  # pylint: disable=too-many-locals,too-many-arguments
        self,
        *,
        primary: str,
        secondary: str | None = None,
        tertiary: str | None = None,
        accent: str | None = None,
        success: str | None = None,
        warning: str | None = None,
        error: str | None = None,
        surface: str | None = None,
        surface2: str | None = None,
        surface3: str | None = None,
        strategy: PaletteGeneratorStrategy = triadic,
    ) -> dict[str, str]:
        """Generates a map of color names to values.

        See `__init__` for more information.
        """

        if isinstance(strategy, str):
            old_strat = strategy
            strategy = STRATEGIES.get(strategy)

            if strategy is None:
                raise KeyError(
                    f"Unknown strategy {old_strat!r}. Please choose from"
                    + f" {list(STRATEGIES.keys())}."
                )

        c_primary = Color.parse(primary, localize=False)

        # Four main colors
        c_primary, *generated = strategy(c_primary)
        c_secondary = _parse_optional(secondary, generated[0])
        c_tertiary = _parse_optional(tertiary, generated[1])
        c_accent = _parse_optional(accent, generated[2])

        # Four surface colors, one for each main color
        c_surface = _parse_optional(surface, SURFACE.blend(c_primary, SURFACE_ALPHA))
        c_surface2 = _parse_optional(
            surface2, SURFACE.blend(c_secondary, SURFACE_ALPHA)
        )
        c_surface3 = _parse_optional(surface3, SURFACE.blend(c_tertiary, SURFACE_ALPHA))
        c_surface4 = _parse_optional(surface3, SURFACE.blend(c_accent, SURFACE_ALPHA))

        # Three semantic colors, blended from primary
        c_success = _parse_optional(success, SUCCESS.blend(c_primary, SEMANTIC_ALPHA))
        c_warning = _parse_optional(warning, WARNING.blend(c_primary, SEMANTIC_ALPHA))
        c_error = _parse_optional(error, ERROR.blend(c_primary, SEMANTIC_ALPHA))

        base_palette: dict[str, Color] = {
            "primary": c_primary,
            "secondary": c_secondary,
            "tertiary": c_tertiary,
            "accent": c_accent,
            "surface": c_surface,
            "surface2": c_surface2,
            "surface3": c_surface3,
            "surface4": c_surface4,
            "success": c_success,
            "warning": c_warning,
            "error": c_error,
        }

        black = Color.parse("#000000")
        white = Color.parse("#FFFFFF")

        data = {}

        for name, color in base_palette.items():
            for shadenumber in range(-SHADE_COUNT, SHADE_COUNT + 1):
                if shadenumber > 0:
                    shadeindex = f"+{shadenumber}"
                    blend_color = white
                    blend_multiplier = 1

                elif shadenumber == 0:
                    shadeindex = ""

                else:
                    shadeindex = str(shadenumber)
                    blend_color = black
                    blend_multiplier = -1

                if shadenumber == 0:
                    blended = color

                else:
                    blended = color.blend(
                        blend_color, blend_multiplier * SHADE_INCREMENT * shadenumber
                    )

                data[f"{name}{shadeindex}"] = blended

                bg_variant = deepcopy(blended)
                bg_variant.background = True
                data[f"@{name}{shadeindex}"] = bg_variant

        return {
            key: ("@" if color.background else "") + color.hex
            for key, color in data.items()
        }

    def regenerate(self, **kwargs: Any) -> Palette:
        """Generates a new palette and replaces self.data with its data.

        Args:
            **kwargs: All key word args passed to the new Palette. See `__init__`.

        Returns:
            This palette, after regeneration.
        """

        other = Palette(**kwargs)

        self.data = other.data
        self.alias()

        return self

    def base_keys(self) -> list[str]:
        """Returns the non-background, non-shade alias keys."""

        return [
            key
            for key in self.data
            if not "+" in key and not "-" in key and not key.startswith("@")
        ]

    def alias(self, lang: MarkupLanguage = tim) -> None:
        """Sets up aliases for the given language.

        Note that no unsetters will be generated.

        Args:
            lang: The language to run `alias_multiple` on.
        """

        lang.clear_cache()
        lang.alias_multiple(**self.data, generate_unsetter=False)

    def __fancy_repr__(self) -> Generator[FancyYield, None, None]:
        """Shows off the palette in a compact form."""

        yield f"<{type(self).__name__}"

        for name, value in [
            ("primary", self.data["primary"]),
            ("secondary", self.data["secondary"]),
            ("tertiary", self.data["tertiary"]),
            ("accent", self.data["accent"]),
        ]:
            yield {
                "text": f" {name}: [@{value} #auto]{value}[/]",
                "highlight": False,
            }

        yield ">\n\n"

        length = max(len(key) for key in self.base_keys()) + 2
        for name in self.base_keys():
            line = ""

            for shadenumber in range(-SHADE_COUNT, SHADE_COUNT + 1):
                if shadenumber > 0:
                    shadeindex = f"+{shadenumber}"

                elif shadenumber == 0:
                    line += f"[@{self.data[name]} #auto] {name:^{length}} "
                    continue

                else:
                    shadeindex = str(shadenumber)

                line += f"[@{self.data[name + shadeindex]} #auto]    "

            yield {
                "text": tim.parse(line + "[/]\n"),
                "highlight": False,
            }

    def print(self) -> None:
        """Shows off the palette in an extended form."""

        length = max(len(key) for key in self.base_keys()) + 4
        keys = self.base_keys()

        for name in keys:
            names = []

            for shadenumber in range(-SHADE_COUNT, SHADE_COUNT + 1):
                if shadenumber > 0:
                    shadeindex = f"+{shadenumber}"

                elif shadenumber == 0:
                    shadeindex = ""

                else:
                    shadeindex = str(shadenumber)

                shaded_name = name + shadeindex
                names.append(shaded_name)

            tim.print("".join(f"[@{self.data[name]}]{' ' * length}" for name in names))
            tim.print(
                "".join(
                    f"[@{self.data[name]} #auto]"
                    + (
                        f"[bold]{name:^{length}}[/]"
                        if name in keys
                        else name[-2:].center(length)
                    )
                    for name in names
                )
            )
            tim.print(
                "".join(
                    f"[@{self.data[name]} #auto]{self.data[name]:^{length}}"
                    for name in names
                )
            )
            tim.print("".join(f"[@{self.data[name]}]{' ' * length}" for name in names))

__fancy_repr__()

Shows off the palette in a compact form.

Source code in pytermgui/palettes.py
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
def __fancy_repr__(self) -> Generator[FancyYield, None, None]:
    """Shows off the palette in a compact form."""

    yield f"<{type(self).__name__}"

    for name, value in [
        ("primary", self.data["primary"]),
        ("secondary", self.data["secondary"]),
        ("tertiary", self.data["tertiary"]),
        ("accent", self.data["accent"]),
    ]:
        yield {
            "text": f" {name}: [@{value} #auto]{value}[/]",
            "highlight": False,
        }

    yield ">\n\n"

    length = max(len(key) for key in self.base_keys()) + 2
    for name in self.base_keys():
        line = ""

        for shadenumber in range(-SHADE_COUNT, SHADE_COUNT + 1):
            if shadenumber > 0:
                shadeindex = f"+{shadenumber}"

            elif shadenumber == 0:
                line += f"[@{self.data[name]} #auto] {name:^{length}} "
                continue

            else:
                shadeindex = str(shadenumber)

            line += f"[@{self.data[name + shadeindex]} #auto]    "

        yield {
            "text": tim.parse(line + "[/]\n"),
            "highlight": False,
        }

__init__(*, primary, secondary=None, tertiary=None, accent=None, success=None, warning=None, error=None, surface=None, surface2=None, surface3=None, strategy=triadic)

Generates a color palette from the given primary color.

If any other color arguments are passed, they will be parsed as a color and used as-is. Otherwise, they will be derived from the primary.

See the class documentation for info on all arguments.

Parameters:

Name Type Description Default
strategy PaletteGeneratorStrategy

A strategy that will be used to derive colors.

triadic
Source code in pytermgui/palettes.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
def __init__(  # pylint: disable=too-many-locals,too-many-arguments
    self,
    *,
    primary: str,
    secondary: str | None = None,
    tertiary: str | None = None,
    accent: str | None = None,
    success: str | None = None,
    warning: str | None = None,
    error: str | None = None,
    surface: str | None = None,
    surface2: str | None = None,
    surface3: str | None = None,
    strategy: PaletteGeneratorStrategy = triadic,
) -> None:
    """Generates a color palette from the given primary color.

    If any other color arguments are passed, they will be parsed as a color
    and used as-is. Otherwise, they will be derived from the primary.

    See the class documentation for info on all arguments.

    Args:
        strategy: A strategy that will be used to derive colors.
    """

    self.data = self._generate_map(
        primary=primary,
        secondary=secondary,
        tertiary=tertiary,
        accent=accent,
        success=success,
        warning=warning,
        error=error,
        surface=surface,
        surface2=surface2,
        surface3=surface3,
        strategy=strategy,
    )

alias(lang=tim)

Sets up aliases for the given language.

Note that no unsetters will be generated.

Parameters:

Name Type Description Default
lang MarkupLanguage

The language to run alias_multiple on.

tim
Source code in pytermgui/palettes.py
300
301
302
303
304
305
306
307
308
309
310
def alias(self, lang: MarkupLanguage = tim) -> None:
    """Sets up aliases for the given language.

    Note that no unsetters will be generated.

    Args:
        lang: The language to run `alias_multiple` on.
    """

    lang.clear_cache()
    lang.alias_multiple(**self.data, generate_unsetter=False)

base_keys()

Returns the non-background, non-shade alias keys.

Source code in pytermgui/palettes.py
291
292
293
294
295
296
297
298
def base_keys(self) -> list[str]:
    """Returns the non-background, non-shade alias keys."""

    return [
        key
        for key in self.data
        if not "+" in key and not "-" in key and not key.startswith("@")
    ]

print()

Shows off the palette in an extended form.

Source code in pytermgui/palettes.py
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
def print(self) -> None:
    """Shows off the palette in an extended form."""

    length = max(len(key) for key in self.base_keys()) + 4
    keys = self.base_keys()

    for name in keys:
        names = []

        for shadenumber in range(-SHADE_COUNT, SHADE_COUNT + 1):
            if shadenumber > 0:
                shadeindex = f"+{shadenumber}"

            elif shadenumber == 0:
                shadeindex = ""

            else:
                shadeindex = str(shadenumber)

            shaded_name = name + shadeindex
            names.append(shaded_name)

        tim.print("".join(f"[@{self.data[name]}]{' ' * length}" for name in names))
        tim.print(
            "".join(
                f"[@{self.data[name]} #auto]"
                + (
                    f"[bold]{name:^{length}}[/]"
                    if name in keys
                    else name[-2:].center(length)
                )
                for name in names
            )
        )
        tim.print(
            "".join(
                f"[@{self.data[name]} #auto]{self.data[name]:^{length}}"
                for name in names
            )
        )
        tim.print("".join(f"[@{self.data[name]}]{' ' * length}" for name in names))

regenerate(**kwargs)

Generates a new palette and replaces self.data with its data.

Parameters:

Name Type Description Default
**kwargs Any

All key word args passed to the new Palette. See __init__.

{}

Returns:

Type Description
Palette

This palette, after regeneration.

Source code in pytermgui/palettes.py
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
def regenerate(self, **kwargs: Any) -> Palette:
    """Generates a new palette and replaces self.data with its data.

    Args:
        **kwargs: All key word args passed to the new Palette. See `__init__`.

    Returns:
        This palette, after regeneration.
    """

    other = Palette(**kwargs)

    self.data = other.data
    self.alias()

    return self

analogous(base)

Colors that sit next to eachother on the colorwheel.

Note that the order of primary and secondary colors are swapped by this function. This is done so the colors, when laid out next to eachother, complete a gradient.

Parameters:

Name Type Description Default
base Color

The color used for derivations.

required

Analogous strategy

Source code in pytermgui/palettes.py
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
def analogous(base: Color) -> tuple[Color, Color, Color, Color]:
    """Colors that sit next to eachother on the colorwheel.

    Note that the order of primary and secondary colors are swapped
    by this function. This is done so the colors, when laid out next
    to eachother, complete a gradient.

    Args:
        base: The color used for derivations.

    ![Analogous strategy](../../assets/analogous.svg)
    """

    before, _, after = base.analogous

    return before, base, after, base.complement

triadic(base)

Three complementary colors.

Each color is offset 120 degrees from the previous one on the colorwheel. If plotted on the colorwheel, they make up a regular triangle.

Parameters:

Name Type Description Default
base Color

The color used for derivations.

required

Triadic strategy

Source code in pytermgui/palettes.py
54
55
56
57
58
59
60
61
62
63
64
65
66
def triadic(base: Color) -> tuple[Color, Color, Color, Color]:
    """Three complementary colors.

    Each color is offset 120 degrees from the previous one on the colorwheel. If
    plotted on the colorwheel, they make up a regular triangle.

    Args:
        base: The color used for derivations.

    ![Triadic strategy](../../assets/triadic.svg)
    """

    return (*base.triadic, base.complement)