Skip to main content
ansicode
OCaml

ANSI escape codes in OCaml — \027, ANSITerminal, Fmt + Logs

OCaml string literals support `"\027"` (decimal — the canonical OCaml ESC form, works on every OCaml back to 1.0), `"\x1b"` (hex, OCaml 4.06+), and `"\o033"` (octal, OCaml 4.06+). **The OCaml twist**: `"\NNN"` is **decimal**, not octal like C — same trap Nim users hit. `"\033"` in C is byte 27 (octal); `"\033"` in OCaml is byte 33 (decimal — the `!` character). When porting from C the canonical fix is `"\027"` (decimal) or — on OCaml 4.06+ — `"\x1b"` (hex) or `"\o033"` (octal, when you want to keep the literal-looking-the-same). There is no `\e` named escape. `print_string "\027[1;31merror:\027[0m permission denied\n"` writes raw bytes through the runtime's `stdout` channel, parsed natively on macOS, Linux, BSDs, and Windows 10+ Conhost 1709+. Reach for **`ANSITerminal`** (opam package, the canonical OCaml ANSI library) — `ANSITerminal.print_string [red; Bold] "error: "` reads as a list of styles followed by the text, auto-emits the reset, and handles capability detection on Windows via native console-mode toggling. For **structured / formatted output** reach for **`fmt`** (the Dune-default formatter) — `Fmt.pr "@{<red>error@}: %s@." msg` parses semantic markup tags into SGR sequences and respects `Fmt.set_style_renderer Format.std_formatter `Ansi_tty`. Pair `fmt` with **`logs`** for structured logging with per-source-level coloured prefixes (`Logs.info (fun m -> m "started on %d" port)` produces a coloured `[INFO]` prefix when stdout is a TTY). For a full TUI runtime reach for **`notty`** — pqwy's high-quality declarative TUI library with image composition (`I.string A.(fg red) "error"` returns an `Notty.image` you compose with `<|>` / `<->` and render via `Notty_unix.Term`). Capability gating: `Unix.isatty Unix.stdout` (the `unix` library, ships with most OCaml installs) returns `false` when stdout is redirected. Pair with `Sys.getenv_opt "NO_COLOR"` for the universal opt-out. **utop REPL caveat** (page's primary SERP differentiator): `print_string "\027[31mfoo\027[0m"` inside utop DOES render red (utop pipes stdout to its underlying lambdaterm which parses ANSI). However, when the REPL displays the **value** of a top-level binding (`let s = "\027[31mfoo\027[0m";;`), utop renders the OCaml string literal back to you — escape bytes appear verbatim as `\027[...]`, not as a colour. The fix: print the string explicitly (`let () = print_string s`) rather than letting utop echo the value, or install `utop-full` and use `UTop.set_phrase_terminator` / `UTop.set_show_box` to customise REPL rendering.

Recommended libraries

  • ANSITerminal

    Canonical OCaml ANSI library — `opam install ansiterminal`. `ANSITerminal.print_string [red; Bold] "error: "` reads as a list of styles + text, auto-emits the reset, supports `[Foreground Red; Background Yellow; Bold; Underlined; Inverse]` plus 24-bit `[Foreground (RGB (255, 128, 0))]`. Module-level `ANSITerminal.set_autoreset true` ensures every print resets. On Windows: native console-mode toggling — no manual VT-enable needed. The one library nearly every OCaml CLI tool eventually pulls in.

  • fmt

    Dune-default formatter — what `Fmt.pr` / `Fmt.epr` are in modern OCaml, replacing direct `Format.printf` for human-facing output. Semantic markup tags (`@{<red>error@}`, `@{<bold>fatal@}`) parse into SGR sequences when `Fmt.set_style_renderer Format.std_formatter `Ansi_tty`. Switch to `\`None` for plain output (CI / piped). The right call when you want format-string-driven coloured output rather than imperative style-list calls.

  • logs

    Structured logging — pairs with `fmt` for coloured per-source-level prefixes. `Logs.info (fun m -> m "started on %d" port)` produces `[INFO] started on 8080` with `[INFO]` rendered in green when stdout is a TTY. Sources (`Logs.Src.create "db" ~doc:"database"`) let you filter per-module via `LOGS_REPORTER` / `LOGS_LEVEL` env vars. The canonical OCaml structured-logging choice; used throughout the modern OCaml ecosystem (cohttp, dune, mirage).

  • notty

    pqwy's high-quality declarative TUI library — the OCaml answer to vty / blessed / brick. `I.string A.(fg red) "error"` returns a `Notty.image` value that composes with `<|>` (horizontal) and `<->` (vertical). Render via `Notty_unix.Term.create ()` for a unix-style stdout-driven loop, or `Notty_lwt.Term.create ()` for an async-friendly Lwt-integrated event loop. Reach for it when you need a redrawing layout, not just a coloured log line.

Idiomatic patterns

Direct print_string — \NNN-is-decimal OCaml twist + \x1b on 4.06+
(* OCaml accepts three ESC forms in "..." literals.
   All compile to the same single byte (0x1b = 27).

     "\027"   — \NNN is DECIMAL (OCaml 1.0+, canonical)
     "\x1b"  — hex (OCaml 4.06+)
     "\o033" — octal (OCaml 4.06+)

   THE OCAML TWIST: \NNN is DECIMAL — not octal like C.
   In C, "\033" is byte 27 (octal 33 = decimal 27 = ESC).
   In OCaml, "\033" is byte 33 (decimal — the '!' character).
   Porting from C? "\033" silently mis-compiles. The fix is
   "\027" (decimal) or — on OCaml 4.06+ — "\x1b" (hex) or
   "\o033" (octal, when you want the literal looking the same).
   There is no \e named escape.

   On OCaml < 4.06 you have ONLY \NNN decimal — must use "\027". *)

let () =
  print_string "\027[1;31merror:\027[0m permission denied\n";
  print_string "\027[33mwarn:\027[0m deprecated flag\n";
  print_string "\x1b[32mok:\x1b[0m 142 tests passed\n";        (* 4.06+ *)
  print_string "\o033[36minfo:\o033[0m skipping cache\n";       (* 4.06+ *)

  (* Truecolor — 38;2;R;G;B *)
  print_string "\027[38;2;255;128;0morange truecolor\027[0m\n";

  (* 256-palette — 38;5;n *)
  print_string "\027[38;5;196mbright red\027[0m\n"

(* Reusable helper — no opam dependency: *)
let esc sgr text = "\027[" ^ sgr ^ "m" ^ text ^ "\027[0m"

let () =
  print_endline (esc "1;31" "FATAL" ^ " server crashed");
  print_endline (esc "32"   "ok:"    ^ " 142 tests passed");
  print_endline (esc "38;2;255;128;0" "truecolor orange")
ANSITerminal — style-list API, autoreset, RGB truecolor
(* opam install ansiterminal
   dune file:
     (executable (name main) (libraries ansiterminal)) *)

open ANSITerminal

let () =
  set_autoreset true;  (* every print auto-resets — recommended *)

  (* Style list + text — the canonical ANSITerminal call: *)
  print_string [red; Bold] "error: ";
  print_endline "permission denied";

  print_string [green] "ok: ";
  print_endline "142 tests passed";

  print_string [Background Yellow; black; Bold] " CAUTION ";
  print_endline " brakes wet";

  (* 24-bit truecolor — Foreground (RGB (r, g, b)): *)
  print_string [Foreground (RGB (255, 128, 0))] "hot orange";
  print_endline "";

  print_string
    [ Foreground (RGB (0, 110, 130))
    ; Background (RGB (240, 240, 240))
    ; Bold ]
    " deep teal on light grey ";
  print_endline "";

  (* Cursor + screen helpers — same module: *)
  erase Eol;            (* erase to end of line *)
  set_cursor 1 1;       (* move to (col, row) *)
  save_cursor ();
  restore_cursor ();

  (* Capability check — ANSITerminal handles Windows via
     native console mode toggling, but you can also check
     manually: *)
  if Unix.isatty Unix.stdout
  then prerr_endline "stdout is a TTY"
  else prerr_endline "stdout redirected — no colour"
Fmt + Logs — Dune-grade structured colour output
(* opam install fmt logs
   dune file:
     (executable
       (name main)
       (libraries fmt fmt.tty logs logs.fmt logs.cli)) *)

(* ─── Fmt — semantic markup tags compile to SGR ─── *)
let () =
  (* Enable ANSI rendering on stdout / stderr formatters.
     Switch to `None for CI / piped output. *)
  Fmt.set_style_renderer Fmt.stdout `Ansi_tty;
  Fmt.set_style_renderer Fmt.stderr `Ansi_tty;

  Fmt.pr "@{<red>error:@} %s@." "permission denied";
  Fmt.pr "@{<yellow>warn:@} %s@." "deprecated flag";
  Fmt.pr "@{<green>ok:@} 142 tests passed@.";
  Fmt.pr "@{<bold>@{<magenta>FATAL@}@}: server crashed@.";

  (* Compose markup with format directives: *)
  Fmt.pr "@{<cyan>request@} %s %s in @{<bold>%dms@}@."
    "GET" "/users/42" 142;

  (* Custom styled printer — reusable: *)
  let pp_status ppf = function
    | `Ok    -> Fmt.pf ppf "@{<green>OK@}"
    | `Fail  -> Fmt.pf ppf "@{<red>FAIL@}"
    | `Skip  -> Fmt.pf ppf "@{<yellow>SKIP@}"
  in
  Fmt.pr "[%a] migration applied@." pp_status `Ok

(* ─── Logs — structured logging with coloured levels ─── *)
let () =
  Logs.set_reporter (Logs_fmt.reporter ());
  Logs.set_level (Some Logs.Info);

  Logs.info  (fun m -> m "started on port %d" 8080);
  Logs.warn  (fun m -> m "slow query: %dms" 1240);
  Logs.err   (fun m -> m "auth failed for user %d" 42);

  (* Per-module log source — filter via LOGS_LEVEL=db:debug *)
  let src = Logs.Src.create "db" ~doc:"database layer" in
  let module Db = (val Logs.src_log src : Logs.LOG) in
  Db.info (fun m -> m "connected to %s" "postgres://...")
Capability gate + utop REPL value-echo caveat + Notty TUI
(* Capability gating in OCaml:
     Unix.isatty Unix.stdout    — stdlib (via unix lib)
     Sys.getenv_opt "NO_COLOR"  — universal opt-out
     Sys.getenv_opt "FORCE_COLOR" — universal opt-in

   utop REPL CAVEAT (page's primary SERP differentiator):
     print_string "\027[31mfoo\027[0m"  → renders RED in utop
       (utop pipes stdout to lambdaterm which parses ANSI)
     let s = "\027[31mfoo\027[0m";;        → utop echoes the
       OCaml string LITERAL: escape bytes show as \027[...] —
       NOT as a colour. utop renders the VALUE, not the meaning.

   Fix: print explicitly, don't let utop echo the value:
     let () = print_string s
   Or use UTop.set_phrase_terminator / UTop.set_show_box to
   customise REPL rendering (utop-full required). *)

let ansi_capable () =
  match Sys.getenv_opt "NO_COLOR", Sys.getenv_opt "FORCE_COLOR" with
  | Some _, _ -> false
  | _, Some _ -> true
  | _         -> Unix.isatty Unix.stdout

let styled text sgr =
  if ansi_capable ()
  then Printf.sprintf "\027[%sm%s\027[0m" sgr text
  else text

let () =
  print_endline (styled "OK"   "32");
  print_endline (styled "FAIL" "1;31")

(* ─── Notty — declarative TUI image composition ─── *)
(* opam install notty
   dune: (executable (name tui) (libraries notty notty.unix)) *)

let tui_demo () =
  let open Notty in
  let open Notty.Infix in

  let title  = I.string A.(fg yellow ++ st bold) "notty demo" in
  let ok     = I.string A.(fg green)  "✓ ok  " <|>
               I.string A.empty       "142 tests passed" in
  let fail   = I.string A.(fg red)    "✗ FAIL" <|>
               I.string A.empty       " 3 tests failed" in
  let info   = I.string A.(fg cyan)   "ℹ info" <|>
               I.string A.empty       " skipping cache" in

  let body = ok <-> fail <-> info in
  let img  = title <-> I.void 1 1 <-> body in

  let term = Notty_unix.Term.create () in
  Notty_unix.Term.image term img;
  ignore (Notty_unix.Term.event term);
  Notty_unix.Term.release term

let () = tui_demo ()

Related sequences

Other languages