Skip to main content

src/spruce@palette.erl

-module(spruce@palette).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/spruce/palette.gleam").
-export([hash/2]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

?MODULEDOC(
    " Deterministic hash-based colors for consistent terminal output.\n"
    "\n"
    " The `palette` module maps strings to colors using a simple hash function,\n"
    " ensuring that the same input always produces the same color. This is useful\n"
    " for coloring log categories, service names, user IDs, or any other repeated\n"
    " identifiers in a visually consistent way.\n"
    "\n"
    " The palette automatically adapts to the terminal's color support:\n"
    " - When color is disabled (`NoColor`), `hash` returns a plain style\n"
    " - When 256-color or truecolor is available, it uses a broader palette\n"
    " - When only basic ANSI is available, it falls back to a smaller set\n"
    "\n"
    " ```gleam\n"
    " import spruce\n"
    " import spruce/palette\n"
    " import spruce/style\n"
    "\n"
    " pub fn main() {\n"
    "   let sp = spruce.detect()\n"
    "   let colored = style.render(sp, palette.hash(sp, \"database\"), \"database\")\n"
    "   // \"database\" will always be rendered with the same color\n"
    " }\n"
    " ```\n"
).

-file("src/spruce/palette.gleam", 63).
-spec bounded_modulo(integer(), integer()) -> integer().
bounded_modulo(Value, Modulus) ->
    case gleam@int:modulo(Value, Modulus) of
        {ok, Value@1} ->
            Value@1;

        {error, nil} ->
            0
    end.

-file("src/spruce/palette.gleam", 54).
-spec hash_text(binary()) -> integer().
hash_text(Text) ->
    _pipe = Text,
    _pipe@1 = gleam@string:to_utf_codepoints(_pipe),
    gleam@list:fold(
        _pipe@1,
        5381,
        fun(Hash, Cp) ->
            Next = (Hash * 33) + gleam_stdlib:identity(Cp),
            bounded_modulo(Next, 2147483647)
        end
    ).

-file("src/spruce/palette.gleam", 70).
-spec palette_for(tty:color_level()) -> list(spruce@style:color()).
palette_for(Level) ->
    case tty:color_level_at_least(Level, ansi256) of
        true ->
            [red,
                green,
                yellow,
                blue,
                magenta,
                cyan,
                bright_red,
                bright_green,
                bright_yellow,
                bright_blue,
                bright_magenta,
                bright_cyan];

        false ->
            [cyan, green, yellow, magenta, blue, red]
    end.

-file("src/spruce/palette.gleam", 39).
?DOC(
    " Map a string to a deterministic color style.\n"
    "\n"
    " The same input string will always produce the same color. The color palette\n"
    " adapts to the context's color level: a broader set of colors is used when\n"
    " 256-color or truecolor support is detected, and a smaller set for basic ANSI.\n"
    "\n"
    " When the context has `NoColor`, this returns a plain style with no color.\n"
).
-spec hash(spruce:spruce(), binary()) -> spruce@style:style().
hash(Sp, Text) ->
    case spruce:supports_color(Sp) of
        false ->
            spruce@style:new();

        true ->
            Colors = palette_for(spruce:color_level(Sp)),
            Index = case erlang:length(Colors) of
                0 -> 0;
                Gleam@denominator -> hash_text(Text) rem Gleam@denominator
            end,
            Color = case gleam@list:drop(Colors, Index) of
                [C | _] ->
                    C;

                [] ->
                    cyan
            end,
            _pipe = spruce@style:new(),
            spruce@style:fg(_pipe, Color)
    end.