FREAKV3 docs freak 0.14.2 (Maverick)

Coloured output#

> There is no colour library. You write ANSI escape sequences into word values yourself — so this page is the full code reference.

FREAK has no std::term, no Color type and no formatting helpers. Colour works because two things are true:

  1. The lexer supports \xNN byte escapes and passes them through to the backend untouched.
  2. Every compiled FREAK program enables ANSI processing at startup.

That is enough. Colour constants are ordinary word bindings, concatenated with +. The compiler's own CLI is written this way, so it doubles as the reference implementation.

examples/colour.fk compilesruns
-- Coloured console output. There is no colour library: you build the
-- ANSI escape sequences yourself as `word` values and concatenate them.
--
-- `\x1b` is the ESC byte. The V3 lexer preserves `\xNN` escapes all the
-- way through to the backend, so they reach the terminal intact.
pilot C_RESET = "\x1b[0m"
pilot C_BOLD  = "\x1b[1m"
pilot C_DIM   = "\x1b[2m"

pilot C_RED   = "\x1b[31m"
pilot C_GREEN = "\x1b[32m"
pilot C_CYAN  = "\x1b[36m"

pilot C_BRED  = "\x1b[1;31m"

-- 24-bit truecolour: ESC [ 38;2;R;G;B m
pilot C_PINK  = "\x1b[38;2;255;100;200m"

task rgb(r: int, g: int, b: int) -> word {
    give back "\x1b[38;2;" + word_from_int(r) + ";" + word_from_int(g) + ";" + word_from_int(b) + "m"
}

task main() -> void {
    say C_BRED + "error" + C_RESET + ": engine failure"
    say C_GREEN + "ok" + C_RESET + ": all systems nominal"
    say C_DIM + "hint: check the coolant line" + C_RESET

    say C_BOLD + C_CYAN + "XM3" + C_RESET + " reflex link online"
    say C_PINK + "freak" + C_RESET + " 0.14.1"

    say rgb(255, 180, 0) + "amber warning" + C_RESET
}
Program output (ANSI rendered)
error: engine failure
ok: all systems nominal
hint: check the coolant line
XM3 reflex link online
freak 0.14.1
amber warning

Anatomy of a sequence#

Everything below is a CSI sequence — ESC [ followed by parameters and a single final letter that selects the operation:

\x1b [ params <final>
 │     │       └── the operation: `m` = set graphics, `H` = move cursor, …
 │     └────────── numeric parameters, `;` separated
 └──────────────── ESC, decimal 27, written \x1b in a FREAK word

\x1b is the only part that needs escaping; [, the digits, the semicolons and the final letter are ordinary characters.

Sequences ending in m are SGR (Select Graphic Rendition) — colour and text style. Everything else moves the cursor or edits the screen.

Multiple parameters combine in one sequence, which is cheaper and more reliable than emitting several:

FREAK
say "\x1b[1;4;31m" + "bold underlined red" + "\x1b[0m"
examples/ansi_codes.fk compilesruns
-- A tour of the SGR (Select Graphic Rendition) code space, to confirm
-- every family survives the V3 lexer, both backends and the runtime.
--
-- Form: ESC [ params m     where ESC is \x1b
pilot RESET = "\x1b[0m"

task sgr(code: word) -> word {
    give back "\x1b[" + code + "m"
}

task swatch(label: word, code: word) -> void {
    say sgr(code) + label + RESET
}

task main() -> void {
    -- Attributes
    swatch("bold", "1")
    swatch("dim", "2")
    swatch("italic", "3")
    swatch("underline", "4")
    swatch("blink", "5")
    swatch("reverse", "7")
    swatch("hidden", "8")
    swatch("strike", "9")

    -- Standard foreground 30-37
    swatch("black", "30")
    swatch("red", "31")
    swatch("green", "32")
    swatch("yellow", "33")
    swatch("blue", "34")
    swatch("magenta", "35")
    swatch("cyan", "36")
    swatch("white", "37")

    -- Bright foreground 90-97
    swatch("bright red", "91")
    swatch("bright green", "92")
    swatch("bright cyan", "96")

    -- Background 40-47 and bright 100-107
    swatch("on red", "41")
    swatch("on blue", "44")
    swatch("on bright yellow", "103")

    -- Combined parameters in one sequence
    swatch("bold + underline + red", "1;4;31")
    swatch("white on magenta", "37;45")

    -- 256-colour: 38;5;N foreground, 48;5;N background
    swatch("256 fg 208", "38;5;208")
    swatch("256 bg 22", "48;5;22")

    -- 24-bit truecolour: 38;2;R;G;B and 48;2;R;G;B
    swatch("rgb fg", "38;2;255;100;200")
    swatch("rgb bg", "48;2;20;20;90")

    -- Selective reset: 22 clears bold/dim, 24 clears underline,
    -- 39 resets foreground, 49 resets background.
    say sgr("1") + "bold " + sgr("22") + "normal again" + RESET
    say sgr("31") + "red " + sgr("39") + "default again" + RESET
}
Program output (ANSI rendered)
bold
dim
italic
underline
blink
reverse
hidden
strike
black
red
green
yellow
blue
magenta
cyan
white
bright red
bright green
bright cyan
on red
on blue
on bright yellow
bold + underline + red
white on magenta
256 fg 208
256 bg 22
rgb fg
rgb bg
bold normal again
red default again
Note

That output block is the program's real bytes, re-rendered as HTML. Two swatches look odd on purpose: blink appears unstyled, because this page does not animate it and many terminals ignore it too; and hidden appears blank, because that is what 8 does. Non-SGR sequences are dropped from the rendering rather than shown as garbage.

SGR: attributes#

Each attribute has a matching off-switch, so you can turn one thing off without resetting everything.

OnOffEffect
0Reset all attributes and colours
122Bold / increased intensity
222Dim / faint
323Italic
424Underline
525Slow blink
625Rapid blink — rarely implemented
727Reverse video (swap foreground and background)
828Conceal / hidden
929Crossed out / strikethrough
2124Double underline — sometimes "bold off" instead
5154Framed
5254Encircled
5355Overlined

Note that 22 clears both bold and dim, since they share an intensity channel.

SGR: foreground colour#

CodeColourCodeBright variant
30Black90Bright black (grey)
31Red91Bright red
32Green92Bright green
33Yellow93Bright yellow
34Blue94Bright blue
35Magenta95Bright magenta
36Cyan96Bright cyan
37White97Bright white
39Default foreground

SGR: background colour#

CodeColourCodeBright variant
40Black100Bright black (grey)
41Red101Bright red
42Green102Bright green
43Yellow103Bright yellow
44Blue104Bright blue
45Magenta105Bright magenta
46Cyan106Bright cyan
47White107Bright white
49Default background

SGR: extended colour#

38 sets the foreground and 48 the background, each with two sub-forms selected by the next parameter.

SequenceMeaning
\x1b[38;5;NmForeground from the 256-colour palette, N = 0–255
\x1b[48;5;NmBackground from the 256-colour palette
\x1b[38;2;R;G;BmForeground, 24-bit truecolour, each channel 0–255
\x1b[48;2;R;G;BmBackground, 24-bit truecolour
\x1b[58;5;NmUnderline colour — an extension, patchy support
\x1b[59mDefault underline colour

The 256-colour index is laid out in three blocks:

RangeContents
07The standard colours, same as 3037
815The bright colours, same as 9097
16231A 6×6×6 RGB cube
23225524 greys, dark to light

The cube index is 16 + 36*r + 6*g + b, where each of r, g, b is 0–5:

FREAK
task cube(r: int, g: int, b: int) -> word {
    give back "\x1b[38;5;" + word_from_int(16 + 36 * r + 6 * g + b) + "m"
}

Truecolour is the simplest option when you know the exact shade you want:

FREAK
task rgb(r: int, g: int, b: int) -> word {
    give back "\x1b[38;2;" + word_from_int(r) + ";" + word_from_int(g) + ";" + word_from_int(b) + "m"
}

Cursor and screen control#

These are CSI sequences with a final letter other than m. They pass through V3 exactly the same way — they are just bytes — but they act on the terminal rather than on the text, so they cannot be shown in a captured output block.

SequenceEffect
\x1b[nACursor up n rows
\x1b[nBCursor down n rows
\x1b[nCCursor forward n columns
\x1b[nDCursor back n columns
\x1b[nECursor to start of line, n rows down
\x1b[nFCursor to start of line, n rows up
\x1b[nGCursor to column n
\x1b[row;colHCursor to an absolute position, 1-based
\x1b[nJErase display — 0 to end, 1 to start, 2 all, 3 all plus scrollback
\x1b[nKErase line — 0 to end, 1 to start, 2 whole line
\x1b[nSScroll up n lines
\x1b[nTScroll down n lines
\x1b[sSave cursor position
\x1b[uRestore saved cursor position
\x1b[?25lHide the cursor
\x1b[?25hShow the cursor
\x1b[?1049hSwitch to the alternate screen buffer
\x1b[?1049lReturn to the main screen buffer

See Printing on the same line below for what to do with these.

Printing on the same line#

say always appends a newline. There is no print-without-newline builtin: the runtime's writer takes a newline flag, and say passes it as true (freak_say in freak_runtime.c). The only builtin that writes without one is ask, and it then blocks reading a line from stdin — fine for a prompt, useless for output.

So there are three techniques, all verified below.

examples/same_line.fk compilesruns
-- Printing on the same line.
--
-- `say` ALWAYS appends a newline -- there is no print-without-newline
-- builtin in V3. These are the three techniques that work instead.

task main() -> void {
    -- ---------------------------------------------------------------
    -- 1. Build the line, then say it once.
    --
    -- The usual answer. Nothing is written until the say, so the pieces
    -- land on one line.
    -- ---------------------------------------------------------------
    pilot mut row: word = "loading:"
    pilot mut i: int = 0
    repeat 5 times {
        row += " " + word_from_int(i)
        i += 1
    }
    say row

    -- For a lot of pieces, word_builder avoids re-copying the word on
    -- every concatenation.
    pilot b: int = word_builder::new()
    word_builder::append(b, "squadron:")
    pilot mut n: int = 1
    repeat 3 times {
        word_builder::append(b, " V-")
        word_builder::append_int(b, n)
        n += 1
    }
    say word_builder::finish(b)

    -- ---------------------------------------------------------------
    -- 2. Carriage return inside ONE say, to overwrite within the line.
    --
    -- "\r" returns the cursor to column 0. Everything after it paints
    -- over what came before, on the same physical line.
    -- ---------------------------------------------------------------
    say "calculating...\r\x1b[Kdone          "

    -- ---------------------------------------------------------------
    -- 3. Cursor-up, to overwrite a line a PREVIOUS say already ended.
    --
    -- "\x1b[1A" moves up one row, "\r" returns to column 0, "\x1b[K"
    -- erases to end of line. The say then redraws that row and its own
    -- newline puts the cursor back where it started.
    --
    -- In a terminal these four lines display as one line counting up.
    -- Captured to a file they stay separate, because the escapes are
    -- cursor movement rather than text.
    -- ---------------------------------------------------------------
    say "progress   0%"
    say "\x1b[1A\r\x1b[Kprogress  33%"
    say "\x1b[1A\r\x1b[Kprogress  66%"
    say "\x1b[1A\r\x1b[Kprogress 100%"

    say "finished"
}
Program output (ANSI rendered)
loading: 0 1 2 3 4
squadron: V-1 V-2 V-3
done
progress 100%
finished

1. Build the line, then say it once#

The usual answer, and the one to reach for first. Nothing reaches the terminal until the say, so the pieces land on one line.

FREAK
pilot mut row: word = "loading:"
pilot mut i: int = 0
repeat 5 times {
    row += " " + word_from_int(i)
    i += 1
}
say row

word += word works as of v0.14.2. For many pieces, prefer [word_builder](words.html#building-words-incrementally), which does not re-copy the accumulated word on every concatenation.

2. Carriage return, within one say#

\r moves the cursor to column 0. Anything after it in the same say overwrites what came before, on the same physical line:

FREAK
say "calculating...\r\x1b[Kdone"

\x1b[K erases to end of line, which matters when the new text is shorter than the old — without it, the tail of calculating... would still be showing.

3. Cursor-up, to redraw a line you already finished#

Once say has emitted its newline the cursor is on the next row, but you can go back:

FREAK
say "progress   0%"
say "\x1b[1A\r\x1b[Kprogress  33%"
say "\x1b[1A\r\x1b[Kprogress 100%"

\x1b[1A moves up one row, \r returns to column 0, \x1b[K clears it. The say redraws the row, and its own newline puts the cursor back where it started. In a terminal those three lines display as one line counting up.

This is the pattern for progress indicators, spinners and live counters.

Careful

Cursor movement only works on a terminal. Redirect the output to a file and the escapes are just bytes — you get every intermediate line, not the final one. With no TTY detection available (see below) your program cannot tell the difference, so keep \x1b[1A tricks out of anything whose output might be piped, or gate them behind a flag.

Note

The output block above is this site replaying the program's real bytes through a small terminal model — carriage returns overwrite, \x1b[1A redraws the previous line — so you see what a terminal shows rather than the raw escape soup.

ANSI is enabled for you#

On Windows, escapes only work when the console has ENABLE_VIRTUAL_TERMINAL_PROCESSING set. The runtime does this for every FREAK program, not just the CLI:

  • freak_enable_ansi() in freakc/runtime/freak_runtime.c sets the flag on both stdout and stderr.
  • The C backend emits a direct freak_enable_ansi(); call into main.
  • The LLVM backend calls it via freak_llvm_setup_args, which the emitted main invokes with argc/argv.

So nothing platform-specific is needed on your side. On POSIX the call is a no-op.

The escape itself#

\x1b is one of a small set of byte escapes the lexer understands:

EscapeByte
\nnewline
\rcarriage return
\ttab
\"double quote
\\backslash
\xNNany byte, two hex digits

The lexer's hex branch re-emits \xNN as that literal two-character sequence specifically so the C and LLVM emitters pass it straight into the generated string constant. Nothing in the pipeline interprets it.

Careful

\x00 is rejected at lex time — "embedded NUL escape is not supported" — because V3 words are NUL-terminated at runtime. A malformed hex escape (fewer than two hex digits) is also a lex error.

The CLI's palette#

src/cli/version.fk declares its whole palette as top-level pilot bindings. That file is plain FREAK compiled by V3, so it is a worked example you can copy.

GroupConstants
StylesC_RESET C_BOLD C_DIM C_ITALIC C_ULINE C_BLINK C_STRIKE
StandardC_RED C_GREEN C_YELLOW C_BLUE C_MAGENTA C_CYAN C_WHITE
BoldC_BRED C_BGREEN C_BYELLOW C_BBLUE C_BMAGENTA C_BCYAN C_BWHITE
Truecolour gradientC_G1C_G6 — pink → purple → blue → cyan
BackgroundC_BG_DARK = \x1b[48;2;20;20;30m

The gradient is what produces the banner from freak help:

FREAK
pilot C_G1 = "\x1b[38;2;255;100;200m"
pilot C_G2 = "\x1b[38;2;220;80;220m"
pilot C_G3 = "\x1b[38;2;180;70;240m"

Box drawing and symbols#

The same file declares Unicode decorations as raw UTF-8 byte escapes, because V3 has no char type and no \u escape — you spell out the encoding:

FREAK
pilot BOX_TL = "\xe2\x95\xad"      -- U+256D  round corner
pilot BOX_H  = "\xe2\x94\x80"      -- U+2500  horizontal
pilot BOX_V  = "\xe2\x94\x82"      -- U+2502  vertical
pilot SYM_CHECK = "\xe2\x9c\x93"   -- U+2713  check mark
Note

.length() counts characters, not bytes, so a box-drawing character is one character even though it is three bytes. Padding maths built on .length() stays correct.

Respecting the user's terminal#

The CLI honours the NO_COLOR convention in cli_configure_output(). Nothing does this for you automatically — the runtime enables ANSI, it does not decide whether you should use it. Copy the pattern:

FREAK
pilot mut C_RESET = "\x1b[0m"
pilot mut C_BRED  = "\x1b[1;31m"

task configure_output() -> void {
    if process::env("NO_COLOR") != "" {
        C_RESET = ""
        C_BRED = ""
    }
}

task main() -> void {
    configure_output()
    say C_BRED + "error" + C_RESET + ": no colour when NO_COLOR is set"
}

Setting the constants to the empty word is exactly what cli_disable_colors() does — every call site keeps working and concatenates nothing.

The CLI checks four variables:

VariableEffect in the CLI
NO_COLORAny non-empty value disables colour
FREAK_NO_COLORTruthy value disables colour
FREAK_ASCIIForce ASCII box drawing and symbols
FREAK_UNICODEForce Unicode even when it would otherwise degrade

On Windows without an explicit override it degrades decorations to ASCII unless it detects a modern terminal via WT_SESSION, TERM_PROGRAM, ANSICON or ConEmuANSI.

Note

There is no TTY detection anywhere. Neither the CLI nor your program can tell whether stdout is a pipe, so escapes are emitted even when output is redirected. That is why tooling around FREAK — including this site's verification harness — has to strip ANSI from captured compiler output.

The compiler's own diagnostics#

V3 hardcodes red for errors rather than using the palette, because the compiler files are concatenated ahead of the CLI's constants:

FREAK
say "\x1b[1;31merror\x1b[0m: " + msg

That line is in src/compiler/v3/helpers.fk, with variants in parser.fk (the

>100-error bail-out) and main.fk (the abort summary). The caret line under a diagnostic is coloured the same way.

Support and practical notes#

  • Always reset. An unclosed sequence leaks into the shell prompt after your program exits.
  • Safe floor: the 8 standard colours and 1/4/0. Universally supported.
  • Widely safe: bright colours, backgrounds, 256-colour, truecolour, 2, 3, 7, 9.
  • Patchy: 5/6 blink (often ignored), 8 conceal, 21 double underline, 5155 framed/encircled/overlined, 58 underline colour.
  • Combine parameters in one sequence rather than emitting several.
  • Colour goes through say, which writes to stdout. There is no stderr writer in V3, so diagnostics and normal output share one stream.

Every FREAK snippet on this site was compiled by freak 0.14.2 (Maverick), built from source with a verified self-host fixed point. 45/45 examples compile; regenerate with python tools/verify.py then python tools/build_docs.py.