├── .gitignore ├── .travis.yml ├── include └── color.hrl ├── src ├── color.app.src └── color.erl ├── rebar.config ├── .github └── workflows │ └── ci.yml ├── README.md └── test └── color_test.erl /.gitignore: -------------------------------------------------------------------------------- 1 | # Rebar3 build artifacts 2 | _build/ 3 | rebar3.crashdump 4 | rebar.lock 5 | 6 | # Erlang compiled files 7 | *.beam 8 | *.o 9 | *.so 10 | *.dll 11 | 12 | # Dialyzer files 13 | .plt 14 | *.plt 15 | .dialyzer_plt 16 | 17 | # Cover 18 | .eunit 19 | eunit.coverdata 20 | 21 | # Logs 22 | *.log 23 | log/ 24 | logs/ 25 | 26 | # Temporary files 27 | .tmp 28 | tmp/ 29 | *.tmp 30 | *.temp 31 | 32 | # Editor files 33 | .idea/ 34 | *.iml 35 | .vscode/ 36 | *.swp 37 | *.swo 38 | *~ 39 | 40 | # OS generated files 41 | .DS_Store 42 | .DS_Store? 43 | ._* 44 | .Spotlight-V100 45 | .Trashes 46 | ehthumbs.db 47 | Thumbs.db 48 | 49 | # Hex 50 | doc/ 51 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | 3 | # Modern Erlang/OTP versions 4 | otp_release: 5 | - 27.0 6 | - 26.2 7 | - 25.3 8 | - 24.3 9 | 10 | # Use modern Ubuntu distribution 11 | dist: jammy 12 | 13 | # Cache dependencies for faster builds 14 | cache: 15 | directories: 16 | - $HOME/.cache/rebar3 17 | 18 | # Install latest rebar3 19 | before_script: 20 | - wget https://s3.amazonaws.com/rebar3/rebar3 && chmod +x rebar3 21 | - export PATH="$PWD:$PATH" 22 | 23 | # Run comprehensive tests 24 | script: 25 | - rebar3 compile 26 | - rebar3 eunit 27 | - rebar3 dialyzer 28 | - rebar3 cover 29 | 30 | # Generate coverage reports 31 | after_success: 32 | - rebar3 as test coveralls send 33 | 34 | # Deploy to hex.pm on tags (requires HEX_API_KEY environment variable) 35 | deploy: 36 | provider: script 37 | script: rebar3 hex publish --yes 38 | on: 39 | tags: true 40 | otp_release: 27.0 41 | 42 | # Notifications 43 | notifications: 44 | email: false 45 | -------------------------------------------------------------------------------- /include/color.hrl: -------------------------------------------------------------------------------- 1 | -define(ESC, <<"\e[">>). 2 | -define(RST, <<"0">>). 3 | -define(BOLD, <<"1">>). 4 | -define(SEP, <<";">>). 5 | -define(END, <<"m">>). 6 | 7 | %% Colors 8 | -define(BLACK, <<"30">>). 9 | -define(RED, <<"31">>). 10 | -define(GREEN, <<"32">>). 11 | -define(YELLOW, <<"33">>). 12 | -define(BLUE, <<"34">>). 13 | -define(MAGENTA, <<"35">>). 14 | -define(CYAN, <<"36">>). 15 | -define(WHITE, <<"37">>). 16 | -define(DEFAULT, <<"39">>). 17 | 18 | %% Background colors 19 | -define(BLACK_BG, <<"40">>). 20 | -define(RED_BG, <<"41">>). 21 | -define(GREEN_BG, <<"42">>). 22 | -define(YELLOW_BG, <<"43">>). 23 | -define(BLUE_BG, <<"44">>). 24 | -define(MAGENTA_BG, <<"45">>). 25 | -define(CYAN_BG, <<"46">>). 26 | -define(WHITE_BG, <<"47">>). 27 | -define(DEFAULT_BG, <<"49">>). 28 | 29 | %% RGB 30 | -define(RGB_FG, [<<"38">>, ?SEP, <<"5">>]). 31 | -define(RGB_BG, [<<"48">>, ?SEP, <<"5">>]). 32 | 33 | %% True 24-bit colors 34 | -define(TRUE_COLOR_FG, [<<"38">>, ?SEP, <<"2">>]). 35 | -define(TRUE_COLOR_BG, [<<"48">>, ?SEP, <<"2">>]). 36 | -------------------------------------------------------------------------------- /src/color.app.src: -------------------------------------------------------------------------------- 1 | {application, color, 2 | [ 3 | {description, "ANSI colors for your Erlang"}, 4 | {vsn, "2.0.0"}, 5 | {modules, [ 6 | color 7 | ]}, 8 | {registered, []}, 9 | {applications, [ 10 | kernel, 11 | stdlib 12 | ]}, 13 | {env, []}, 14 | 15 | %% Hex.pm package metadata 16 | {pkg_name, erlang_color}, 17 | {maintainers, ["Julian Duque", "Duncan McGreggor"]}, 18 | {licenses, ["MIT"]}, 19 | {links, [ 20 | {"GitHub", "https://github.com/julianduque/erlang-color"}, 21 | {"Hex", "https://hex.pm/packages/erlang_color"} 22 | ]}, 23 | {files, [ 24 | "include", 25 | "src", 26 | "README.md", 27 | "rebar.config" 28 | ]}, 29 | {build_tools, ["rebar3"]}, 30 | {requirements, []}, 31 | {extra, #{ 32 | <<"maintainers">> => [<<"Julian Duque">>, <<"Duncan McGreggor">>], 33 | <<"licenses">> => [<<"MIT">>], 34 | <<"links">> => #{ 35 | <<"GitHub">> => <<"https://github.com/julianduque/erlang-color">>, 36 | <<"Hex">> => <<"https://hex.pm/packages/erlang_color">> 37 | } 38 | }} 39 | ]}. 40 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [debug_info, {i, "include"}]}. 2 | 3 | %% Dependencies 4 | {deps, []}. 5 | 6 | %% Profiles 7 | {profiles, [ 8 | {test, [ 9 | {erl_opts, [debug_info, {i, "include"}]}, 10 | {deps, [ 11 | {coveralls, "2.2.0"} 12 | ]}, 13 | {plugins, [ 14 | {coveralls, "2.2.0"} 15 | ]} 16 | ]}, 17 | {prod, [ 18 | {erl_opts, [no_debug_info, {i, "include"}]} 19 | ]} 20 | ]}. 21 | 22 | %% Cover 23 | {cover_enabled, true}. 24 | {cover_opts, [verbose]}. 25 | 26 | %% Coveralls 27 | {coveralls_coverdata, "_build/test/cover/eunit.coverdata"}. 28 | {coveralls_service_name, "travis-ci"}. 29 | 30 | %% EUnit 31 | {eunit_opts, [verbose, {report, {eunit_surefire, [{dir, "."}]}}]}. 32 | 33 | %% Dialyzer 34 | {dialyzer, [ 35 | {warnings, [ 36 | no_return, 37 | no_unused, 38 | no_improper_lists, 39 | no_fun_app, 40 | no_match, 41 | no_opaque, 42 | no_fail_call, 43 | error_handling, 44 | unmatched_returns 45 | ]} 46 | ]}. 47 | 48 | %% Hex.pm configuration 49 | {hex, [ 50 | {doc, #{provider => ex_doc}} 51 | ]}. 52 | 53 | %% Project plugins 54 | {plugins, [ 55 | rebar3_hex, 56 | rebar3_ex_doc 57 | ]}. 58 | 59 | %% Minimum OTP version 60 | {minimum_otp_vsn, "24"}. 61 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | 9 | jobs: 10 | test: 11 | name: Test OTP ${{matrix.otp}} 12 | runs-on: ubuntu-22.04 13 | 14 | strategy: 15 | matrix: 16 | otp: [24.3, 25.3, 26.2, 27.0] 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Setup Erlang/OTP 22 | uses: erlef/setup-beam@v1 23 | with: 24 | otp-version: ${{matrix.otp}} 25 | 26 | - name: Install rebar3 27 | run: | 28 | curl -fsSL https://github.com/erlang/rebar3/releases/latest/download/rebar3 -o rebar3 29 | chmod +x rebar3 30 | sudo mv rebar3 /usr/local/bin/ 31 | rebar3 version 32 | 33 | - name: Restore dependencies cache 34 | uses: actions/cache@v3 35 | with: 36 | path: | 37 | _build 38 | ~/.cache/rebar3 39 | key: ${{ runner.os }}-rebar3-${{ hashFiles('**/rebar.config') }} 40 | restore-keys: ${{ runner.os }}-rebar3- 41 | 42 | - name: Compile 43 | run: rebar3 compile 44 | 45 | - name: Run tests 46 | run: rebar3 eunit 47 | 48 | - name: Run dialyzer 49 | run: rebar3 dialyzer 50 | 51 | - name: Generate coverage 52 | run: rebar3 cover 53 | 54 | - name: Upload coverage to Codecov 55 | uses: codecov/codecov-action@v3 56 | with: 57 | file: ./_build/test/cover/eunit.coverdata 58 | flags: unittests 59 | name: codecov-umbrella 60 | 61 | publish: 62 | name: Publish to Hex.pm 63 | runs-on: ubuntu-22.04 64 | needs: test 65 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') 66 | 67 | steps: 68 | - uses: actions/checkout@v4 69 | 70 | - name: Setup Erlang/OTP 71 | uses: erlef/setup-beam@v1 72 | with: 73 | otp-version: 27.0 74 | 75 | - name: Install rebar3 76 | run: | 77 | curl -fsSL https://github.com/erlang/rebar3/releases/latest/download/rebar3 -o rebar3 78 | chmod +x rebar3 79 | sudo mv rebar3 /usr/local/bin/ 80 | rebar3 version 81 | 82 | - name: Publish to Hex.pm 83 | env: 84 | HEX_API_KEY: ${{ secrets.HEX_API_KEY }} 85 | run: rebar3 hex publish --yes -------------------------------------------------------------------------------- /src/color.erl: -------------------------------------------------------------------------------- 1 | -module(color). 2 | -export([black/1, blackb/1, red/1, redb/1, green/1, greenb/1, blue/1, blueb/1]). 3 | -export([yellow/1, yellowb/1, magenta/1, magentab/1, cyan/1, cyanb/1, white/1, whiteb/1]). 4 | -export([on_black/1, on_red/1, on_green/1, on_blue/1, on_yellow/1, on_magenta/1, on_cyan/1, on_white/1]). 5 | -export([rgb/2, on_rgb/2]). 6 | -export([true/2, on_true/2]). 7 | 8 | -include("color.hrl"). 9 | 10 | black(Text) -> [color(?BLACK), Text, reset()]. 11 | blackb(Text) -> [colorb(?BLACK), Text, reset()]. 12 | red(Text) -> [color(?RED), Text, reset()]. 13 | redb(Text) -> [colorb(?RED), Text, reset()]. 14 | green(Text) -> [color(?GREEN), Text, reset()]. 15 | greenb(Text) -> [colorb(?GREEN), Text, reset()]. 16 | yellow(Text) -> [color(?YELLOW), Text, reset()]. 17 | yellowb(Text) -> [colorb(?YELLOW), Text, reset()]. 18 | blue(Text) -> [color(?BLUE), Text, reset()]. 19 | blueb(Text) -> [colorb(?BLUE), Text, reset()]. 20 | magenta(Text) -> [color(?MAGENTA), Text, reset()]. 21 | magentab(Text) -> [colorb(?MAGENTA), Text, reset()]. 22 | cyan(Text) -> [color(?CYAN), Text, reset()]. 23 | cyanb(Text) -> [colorb(?CYAN), Text, reset()]. 24 | white(Text) -> [color(?WHITE), Text, reset()]. 25 | whiteb(Text) -> [colorb(?WHITE), Text, reset()]. 26 | on_black(Text) -> [color(?BLACK_BG), Text, reset_bg()]. 27 | on_red(Text) -> [color(?RED_BG), Text, reset_bg()]. 28 | on_green(Text) -> [color(?GREEN_BG), Text, reset_bg()]. 29 | on_blue(Text) -> [color(?BLUE_BG), Text, reset_bg()]. 30 | on_yellow(Text) -> [color(?YELLOW_BG), Text, reset_bg()]. 31 | on_magenta(Text) -> [color(?MAGENTA_BG), Text, reset_bg()]. 32 | on_cyan(Text) -> [color(?CYAN_BG), Text, reset_bg()]. 33 | on_white(Text) -> [color(?WHITE_BG), Text, reset_bg()]. 34 | 35 | rgb(RGB, Text) -> 36 | [?ESC, ?RGB_FG, ?SEP, rgb_color(RGB), ?END, Text, reset()]. 37 | 38 | on_rgb(RGB, Text) -> 39 | [?ESC, ?RGB_BG, ?SEP, rgb_color(RGB), ?END, Text, reset_bg()]. 40 | 41 | true(RGB, Text) -> 42 | [?ESC, ?TRUE_COLOR_FG, ?SEP, true_color(RGB), ?END, Text, reset()]. 43 | 44 | on_true(RGB, Text) -> 45 | [?ESC, ?TRUE_COLOR_BG, ?SEP, true_color(RGB), ?END, Text, reset()]. 46 | 47 | %% Internal 48 | color(Color) -> 49 | <>. 50 | 51 | colorb(Color) -> 52 | <>. 53 | 54 | rgb_color([R, G, B]) when R >= 0, R =< 5, G >= 0, G =< 5, B >= 0, B =< 5 -> 55 | integer_to_list(16 + (R * 36) + (G * 6) + B). 56 | 57 | true_color([R1, R2, G1, G2, B1, B2]) -> 58 | R = erlang:list_to_integer([R1, R2], 16), 59 | G = erlang:list_to_integer([G1, G2], 16), 60 | B = erlang:list_to_integer([B1, B2], 16), 61 | true_color([R, G, B]); 62 | 63 | true_color([R, G, B]) when R >= 0, R =< 255, G >= 0, G =< 255, B >= 0, B =< 255 -> 64 | [integer_to_list(R), ?SEP, integer_to_list(G), ?SEP, integer_to_list(B)]. 65 | 66 | reset() -> 67 | <>. 68 | 69 | reset_bg() -> 70 | <>. 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # erlang-color 2 | 3 | [![CI](https://github.com/julianduque/erlang-color/workflows/CI/badge.svg)](https://github.com/julianduque/erlang-color/actions) 4 | [![Build Status](https://travis-ci.org/julianduque/erlang-color.png)](https://travis-ci.org/julianduque/erlang-color) 5 | [![Hex.pm Version](https://img.shields.io/hexpm/v/erlang_color.svg)](https://hex.pm/packages/erlang_color) 6 | [![Coverage Status](https://codecov.io/gh/julianduque/erlang-color/branch/master/graph/badge.svg)](https://codecov.io/gh/julianduque/erlang-color) 7 | 8 | ANSI colors for your Erlang 9 | 10 | **Supports Erlang/OTP 24+ and rebar3**. 11 | 12 | ## Usage: 13 | 14 | ### ANSI standard colors 15 | 16 | ``` erlang 17 | 1> io:format("Hello, this is the ~s color~n", [ color:red("red") ]). 18 | ``` 19 | 20 | ![Screenshot 1](https://cldup.com/20QQ-o0RTT.png) 21 | 22 | ``` erlang 23 | 1> io:format("Hello, this is on ~s~n", [ color:on_blue("blue blackground") ]). 24 | ``` 25 | 26 | ![Screenshot 2](https://cldup.com/g_BUaqeNTw.png) 27 | 28 | Make sure you use the `~s` string type, `~p` will escape the ANSI code. 29 | 30 | #### xterm 256 colors 31 | 32 | ``` erlang 33 | 1> io:format("Hello, this color is ~s~n", [ color:rgb([0,1,0], "green") ]). 34 | ``` 35 | 36 | ![Screenshot 3](https://cldup.com/wKesRAqdFj.png) 37 | 38 | #### true 24-bit colors 39 | 40 | Note: as of this writing, 24-bit colors (ISO-8613-3) are not widely adopted in terminal emulators. Konsole and iTerm2 nighly builds are known to support them. 41 | 42 | ``` erlang 43 | 1> io:format("The solarized template is ~s ~s ~s ~s ~s ~s ~s ~s ~s ~s ~s ~s ~s ~s ~s ~s ~n", [ 44 | color:true("002B36", "base03"), color:true("073642", "base02"), color:true("586E75", "base01"), 45 | color:true("657B83", "base00"), color:true("839496", "base0"), color:true("93A1A1", "base1"), 46 | color:true("EEE8D5", "base2"), color:true("FDF6E3", "base3"), color:true("B58900", "yellow"), 47 | color:true("CB4B16", "orange"), color:true("DC322F", "red"), color:true("D33682", "magenta"), 48 | color:true("6C71C4", "violet"), color:true("268BD2", "blue"), color:true("2AA198", "cyan"), 49 | color:true("859900", "green")]). 50 | ``` 51 | 52 | ![Screenshot 4](https://cldup.com/M-JcBjy9t1.png) 53 | 54 | ## The MIT License (MIT) 55 | 56 | Copyright (c) 2016-2025 Julián Duque, Evgeni Kolev, Duncan McGreggor 57 | 58 | Permission is hereby granted, free of charge, to any person obtaining a copy 59 | of this software and associated documentation nfiles (the "Software"), to deal 60 | in the Software without restriction, including without limitation the rights 61 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 62 | copies of the Software, and to permit persons to whom the Software is 63 | furnished to do so, subject to the following conditions: 64 | 65 | The above copyright notice and this permission notice shall be included in 66 | all copies or substantial portions of the Software. 67 | 68 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 69 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 70 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 71 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 72 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 73 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 74 | THE SOFTWARE. 75 | -------------------------------------------------------------------------------- /test/color_test.erl: -------------------------------------------------------------------------------- 1 | -module(color_test). 2 | -include_lib("eunit/include/eunit.hrl"). 3 | 4 | %% Colors 5 | color_test_() -> 6 | [?_assertEqual(To, to_s(color:Fun(From))) || {From, Fun, To} <- [ 7 | % Normal colors 8 | {"black", black, "\e[30mblack\e[0m"}, 9 | {"red", red, "\e[31mred\e[0m"}, 10 | {"green", green, "\e[32mgreen\e[0m"}, 11 | {"yellow", yellow, "\e[33myellow\e[0m"}, 12 | {"blue", blue, "\e[34mblue\e[0m"}, 13 | {"magenta", magenta, "\e[35mmagenta\e[0m"}, 14 | {"cyan", cyan, "\e[36mcyan\e[0m"}, 15 | {"white", white, "\e[37mwhite\e[0m"}, 16 | % Bright colors 17 | {"black", blackb, "\e[30;1mblack\e[0m"}, 18 | {"red", redb, "\e[31;1mred\e[0m"}, 19 | {"green", greenb, "\e[32;1mgreen\e[0m"}, 20 | {"yellow", yellowb, "\e[33;1myellow\e[0m"}, 21 | {"blue", blueb, "\e[34;1mblue\e[0m"}, 22 | {"magenta", magentab, "\e[35;1mmagenta\e[0m"}, 23 | {"cyan", cyanb, "\e[36;1mcyan\e[0m"}, 24 | {"white", whiteb, "\e[37;1mwhite\e[0m"}, 25 | % Background colors 26 | {"on_black", on_black, "\e[40mon_black\e[49m"}, 27 | {"on_red", on_red, "\e[41mon_red\e[49m"}, 28 | {"on_green", on_green, "\e[42mon_green\e[49m"}, 29 | {"on_yellow", on_yellow, "\e[43mon_yellow\e[49m"}, 30 | {"on_blue", on_blue, "\e[44mon_blue\e[49m"}, 31 | {"on_magenta", on_magenta, "\e[45mon_magenta\e[49m"}, 32 | {"on_cyan", on_cyan, "\e[46mon_cyan\e[49m"}, 33 | {"on_white", on_white, "\e[47mon_white\e[49m"} 34 | ]]. 35 | 36 | rgb_test_() -> 37 | [?_assertEqual(Result, to_s(color:rgb(Color, "rgb"))) 38 | || {Color, Result} <- [ 39 | {[0, 0, 0], "\e[38;5;16mrgb\e[0m"}, 40 | {[1, 1, 1], "\e[38;5;59mrgb\e[0m"}, 41 | {[2, 2, 2], "\e[38;5;102mrgb\e[0m"}, 42 | {[3, 3, 3], "\e[38;5;145mrgb\e[0m"}, 43 | {[4, 4, 4], "\e[38;5;188mrgb\e[0m"}, 44 | {[5, 5, 5], "\e[38;5;231mrgb\e[0m"} 45 | ] 46 | ]. 47 | 48 | on_rgb_test_() -> 49 | [?_assertEqual(Result, to_s(color:on_rgb(Color, "rgb"))) 50 | || {Color, Result} <- [ 51 | {[0, 0, 0], "\e[48;5;16mrgb\e[49m"}, 52 | {[1, 1, 1], "\e[48;5;59mrgb\e[49m"}, 53 | {[2, 2, 2], "\e[48;5;102mrgb\e[49m"}, 54 | {[3, 3, 3], "\e[48;5;145mrgb\e[49m"}, 55 | {[4, 4, 4], "\e[48;5;188mrgb\e[49m"}, 56 | {[5, 5, 5], "\e[48;5;231mrgb\e[49m"} 57 | ] 58 | ]. 59 | 60 | true_color_test_() -> 61 | [?_assertEqual(Result, to_s(color:true(Color, "true"))) 62 | || {Color, Result} <- [ 63 | {[0, 0, 0], "\e[38;2;0;0;0mtrue\e[0m"}, 64 | {[255, 255, 255], "\e[38;2;255;255;255mtrue\e[0m"}, 65 | {"000000", "\e[38;2;0;0;0mtrue\e[0m"}, 66 | {"444444", "\e[38;2;68;68;68mtrue\e[0m"}, 67 | {"888888", "\e[38;2;136;136;136mtrue\e[0m"}, 68 | {"BBBBBB", "\e[38;2;187;187;187mtrue\e[0m"}, 69 | {"FFFFFF", "\e[38;2;255;255;255mtrue\e[0m"} 70 | ] 71 | ]. 72 | 73 | on_true_color_test_() -> 74 | [?_assertEqual(Result, to_s(color:on_true(Color, "true"))) 75 | || {Color, Result} <- [ 76 | {[0, 0, 0], "\e[48;2;0;0;0mtrue\e[0m"}, 77 | {[255, 255, 255], "\e[48;2;255;255;255mtrue\e[0m"}, 78 | {"000000", "\e[48;2;0;0;0mtrue\e[0m"}, 79 | {"444444", "\e[48;2;68;68;68mtrue\e[0m"}, 80 | {"888888", "\e[48;2;136;136;136mtrue\e[0m"}, 81 | {"BBBBBB", "\e[48;2;187;187;187mtrue\e[0m"}, 82 | {"FFFFFF", "\e[48;2;255;255;255mtrue\e[0m"} 83 | ] 84 | ]. 85 | 86 | iodata_test() -> 87 | ?assertEqual( 88 | "\e[30mtest\e[0m", 89 | to_s(color:black([<<"t">>, "e", 115, [[<<"t">>]]])) 90 | ). 91 | 92 | %% Internal 93 | 94 | to_s(IOData) -> 95 | binary_to_list(iolist_to_binary(IOData)). 96 | --------------------------------------------------------------------------------