Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ It should:
- construct DEC auto wrap enable/disable sequences
- construct scrolling-margin and reverse-index sequences
- construct low-level SGR sequences and fixed SGR attribute bytes
- construct foreground and background SGR sequences from semantic
`@color.Color` values for root `Tty` methods
- document the standard or terminal family each sequence comes from

It should not:
Expand Down Expand Up @@ -107,8 +109,8 @@ It should not:
- decide whether colors should be enabled
- mutate terminal palettes or query terminal color state

Root `Tty` methods map color values onto SGR byte sequences when writing to a
terminal.
Root `Tty` methods delegate color-to-SGR byte encoding to `internal/vt` when
writing to a terminal.

OSC dynamic color queries for the terminal default foreground, default
background, and text cursor color belong on root `Tty` because they write a
Expand Down
125 changes: 125 additions & 0 deletions docs/plans/2026-06-15-move-color-encoding-to-internal-vt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Move Color Encoding To Internal VT

## Goal

Move the VT byte encoding for semantic color values out of root `style.mbt` and
into `moonbit-community/tty/internal/vt`, while keeping root `Tty` color command
methods as the public user-facing API.

## Status

Done.

## Context And Decisions

- The root package currently owns private helpers that map `@color.Color` values
to SGR byte sequences.
- `internal/vt` already owns pure VT byte construction for cursor, screen,
erase, scroll, mode, and fixed SGR attribute commands.
- `color/` remains the semantic color value package and should not construct
terminal byte sequences or write to streams.
- `internal/vt` may depend on `moonbit-community/tty/color` because it is an
implementation package used by root `Tty` methods, not downstream API.
- Root `Tty` methods remain command-style methods that only write generated
bytes and do not track terminal style state.

## References Or Standards

- ECMA-48 SGR parameters:
- SGR 0 resets all SGR style attributes.
- SGR 30-37 and 90-97 set basic and bright foreground colors.
- SGR 40-47 and 100-107 set basic and bright background colors.
- SGR 38;5;n and 48;5;n set indexed foreground/background colors.
- SGR 38;2;r;g;b and 48;2;r;g;b set truecolor foreground/background colors.
- SGR 39 and 49 reset foreground/background colors to terminal defaults.

## Target Files

- `docs/plans/2026-06-15-move-color-encoding-to-internal-vt.md`
- `docs/architecture.md`
- `internal/vt/moon.pkg`
- `internal/vt/sgr.mbt`
- `internal/vt/sgr_test.mbt`
- `internal/vt/pkg.generated.mbti`
- `style.mbt`
- `tty_wbtest.mbt`

## Public API Changes

Root package public API should remain unchanged:

- `pkg.generated.mbti` should have no diff.
- `color/pkg.generated.mbti` should have no diff.

Internal VT package API is expected to gain:

- `reset_style : Bytes`
- `reset_foreground : Bytes`
- `reset_background : Bytes`
- `set_foreground(@color.Color) -> Bytes`
- `set_background(@color.Color) -> Bytes`

The internal VT package `.mbti` is not downstream public API, but it is still
reviewed as an internal package boundary.

## Invariants

- `internal/vt` remains pure byte construction and does not write to or own an
output stream.
- `internal/vt` does not import or depend on the root `tty` package.
- `color/` remains value-only and does not construct SGR bytes.
- Root `Tty` color methods keep their signatures and behavior.
- Numeric indexed and RGB channels remain clamped to `0..255` at the SGR
encoding boundary.

## Acceptance Criteria

- The root `style.mbt` no longer contains color-to-SGR encoding helpers.
- Root `Tty::set_foreground`, `Tty::set_background`, `Tty::reset_foreground`,
`Tty::reset_background`, and `Tty::reset_style` delegate to `@vt`.
- `internal/vt` tests cover default, basic, bright, indexed, truecolor, and
reset color sequences.
- Existing root pipe tests still prove `Tty` writes the same command bytes.

## Validation Commands

```sh
moon fmt
moon test internal/vt
moon test .
moon check
moon info
moon test
git diff --check
```

## Public API Audit

- Root `pkg.generated.mbti` has no diff.
- `color/pkg.generated.mbti` has no diff.
- `internal/vt/pkg.generated.mbti` gained only the planned `@color` import,
reset byte constants, and foreground/background color SGR constructors.
- No root private helper leaked into downstream public API.
- The internal VT `.mbti` diff was reviewed after `moon info`.

## Result Notes

- Moved semantic color SGR encoding helpers from root `style.mbt` into
`internal/vt/sgr.mbt`.
- Root `Tty` color command methods now delegate to `@vt.set_foreground`,
`@vt.set_background`, `@vt.reset_foreground`, `@vt.reset_background`, and
`@vt.reset_style`.
- Moved direct color encoding tests from root white-box tests to
`internal/vt/sgr_test.mbt`; root pipe tests still cover end-to-end command
bytes.
- `moon fmt` passed.
- `moon test internal/vt` passed: 20 tests.
- `moon test .` passed: 24 tests.
- `moon check` passed.
- `moon info` passed.
- `moon test` passed: 165 tests.
- `git diff --check` passed.

## Open Questions

- None after approval on 2026-06-15.
4 changes: 4 additions & 0 deletions internal/vt/moon.pkg
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import {
"moonbit-community/tty/color",
}

import {
"moonbitlang/core/bench",
"moonbitlang/core/debug",
Expand Down
14 changes: 14 additions & 0 deletions internal/vt/pkg.generated.mbti
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// Generated using `moon info`, DON'T EDIT IT
package "moonbit-community/tty/internal/vt"

import {
"moonbit-community/tty/color",
}

// Values
pub let begin_synchronized_update : Bytes

Expand Down Expand Up @@ -86,12 +90,18 @@ pub let request_cursor_position : Bytes

pub let request_primary_device_attributes : Bytes

pub let reset_background : Bytes

pub let reset_bold : Bytes

pub let reset_foreground : Bytes

pub let reset_italic : Bytes

pub let reset_reverse : Bytes

pub let reset_style : Bytes

pub let reset_top_bottom_margins : Bytes

pub let reset_underline : Bytes
Expand All @@ -100,6 +110,10 @@ pub let reverse : Bytes

pub let reverse_index : Bytes

pub fn set_background(@color.Color) -> Bytes

pub fn set_foreground(@color.Color) -> Bytes

pub fn set_top_bottom_margins(Int, Int) -> Bytes

pub fn sgr1(Int) -> Bytes
Expand Down
91 changes: 91 additions & 0 deletions internal/vt/sgr.mbt
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
///|
/// Reset all SGR style attributes (SGR 0), ECMA-48.
pub let reset_style : Bytes = b"\x1b[0m"

///|
/// Reset foreground color to terminal default (SGR 39), ECMA-48.
pub let reset_foreground : Bytes = b"\x1b[39m"

///|
/// Reset background color to terminal default (SGR 49), ECMA-48.
pub let reset_background : Bytes = b"\x1b[49m"

///|
/// Bold or increased intensity (SGR 1), ECMA-48.
pub let bold : Bytes = b"\x1b[1m"
Expand Down Expand Up @@ -80,3 +92,82 @@ pub fn sgr5(p1 : Int, p2 : Int, p3 : Int, p4 : Int, p5 : Int) -> Bytes {
buf[offset] = b'm'
buf.unsafe_reinterpret_as_bytes()
}

///|
fn clamp_byte_value(value : Int) -> Int {
if value < 0 {
0
} else if value > 255 {
255
} else {
value
}
}

///|
fn basic_foreground_code(color : @color.BasicColor) -> Int {
match color {
Black => 30
Red => 31
Green => 32
Yellow => 33
Blue => 34
Magenta => 35
Cyan => 36
White => 37
BrightBlack => 90
BrightRed => 91
BrightGreen => 92
BrightYellow => 93
BrightBlue => 94
BrightMagenta => 95
BrightCyan => 96
BrightWhite => 97
}
}

///|
fn basic_background_code(color : @color.BasicColor) -> Int {
match color {
Black => 40
Red => 41
Green => 42
Yellow => 43
Blue => 44
Magenta => 45
Cyan => 46
White => 47
BrightBlack => 100
BrightRed => 101
BrightGreen => 102
BrightYellow => 103
BrightBlue => 104
BrightMagenta => 105
BrightCyan => 106
BrightWhite => 107
}
}

///|
/// Construct the SGR sequence for a foreground color command.
pub fn set_foreground(color : @color.Color) -> Bytes {
match color {
Default => reset_foreground
Basic(color) => sgr1(basic_foreground_code(color))
Indexed(index) => sgr3(38, 5, clamp_byte_value(index))
Rgb(r, g, b) =>
sgr5(38, 2, clamp_byte_value(r), clamp_byte_value(g), clamp_byte_value(b))
}
}

///|
/// Construct the SGR sequence for a background color command.
pub fn set_background(color : @color.Color) -> Bytes {
match color {
Default => reset_background
Basic(color) => sgr1(basic_background_code(color))
Indexed(index) => sgr3(48, 5, clamp_byte_value(index))
Rgb(r, g, b) =>
sgr5(48, 2, clamp_byte_value(r), clamp_byte_value(g), clamp_byte_value(b))
}
}
35 changes: 35 additions & 0 deletions internal/vt/sgr_test.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,42 @@ test "sgr sequences" {
@debug.assert_eq(@vt.reset_underline, b"\x1b[24m")
@debug.assert_eq(@vt.reverse, b"\x1b[7m")
@debug.assert_eq(@vt.reset_reverse, b"\x1b[27m")
@debug.assert_eq(@vt.reset_style, b"\x1b[0m")
@debug.assert_eq(@vt.reset_foreground, b"\x1b[39m")
@debug.assert_eq(@vt.reset_background, b"\x1b[49m")
@debug.assert_eq(@vt.sgr1(0), b"\x1b[0m")
@debug.assert_eq(@vt.sgr3(38, 5, 42), b"\x1b[38;5;42m")
@debug.assert_eq(@vt.sgr5(38, 2, 1, 127, 255), b"\x1b[38;2;1;127;255m")
}

///|
test "sgr color sequences encode semantic colors" {
@debug.assert_eq(@vt.set_foreground(Default), b"\x1b[39m")
@debug.assert_eq(@vt.set_foreground(Basic(Black)), b"\x1b[30m")
@debug.assert_eq(@vt.set_foreground(Basic(Red)), b"\x1b[31m")
@debug.assert_eq(@vt.set_foreground(Basic(White)), b"\x1b[37m")
@debug.assert_eq(@vt.set_foreground(Basic(BrightBlack)), b"\x1b[90m")
@debug.assert_eq(@vt.set_foreground(Basic(BrightRed)), b"\x1b[91m")
@debug.assert_eq(@vt.set_foreground(Basic(BrightWhite)), b"\x1b[97m")
@debug.assert_eq(@vt.set_background(Default), b"\x1b[49m")
@debug.assert_eq(@vt.set_background(Basic(Black)), b"\x1b[40m")
@debug.assert_eq(@vt.set_background(Basic(Red)), b"\x1b[41m")
@debug.assert_eq(@vt.set_background(Basic(White)), b"\x1b[47m")
@debug.assert_eq(@vt.set_background(Basic(BrightBlack)), b"\x1b[100m")
@debug.assert_eq(@vt.set_background(Basic(BrightRed)), b"\x1b[101m")
@debug.assert_eq(@vt.set_background(Basic(BrightWhite)), b"\x1b[107m")
@debug.assert_eq(@vt.set_foreground(Indexed(-1)), b"\x1b[38;5;0m")
@debug.assert_eq(@vt.set_foreground(Indexed(42)), b"\x1b[38;5;42m")
@debug.assert_eq(@vt.set_foreground(Indexed(999)), b"\x1b[38;5;255m")
@debug.assert_eq(@vt.set_background(Indexed(-1)), b"\x1b[48;5;0m")
@debug.assert_eq(@vt.set_background(Indexed(42)), b"\x1b[48;5;42m")
@debug.assert_eq(@vt.set_background(Indexed(999)), b"\x1b[48;5;255m")
@debug.assert_eq(
@vt.set_foreground(Rgb(-1, 127, 999)),
b"\x1b[38;2;0;127;255m",
)
@debug.assert_eq(
@vt.set_background(Rgb(-1, 127, 999)),
b"\x1b[48;2;0;127;255m",
)
}
3 changes: 3 additions & 0 deletions moon.pkg
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import {
"moonbitlang/async",
"moonbitlang/async/pipe" @async/pipe,
"moonbitlang/async/stdio" @async/stdio,
"moonbitlang/core/debug",
"moonbit-community/tty/color",
"moonbit-community/tty/input" @public_input,
} for "test"

import {
Expand Down
Loading
Loading