From 4b5400540190d4a30d5bbb389331ea963aa6620f Mon Sep 17 00:00:00 2001 From: Nanaloveyuki Date: Sun, 10 May 2026 15:32:48 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20ANSI=20color=20support=20fall?= =?UTF-8?q?back=20modes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 + bitlogger/BitLogger_test.mbt | 24 +++- bitlogger/BitLogger_wbtest.mbt | 16 +++ bitlogger/README.mbt.md | 1 + bitlogger/config.mbt | 14 +++ bitlogger/formatter.mbt | 201 ++++++++++++++++++++++++++++----- docs/README-en.md | 2 + docs/changes/0.4.0.md | 3 + 8 files changed, 234 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index c9f358f..cffd03f 100644 --- a/README.md +++ b/README.md @@ -305,6 +305,7 @@ match logger.file_runtime_state() { - `QueueConfig`, `TextFormatterConfig`, `SinkConfig` 可分别通过 `queue_config_to_json(...)` / `stringify_queue_config(...)`, `text_formatter_config_to_json(...)` / `stringify_text_formatter_config(...)`, `sink_config_to_json(...)` / `stringify_sink_config(...)` 单独导出 JSON - 支持字段: `min_level`, `target`, `timestamp`, `sink.kind`, `sink.path`, `sink.append`, `sink.auto_flush`, `sink.rotation`, `sink.text_formatter`, `queue` - `TextFormatter` / `TextFormatterConfig` 提供 `color_mode = Never | Auto | Always`, 可控制 ANSI 文本着色 +- `TextFormatter` / `TextFormatterConfig` 提供 `color_support = basic | truecolor`, 可控制 hex / RGB 样式是否降级到基础 ANSI 色 - `TextFormatter` / `TextFormatterConfig` 提供 `style_markup = disabled | builtin | full`, 可决定是否解析 style markup 以及是否启用 custom style tag - `target_style_markup` 与 `fields_style_markup` 可独立控制 `target` 和 `fields` 是否解析 style markup - `message` 支持轻量 inline style tag: `...`, `...`, `<#ff0000>...`, `...` @@ -338,6 +339,7 @@ match logger.file_runtime_state() { - `file_sink_state_to_json(...)`, `stringify_file_sink_state(...)`, `runtime_file_state_to_json(...)`, `stringify_runtime_file_state(...)` 可直接把 file / queued-file 快照导出为 JSON, 便于排障或上报 - `sink.text_formatter.template` 支持固定 token: `{timestamp}`, `{timestamp_ms}`, `{level}`, `{target}`, `{message}`, `{fields}` - `sink.text_formatter.color_mode` 支持 `never`, `auto`, `always` +- `sink.text_formatter.color_support` 支持 `basic`, `truecolor` - `sink.text_formatter.style_markup` 支持 `disabled`, `builtin`, `full` - `sink.text_formatter.target_style_markup` 与 `sink.text_formatter.fields_style_markup` 支持 `disabled`, `builtin`, `full` - `sink.text_formatter.style_tags.` 支持 `fg`, `bg`, `bold`, `dim`, `italic`, `underline` diff --git a/bitlogger/BitLogger_test.mbt b/bitlogger/BitLogger_test.mbt index 632c3c0..5f7ee67 100644 --- a/bitlogger/BitLogger_test.mbt +++ b/bitlogger/BitLogger_test.mbt @@ -74,9 +74,10 @@ test "logger config parser reads formatter and queue options" { test "logger config parser reads formatter style tags" { let config = parse_logger_config_text( - "{\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"color_mode\":\"always\",\"style_markup\":\"builtin\",\"target_style_markup\":\"builtin\",\"fields_style_markup\":\"disabled\",\"style_tags\":{\"accent\":{\"fg\":\"#4cc9f0\",\"bold\":true},\"panel\":{\"bg\":\"#202020\",\"underline\":true}}}}}", + "{\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"color_mode\":\"always\",\"color_support\":\"basic\",\"style_markup\":\"builtin\",\"target_style_markup\":\"builtin\",\"fields_style_markup\":\"disabled\",\"style_tags\":{\"accent\":{\"fg\":\"#4cc9f0\",\"bold\":true},\"panel\":{\"bg\":\"#202020\",\"underline\":true}}}}}", ) inspect(style_markup_mode_label(config.sink.text_formatter.style_markup), content="builtin") + inspect(color_support_label(config.sink.text_formatter.color_support), content="basic") inspect(style_markup_mode_label(config.sink.text_formatter.target_style_markup), content="builtin") inspect(style_markup_mode_label(config.sink.text_formatter.fields_style_markup), content="disabled") inspect(config.sink.text_formatter.style_tags.length(), content="2") @@ -151,6 +152,7 @@ test "logger config stringify roundtrips formatter style tags" { kind=SinkKind::TextConsole, text_formatter=TextFormatterConfig::new( color_mode=ColorMode::Always, + color_support=ColorSupport::Basic, style_markup=StyleMarkupMode::Builtin, target_style_markup=StyleMarkupMode::Builtin, fields_style_markup=StyleMarkupMode::Disabled, @@ -164,6 +166,7 @@ test "logger config stringify roundtrips formatter style tags" { ) let config = parse_logger_config_text(text) inspect(color_mode_label(config.sink.text_formatter.color_mode), content="always") + inspect(color_support_label(config.sink.text_formatter.color_support), content="basic") inspect(style_markup_mode_label(config.sink.text_formatter.style_markup), content="builtin") inspect(style_markup_mode_label(config.sink.text_formatter.target_style_markup), content="builtin") inspect(style_markup_mode_label(config.sink.text_formatter.fields_style_markup), content="disabled") @@ -213,6 +216,7 @@ test "config subtype json helpers stringify stable shapes" { field_separator=",", template="[{level}] {message} :: {fields}", color_mode=ColorMode::Always, + color_support=ColorSupport::Basic, style_markup=StyleMarkupMode::Builtin, target_style_markup=StyleMarkupMode::Builtin, fields_style_markup=StyleMarkupMode::Disabled, @@ -221,7 +225,7 @@ test "config subtype json helpers stringify stable shapes" { }, ), ), - content="{\"show_timestamp\":false,\"show_level\":true,\"show_target\":false,\"show_fields\":true,\"separator\":\" | \",\"field_separator\":\",\",\"template\":\"[{level}] {message} :: {fields}\",\"color_mode\":\"always\",\"style_markup\":\"builtin\",\"target_style_markup\":\"builtin\",\"fields_style_markup\":\"disabled\",\"style_tags\":{\"accent\":{\"bold\":true,\"dim\":false,\"italic\":false,\"underline\":false,\"fg\":\"#4cc9f0\"}}}", + content="{\"show_timestamp\":false,\"show_level\":true,\"show_target\":false,\"show_fields\":true,\"separator\":\" | \",\"field_separator\":\",\",\"template\":\"[{level}] {message} :: {fields}\",\"color_mode\":\"always\",\"color_support\":\"basic\",\"style_markup\":\"builtin\",\"target_style_markup\":\"builtin\",\"fields_style_markup\":\"disabled\",\"style_tags\":{\"accent\":{\"bold\":true,\"dim\":false,\"italic\":false,\"underline\":false,\"fg\":\"#4cc9f0\"}}}", ) inspect( stringify_sink_config( @@ -234,10 +238,24 @@ test "config subtype json helpers stringify stable shapes" { text_formatter=TextFormatterConfig::new(show_timestamp=false, color_mode=ColorMode::Auto), ), ), - content="{\"kind\":\"file\",\"path\":\"demo.log\",\"append\":false,\"auto_flush\":false,\"text_formatter\":{\"show_timestamp\":false,\"show_level\":true,\"show_target\":true,\"show_fields\":true,\"separator\":\" \",\"field_separator\":\" \",\"template\":\"\",\"color_mode\":\"auto\",\"style_markup\":\"full\",\"target_style_markup\":\"disabled\",\"fields_style_markup\":\"disabled\"},\"rotation\":{\"max_bytes\":128,\"max_backups\":2}}", + content="{\"kind\":\"file\",\"path\":\"demo.log\",\"append\":false,\"auto_flush\":false,\"text_formatter\":{\"show_timestamp\":false,\"show_level\":true,\"show_target\":true,\"show_fields\":true,\"separator\":\" \",\"field_separator\":\" \",\"template\":\"\",\"color_mode\":\"auto\",\"color_support\":\"truecolor\",\"style_markup\":\"full\",\"target_style_markup\":\"disabled\",\"fields_style_markup\":\"disabled\"},\"rotation\":{\"max_bytes\":128,\"max_backups\":2}}", ) } +test "config basic color support downgrades hex colors" { + let formatter = TextFormatterConfig::new( + show_level=false, + show_target=false, + color_mode=ColorMode::Always, + color_support=ColorSupport::Basic, + ) + let rendered = format_text( + Record::new(Level::Info, "<#ff0000>hot bg"), + formatter=formatter.to_formatter(), + ) + inspect(rendered, content="\u{001b}[31mhot\u{001b}[0m \u{001b}[100mbg\u{001b}[0m") +} + test "config formatter style tags render in built logger" { let formatter = TextFormatterConfig::new( show_timestamp=false, diff --git a/bitlogger/BitLogger_wbtest.mbt b/bitlogger/BitLogger_wbtest.mbt index 7dee2ab..f4a69ad 100644 --- a/bitlogger/BitLogger_wbtest.mbt +++ b/bitlogger/BitLogger_wbtest.mbt @@ -139,6 +139,22 @@ test "text formatter supports hex inline colors" { ) } +test "text formatter can downgrade hex colors to basic ansi" { + let rec = record(Level::Info, "<#ff0000>hot bg") + inspect( + format_text( + rec, + formatter=text_formatter( + show_level=false, + show_target=false, + color_mode=ColorMode::Always, + color_support=ColorSupport::Basic, + ), + ), + content="\u{001b}[31mhot\u{001b}[0m \u{001b}[100mbg\u{001b}[0m", + ) +} + test "text formatter keeps unknown inline tags as plain text" { let rec = record(Level::Info, "boom") inspect( diff --git a/bitlogger/README.mbt.md b/bitlogger/README.mbt.md index 5196366..996d72d 100644 --- a/bitlogger/README.mbt.md +++ b/bitlogger/README.mbt.md @@ -251,6 +251,7 @@ test { - supported tokens / 支持的 token: `{timestamp}`, `{timestamp_ms}`, `{level}`, `{target}`, `{message}`, `{fields}` - `color_mode` / `color_mode`: `never`, `auto`, `always` +- `color_support` / `color_support`: `basic`, `truecolor` - `style_markup` / `style_markup`: `disabled`, `builtin`, `full` - `target_style_markup` / `target_style_markup`, `fields_style_markup` / `fields_style_markup`: `disabled`, `builtin`, `full` - inline style tags / inline 样式标签: `...`, `...`, `<#ff0000>...`, `...` diff --git a/bitlogger/config.mbt b/bitlogger/config.mbt index cdbf1ed..de4c435 100644 --- a/bitlogger/config.mbt +++ b/bitlogger/config.mbt @@ -18,6 +18,7 @@ pub struct TextFormatterConfig { field_separator : String template : String color_mode : ColorMode + color_support : ColorSupport style_markup : StyleMarkupMode target_style_markup : StyleMarkupMode fields_style_markup : StyleMarkupMode @@ -33,6 +34,7 @@ pub fn TextFormatterConfig::new( field_separator~ : String = " ", template~ : String = "", color_mode~ : ColorMode = ColorMode::Never, + color_support~ : ColorSupport = ColorSupport::TrueColor, style_markup~ : StyleMarkupMode = StyleMarkupMode::Full, target_style_markup~ : StyleMarkupMode = StyleMarkupMode::Disabled, fields_style_markup~ : StyleMarkupMode = StyleMarkupMode::Disabled, @@ -47,6 +49,7 @@ pub fn TextFormatterConfig::new( field_separator, template, color_mode, + color_support, style_markup, target_style_markup, fields_style_markup, @@ -72,6 +75,7 @@ pub fn TextFormatterConfig::to_formatter(self : TextFormatterConfig) -> TextForm field_separator=self.field_separator, template=self.template, color_mode=self.color_mode, + color_support=self.color_support, style_markup=self.style_markup, target_style_markup=self.target_style_markup, fields_style_markup=self.fields_style_markup, @@ -869,6 +873,14 @@ fn parse_color_mode(name : String) -> ColorMode raise ConfigError { } } +fn parse_color_support(name : String) -> ColorSupport raise ConfigError { + match name.to_upper() { + "BASIC" => ColorSupport::Basic + "TRUECOLOR" => ColorSupport::TrueColor + _ => raise ConfigError::InvalidConfig("Unsupported color support: " + name) + } +} + fn parse_style_markup_mode(name : String) -> StyleMarkupMode raise ConfigError { match name.to_upper() { "DISABLED" => StyleMarkupMode::Disabled @@ -898,6 +910,7 @@ fn parse_text_formatter_config(value : @json_parser.JsonValue) -> TextFormatterC field_separator=get_string(obj, "field_separator", default=" "), template=get_string(obj, "template", default=""), color_mode=parse_color_mode(get_string(obj, "color_mode", default="never")), + color_support=parse_color_support(get_string(obj, "color_support", default="truecolor")), style_markup=parse_style_markup_mode(get_string(obj, "style_markup", default="full")), target_style_markup=parse_style_markup_mode(get_string(obj, "target_style_markup", default="disabled")), fields_style_markup=parse_style_markup_mode(get_string(obj, "fields_style_markup", default="disabled")), @@ -1024,6 +1037,7 @@ pub fn text_formatter_config_to_json(config : TextFormatterConfig) -> @json_pars "field_separator": @json_parser.JsonValue::String(config.field_separator), "template": @json_parser.JsonValue::String(config.template), "color_mode": @json_parser.JsonValue::String(color_mode_label(config.color_mode)), + "color_support": @json_parser.JsonValue::String(color_support_label(config.color_support)), "style_markup": @json_parser.JsonValue::String(style_markup_mode_label(config.style_markup)), "target_style_markup": @json_parser.JsonValue::String(style_markup_mode_label(config.target_style_markup)), "fields_style_markup": @json_parser.JsonValue::String(style_markup_mode_label(config.fields_style_markup)), diff --git a/bitlogger/formatter.mbt b/bitlogger/formatter.mbt index b9a3421..48c7442 100644 --- a/bitlogger/formatter.mbt +++ b/bitlogger/formatter.mbt @@ -6,6 +6,11 @@ pub(all) enum ColorMode { Always } +pub(all) enum ColorSupport { + Basic + TrueColor +} + pub(all) enum StyleMarkupMode { Disabled Builtin @@ -149,6 +154,8 @@ pub fn reset_global_style_tag_registry() -> Unit { priv struct InlineStyle { fg_code : String? bg_code : String? + fg_basic_name : String? + bg_basic_name : String? bold : Bool dim : Bool italic : Bool @@ -182,6 +189,7 @@ pub struct TextFormatter { field_separator : String template : String color_mode : ColorMode + color_support : ColorSupport style_markup : StyleMarkupMode target_style_markup : StyleMarkupMode fields_style_markup : StyleMarkupMode @@ -197,6 +205,7 @@ pub fn text_formatter( field_separator~ : String = " ", template~ : String = "", color_mode~ : ColorMode = ColorMode::Never, + color_support~ : ColorSupport = ColorSupport::TrueColor, style_markup~ : StyleMarkupMode = StyleMarkupMode::Full, target_style_markup~ : StyleMarkupMode = StyleMarkupMode::Disabled, fields_style_markup~ : StyleMarkupMode = StyleMarkupMode::Disabled, @@ -211,6 +220,7 @@ pub fn text_formatter( field_separator, template, color_mode, + color_support, style_markup, target_style_markup, fields_style_markup, @@ -218,6 +228,17 @@ pub fn text_formatter( } } +pub fn color_support_label(support : ColorSupport) -> String { + match support { + ColorSupport::Basic => "basic" + ColorSupport::TrueColor => "truecolor" + } +} + +pub fn TextFormatter::with_color_support(self : TextFormatter, color_support : ColorSupport) -> TextFormatter { + { ..self, color_support } +} + pub fn style_markup_mode_label(mode : StyleMarkupMode) -> String { match mode { StyleMarkupMode::Disabled => "disabled" @@ -314,7 +335,16 @@ fn ansi_wrap_with_style(text : String, style : InlineStyle, enabled : Bool) -> S } fn default_inline_style() -> InlineStyle { - { fg_code: None, bg_code: None, bold: false, dim: false, italic: false, underline: false } + { + fg_code: None, + bg_code: None, + fg_basic_name: None, + bg_basic_name: None, + bold: false, + dim: false, + italic: false, + underline: false, + } } fn named_color_code(tag : String) -> String? { @@ -361,6 +391,88 @@ fn named_bg_color_code(tag : String) -> String? { } } +fn basic_name_from_hex(value : String) -> String { + let r = hex_pair_to_int(value.unsafe_get(1), value.unsafe_get(2)) + let g = hex_pair_to_int(value.unsafe_get(3), value.unsafe_get(4)) + let b = hex_pair_to_int(value.unsafe_get(5), value.unsafe_get(6)) + let max_value = if r >= g && r >= b { r } else if g >= b { g } else { b } + if max_value < 48 { + "bright_black" + } else { + let min_value = if r <= g && r <= b { r } else if g <= b { g } else { b } + let spread = max_value - min_value + if spread < 24 { + if max_value > 170 { + "white" + } else if max_value > 96 { + "bright_black" + } else { + "black" + } + } else if r >= g && r >= b { + if g > 96 && b < 96 { + "yellow" + } else if b > 96 && g < 96 { + "magenta" + } else { + "red" + } + } else if g >= r && g >= b { + if r > 96 && b < 96 { + "yellow" + } else if b > 96 && r < 96 { + "cyan" + } else { + "green" + } + } else { + if r > 96 && g < 96 { + "magenta" + } else if g > 96 && r < 96 { + "cyan" + } else { + "blue" + } + } + } +} + +fn resolve_inline_color_code( + formatter : TextFormatter, + basic_name : String?, + truecolor_code : String, +) -> String { + match formatter.color_support { + ColorSupport::TrueColor => truecolor_code + ColorSupport::Basic => match basic_name { + Some(name) => named_code_from_basic_name(name).unwrap_or(truecolor_code) + None => truecolor_code + } + } +} + +fn named_code_from_basic_name(name : String) -> String? { + named_color_code(name) +} + +fn named_bg_code_from_basic_name(name : String) -> String? { + named_bg_color_code(name) +} + +fn resolve_inline_bg_code( + formatter : TextFormatter, + basic_name : String?, + truecolor_code : String, +) -> String { + match formatter.color_support { + ColorSupport::TrueColor => truecolor_code + ColorSupport::Basic => match basic_name { + Some(name) => named_bg_code_from_basic_name(name).unwrap_or(truecolor_code) + None => truecolor_code + } + } +} + fn is_hex_color(value : String) -> Bool { if value.length() != 7 { return false @@ -416,34 +528,61 @@ fn rgb_bg_code_from_hex(value : String) -> String { "48;2;\{r};\{g};\{b}" } -fn inline_style_from_text_style(base : InlineStyle, style : TextStyle) -> InlineStyle? { - let fg_code = match style.fg { - None => Some(base.fg_code) - Some(value) => match named_color_code(normalize_style_tag_name(value)) { - Some(code) => Some(Some(code)) - None => if is_hex_color(value) { - Some(Some(rgb_fg_code(value))) - } else { - None +fn inline_style_from_text_style( + base : InlineStyle, + style : TextStyle, + formatter : TextFormatter, +) -> InlineStyle? { + let fg = match style.fg { + None => Some((base.fg_code, base.fg_basic_name)) + Some(value) => { + let normalized = normalize_style_tag_name(value) + match named_color_code(normalized) { + Some(code) => Some((Some(code), Some(normalized))) + None => if is_hex_color(value) { + let basic_name = basic_name_from_hex(value) + Some(( + Some(resolve_inline_color_code(formatter, Some(basic_name), rgb_fg_code(value))), + Some(basic_name), + )) + } else { + None + } } } } - let bg_code = match style.bg { - None => Some(base.bg_code) - Some(value) => match named_bg_color_code(normalize_style_tag_name(value)) { - Some(code) => Some(Some(code)) - None => if is_hex_color(value) { - Some(Some(rgb_bg_code_from_hex(value))) - } else { - None + let bg = match style.bg { + None => Some((base.bg_code, base.bg_basic_name)) + Some(value) => { + let normalized = normalize_style_tag_name(value) + match named_bg_color_code(normalized) { + Some(code) => { + let bg_name = if normalized.has_prefix("bright_") { + normalized + } else { + normalized + } + Some((Some(code), Some(bg_name))) + } + None => if is_hex_color(value) { + let basic_name = basic_name_from_hex(value) + Some(( + Some(resolve_inline_bg_code(formatter, Some(basic_name), rgb_bg_code_from_hex(value))), + Some(basic_name), + )) + } else { + None + } } } } - match fg_code { - Some(next_fg) => match bg_code { - Some(next_bg) => Some({ - fg_code: next_fg, - bg_code: next_bg, + match fg { + Some((next_fg_code, next_fg_name)) => match bg { + Some((next_bg_code, next_bg_name)) => Some({ + fg_code: next_fg_code, + bg_code: next_bg_code, + fg_basic_name: next_fg_name, + bg_basic_name: next_bg_name, bold: base.bold || style.bold, dim: base.dim || style.dim, italic: base.italic || style.italic, @@ -511,12 +650,22 @@ fn resolve_registered_text_style(tag : String, formatter : TextFormatter) -> Tex fn apply_inline_tag(style : InlineStyle, tag : String, formatter : TextFormatter) -> InlineStyle? { let normalized = normalize_style_tag_name(tag) if is_hex_color(tag) { - Some({ ..style, fg_code: Some(rgb_fg_code(tag)) }) + let basic_name = basic_name_from_hex(tag) + Some({ + ..style, + fg_code: Some(resolve_inline_color_code(formatter, Some(basic_name), rgb_fg_code(tag))), + fg_basic_name: Some(basic_name), + }) } else if normalized.length() == 10 && normalized.has_prefix("bg:") && is_hex_color(normalized[3:].to_owned()) { - Some({ ..style, bg_code: Some(rgb_bg_code(normalized)) }) + let basic_name = basic_name_from_hex(normalized[3:].to_owned()) + Some({ + ..style, + bg_code: Some(resolve_inline_bg_code(formatter, Some(basic_name), rgb_bg_code(normalized))), + bg_basic_name: Some(basic_name), + }) } else { match resolve_registered_text_style(normalized, formatter) { - Some(spec) => inline_style_from_text_style(style, spec) + Some(spec) => inline_style_from_text_style(style, spec, formatter) None => None } } diff --git a/docs/README-en.md b/docs/README-en.md index e34c67c..b57702d 100644 --- a/docs/README-en.md +++ b/docs/README-en.md @@ -281,6 +281,7 @@ match logger.file_runtime_state() { - `QueueConfig`, `TextFormatterConfig`, and `SinkConfig` can also be exported independently through `queue_config_to_json(...)` / `stringify_queue_config(...)`, `text_formatter_config_to_json(...)` / `stringify_text_formatter_config(...)`, and `sink_config_to_json(...)` / `stringify_sink_config(...)`. - Supported keys include `min_level`, `target`, `timestamp`, `sink.kind`, `sink.path`, `sink.append`, `sink.auto_flush`, `sink.rotation`, `sink.text_formatter`, and `queue`. - `TextFormatter` and `TextFormatterConfig` now include `color_mode = Never | Auto | Always` for ANSI text coloring control. +- `TextFormatter` and `TextFormatterConfig` also include `color_support = basic | truecolor` so hex / RGB styling can be forced to downgrade to basic ANSI colors. - `TextFormatter` and `TextFormatterConfig` also include `style_markup = disabled | builtin | full` so callers can choose whether style markup is parsed and whether custom tags are active. - `target_style_markup` and `fields_style_markup` independently control whether `target` and `fields` are parsed for style markup. - `message` also supports lightweight inline style tags such as `...`, `...`, `<#ff0000>...`, and `...`. @@ -314,6 +315,7 @@ match logger.file_runtime_state() { - `file_sink_state_to_json(...)`, `stringify_file_sink_state(...)`, `runtime_file_state_to_json(...)`, and `stringify_runtime_file_state(...)` can export file and queued-file snapshots directly as JSON for diagnostics or reporting. - `sink.text_formatter.template` currently supports fixed tokens: `{timestamp}`, `{timestamp_ms}`, `{level}`, `{target}`, `{message}`, and `{fields}`. - `sink.text_formatter.color_mode` currently supports `never`, `auto`, and `always`. +- `sink.text_formatter.color_support` currently supports `basic` and `truecolor`. - `sink.text_formatter.style_markup` currently supports `disabled`, `builtin`, and `full`. - `sink.text_formatter.target_style_markup` and `sink.text_formatter.fields_style_markup` currently support `disabled`, `builtin`, and `full`. - `sink.text_formatter.style_tags.` currently supports `fg`, `bg`, `bold`, `dim`, `italic`, and `underline`. diff --git a/docs/changes/0.4.0.md b/docs/changes/0.4.0.md index 0b1d4b0..9570105 100644 --- a/docs/changes/0.4.0.md +++ b/docs/changes/0.4.0.md @@ -19,6 +19,7 @@ version 0.4.0 - feat: add builtin semantic style tags such as `accent`, `info`, `success`, `warning`, `danger`, and `muted` - feat: add independent `target_style_markup` and `fields_style_markup` controls for `TextFormatter` and `TextFormatterConfig` - feat: support named closing tags like `` alongside the existing short close `` +- feat: add `ColorSupport = Basic | TrueColor` and support `sink.text_formatter.color_support` so hex / RGB styles can downgrade to basic ANSI colors ### Test @@ -33,6 +34,7 @@ version 0.4.0 - test: cover builtin semantic tag rendering and confirm user overrides still take precedence - test: cover target and field markup rendering, plus config roundtrip for the new formatter markup scopes - test: cover named closing tags, mixed short/named closing, and unmatched named-close fallback behavior +- test: cover basic color-support downgrade for hex foreground/background rendering in runtime and config paths ### Example @@ -49,3 +51,4 @@ version 0.4.0 - JSON config currently supports concrete style objects only; alias-style declarations remain runtime-only - users can now decide whether custom style parsing is enabled through runtime formatter APIs or `sink.text_formatter.style_markup` - `fields_style_markup` currently styles field values only and intentionally leaves field keys raw +- `basic` color support currently keeps ANSI styling but downgrades hex / RGB colors to the nearest basic ANSI color family