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