From 5030d4b474bf2e315be82823099e7fd4d09a3fe1 Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 29 Aug 2025 19:09:07 +0200 Subject: [PATCH] ANSI 2 --- codex/base/ansi2/capabilities.go | 13 + ...{z_itoa_test.go => capabilities_detect.go} | 114 ++++-- .../base/ansi2/capabilities_detect_windows.go | 150 ++++++++ codex/base/ansi2/color.go | 57 +++ codex/base/ansi2/style.go | 163 ++++++++- codex/base/ansi2/z_ansi_all_test.go | 331 ++++++++++++++++++ ...ark_test.go => z_build_benchmark_test.gos} | 0 7 files changed, 794 insertions(+), 34 deletions(-) rename codex/base/ansi2/{z_itoa_test.go => capabilities_detect.go} (54%) create mode 100644 codex/base/ansi2/capabilities_detect_windows.go create mode 100644 codex/base/ansi2/z_ansi_all_test.go rename codex/base/ansi2/{z_build_benchmark_test.go => z_build_benchmark_test.gos} (100%) diff --git a/codex/base/ansi2/capabilities.go b/codex/base/ansi2/capabilities.go index 78f99c4..59f3de6 100644 --- a/codex/base/ansi2/capabilities.go +++ b/codex/base/ansi2/capabilities.go @@ -98,6 +98,7 @@ const ( + type ( Capabilities uint16 @@ -107,6 +108,17 @@ type ( // Métodos para verificar capacidades de color +func (c Capabilities) CanColorType (ct ColorType) bool { + switch ct { + case ColorTypeNone : return true + case ColorTypeBasic : return c&CanBasicColor != 0 + case ColorTypeIndex : return c&Can256Color != 0 + case ColorTypeTrue : return c&CanTrueColor != 0 + default: + return false + } +} + func (c Capabilities) CanBasicColor () bool { return c&CanBasicColor != 0 } func (c Capabilities) Can256Color () bool { return c&Can256Color != 0 } func (c Capabilities) CanTrueColor () bool { return c&CanTrueColor != 0 } @@ -148,3 +160,4 @@ func (c Capabilities) String () string { return strings.Join(cs, "|") } + diff --git a/codex/base/ansi2/z_itoa_test.go b/codex/base/ansi2/capabilities_detect.go similarity index 54% rename from codex/base/ansi2/z_itoa_test.go rename to codex/base/ansi2/capabilities_detect.go index d31b33a..9f61e3d 100644 --- a/codex/base/ansi2/z_itoa_test.go +++ b/codex/base/ansi2/capabilities_detect.go @@ -1,10 +1,10 @@ /* —————————————————————————————————————————————————————————————————————————— Proyecto WIAWAIW ————————————————————————————————————————————————————————————————————————————— - Fichero z_itoa_test.go + Fichero capabilities_detect.go Package ansi2 Autor Juan V. Navarro juanvnl@activething.com - Creado 28/08/2025 + Creado 29/08/2025 ————————————————————————————————————————————————————————————————————————————— LICENSES AND TERMS OF USE @@ -47,32 +47,94 @@ Copyright (c) 2025 Active Thing ————————————————————————————————————————————————————————————————————————————— */ +//go:build !windows +// +build !windows + package ansi2 -import "testing" - -func TestAppendItoa(t *testing.T) { - tests := []struct { - num uint8 - expected string - }{ - {0, "0"}, - {5, "5"}, - {9, "9"}, - {10, "10"}, - {42, "42"}, - {99, "99"}, - {100, "100"}, - {123, "123"}, - {255, "255"}, +import ( +"os" +"strings" +) + +// DetectTerminalCapabilities detecta capacidades ANSI para un descriptor de salida en sistemas tipo Unix. +// - f: archivo de salida (por ejemplo os.Stdout o os.Stderr) +// - No usa procesos externos, se basa en heurísticas y en si f parece un dispositivo de caracteres. +func DetectTerminalCapabilities(f *os.File) Capabilities { + if f == nil { + f = os.Stdout + } + + // Respeta NO_COLOR + if _, ok := os.LookupEnv("NO_COLOR"); ok { + return CanNone + } + + // Si el descriptor no es un dispositivo de caracteres (p. ej. redirigido a archivo/pipe), no hay color. + if !isCharDevice(f) { + return CanNone + } + + // FORCE_COLOR fuerza todas las capacidades + if _, ok := os.LookupEnv("FORCE_COLOR"); ok { + return forcedAllCapabilities() } - for _, tt := range tests { - t.Run("num="+tt.expected, func(t *testing.T) { - buffer := AppendItoa([]byte{}, tt.num) - if got := string(buffer); got != tt.expected { - t.Errorf("AppendItoa(%d) = %q, want %q", tt.num, got, tt.expected) - } - }) + term := strings.ToLower(os.Getenv("TERM")) + colorterm := strings.ToLower(os.Getenv("COLORTERM")) + termProgram := strings.ToLower(os.Getenv("TERM_PROGRAM")) + + var caps Capabilities + + // --- Colores --- + switch { + case strings.Contains(colorterm, "truecolor") || strings.Contains(colorterm, "24bit"): + caps |= CanTrueColor | Can256Color | CanBasicColor + case strings.Contains(term, "256color") || strings.Contains(term, "256-colors") || strings.Contains(term, "256-colour"): + caps |= Can256Color | CanBasicColor + case term != "" && strings.Contains(term, "color"): + caps |= CanBasicColor } -} \ No newline at end of file + + // Heurísticas: terminales modernos que suelen soportar estilos avanzados y a menudo truecolor + if colorterm != "" || + strings.Contains(term, "xterm") || + strings.Contains(term, "xterm-256color") || + strings.Contains(term, "screen") || + strings.Contains(term, "tmux") || + strings.Contains(term, "rxvt") || + strings.Contains(term, "konsole") || + strings.Contains(termProgram, "iterm") || + strings.Contains(termProgram, "wezterm") || + strings.Contains(termProgram, "alacritty") || + strings.Contains(termProgram, "kitty") || + strings.Contains(termProgram, "vscode") || + strings.Contains(termProgram, "gnome") { + caps |= allStyleCapabilities() + } + + // TMUX suele preservar capacidades, aseguramos al menos basic color si TERM sugiere color + if os.Getenv("TMUX") != "" { + if caps&CanBasicColor == 0 { + caps |= CanBasicColor + } + caps |= CanAllExtended + } + + return caps +} + +// isCharDevice intenta determinar si f es un dispositivo de caracteres (tty). +// Es una comprobación portable usando FileInfo.Mode & os.ModeCharDevice, que funciona en +// la mayoría de sistemas Unix-like de forma simple y sin dependencias externas. +func isCharDevice(f *os.File) bool { + if f == nil { + return false + } + fi, err := f.Stat() + if err != nil { + return false + } + return fi.Mode()&os.ModeCharDevice != 0 +} + diff --git a/codex/base/ansi2/capabilities_detect_windows.go b/codex/base/ansi2/capabilities_detect_windows.go new file mode 100644 index 0000000..f2d2c43 --- /dev/null +++ b/codex/base/ansi2/capabilities_detect_windows.go @@ -0,0 +1,150 @@ +/* —————————————————————————————————————————————————————————————————————————— + Proyecto WIAWAIW +————————————————————————————————————————————————————————————————————————————— + Fichero Canabilities_detect_windows.go + Package ansi2 + Autor Juan V. Navarro juanvnl@activething.com + Creado 29/08/2025 +————————————————————————————————————————————————————————————————————————————— + + LICENSES AND TERMS OF USE + ------------------------- + + This software is licensed under the Elastic License v2.0 (the "License"). + For full terms and additional information regarding permitted and prohibited + uses, please visit: + https://activething.com/wiawiaw/licenses + + You may use, copy, modify, and redistribute this software internally within + your organization for any purpose, including research, development, and + testing, subject to the terms of this License. + + You may NOT, however, use, provide, distribute, or make this software + available to any third party as part of a hosted service, SaaS offering, or + commercial product without first obtaining a commercial license from + Active Thing. + + You may combine this software with other code, provided that such + combination does not circumvent the restrictions of this License. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. In no event shall + the authors or copyright holders be liable for any claim, damages, or other + liability arising from the use of this software. + +————————————————————————————————————————————————————————————————————————————— + + Web : activething.com | wiawiaw.com + git : g.activething.com | github.com/activething + Correo : dev@activething.com + +————————————————————————————————————————————————————————————————————————————— + No deseo caminar sobre el agua", dijo Siddhartha. + Que los antiguos chamanes se contenten con tales habilidades. + —— Hermann Hesse, Siddhartha +————————————————————————————————————————————————————————————————————————————— + Copyright (c) 2025 Active Thing +————————————————————————————————————————————————————————————————————————————— */ + +//go:build windows +// +build windows + +package ansi2 + +import ( + "os" + "strings" + "syscall" + "unsafe" +) + + +// flags Windows console +const ( + enablePOutput = 0x0001 + enableVTerminal = 0x0004 +) + + +// DetectTerminalCapabilities detecta capacidades ANSI para un descriptor de salida en Windows. +// - Intenta habilitar Virtual Terminal Processing para el handle dado (sin lanzar procesos externos). +// - Si el handle no es consola o no se puede habilitar, es conservador. +func DetectTerminalCapabilities(f *os.File) Capabilities { + if f == nil { + f = os.Stdout + } + + // Respeta NO_COLOR + if _, ok := os.LookupEnv("NO_COLOR"); ok { + return CanNone + } + + // FORCE_COLOR fuerza todas las capacidades + if _, ok := os.LookupEnv("FORCE_COLOR"); ok { + return CanAll + } + + handle := f.Fd() + + // Si no es consola (GetConsoleMode falla), entonces está redirigido -> sin color. + if !isConsole(handle) { + // Sin consola: pero si WT_SESSION / TERM_PROGRAM indican emuladores remotos, forzamos capacidades + if os.Getenv("WT_SESSION") != "" || strings.Contains(strings.ToLower(os.Getenv("TERM_PROGRAM")), "vscode") { + return CanAll + } + return CanNone + } + + // Intentar habilitar Virtual Terminal Processing para ese handle. + enabled := enableVTProcessing(handle) + + // Si logramos habilitar VT, asumimos soporte de estilos + colores básicos. + if enabled { + caps := CanBasicColor | CanAllExtended + + colorterm := strings.ToLower(os.Getenv("COLORTERM")) + term := strings.ToLower(os.Getenv("TERM")) + if strings.Contains(colorterm, "truecolor") || strings.Contains(colorterm, "24bit") { + caps |= CanTrueColor | Can256Color + } else if strings.Contains(term, "256color") { + caps |= Can256Color + } + // También si estamos en Windows Terminal o VSCode, damos todo por hecho + if os.Getenv("WT_SESSION") != "" || strings.Contains(strings.ToLower(os.Getenv("TERM_PROGRAM")), "vscode") { + caps |= CanTrueColor | Can256Color + } + return caps + } + + // Si no pudimos habilitar VT, devolvemos lo básico si estamos en consola. + // En versiones antiguas de consola sólo colores básicos funcionan. + return CanBasicColor +} + +func isConsole(handle uintptr) bool { + kernel32 := syscall.NewLazyDLL("kernel32.dll") + getConsoleMode := kernel32.NewProc("GetConsoleMode") + var mode uint32 + r1, _, _ := getConsoleMode.Call(handle, uintptr(unsafe.Pointer(&mode))) + return r1 != 0 +} + +// enableVTProcessing intenta activar VT para el handle dado. +// Retorna true si se activó correctamente. +func enableVTProcessing(handle uintptr) bool { + kernel32 := syscall.NewLazyDLL("kernel32.dll") + getConsoleMode := kernel32.NewProc("GetConsoleMode") + setConsoleMode := kernel32.NewProc("SetConsoleMode") + + var mode uint32 + r1, _, _ := getConsoleMode.Call(handle, uintptr(unsafe.Pointer(&mode))) + if r1 == 0 { + return false + } + + newMode := mode | enableVTerminal | enablePOutput + r1, _, _ = setConsoleMode.Call(handle, uintptr(newMode)) + return r1 != 0 +} + diff --git a/codex/base/ansi2/color.go b/codex/base/ansi2/color.go index e0b026b..4d4fbde 100644 --- a/codex/base/ansi2/color.go +++ b/codex/base/ansi2/color.go @@ -209,3 +209,60 @@ func (c Color) trueColor () (r, g, b uint8) {return uint8((c & maskR) >> shiftR) +// RGBToIndex convierte un color RGB a un índice de 256 colores (aproximación). +func RGBToIndex(r, g, b uint8) uint8 { + // Mapeo al cubo 6x6x6 de la paleta de 256 colores (índices 16-231) + // Escala cada componente RGB a un valor de 0 a 5 (6 niveles) + r6 := (uint32(r) * 5 + 127) / 255 // Escala 0-255 a 0-5 + g6 := (uint32(g) * 5 + 127) / 255 + b6 := (uint32(b) * 5 + 127) / 255 + // Índice: 16 + 36*r6 + 6*g6 + b6 + return uint8(16 + 36*r6 + 6*g6 + b6) +} + + +// IndexToBasic convierte un índice de 256 colores a un color básico (0-15). +func IndexToBasic(index uint8) uint8 { + // Colores básicos (0-15) se devuelven directamente + if index < 16 { + return index + } + // Para índices 16-231 (cubo 6x6x6), aproximamos al color básico más cercano + if index < 232 { + r := (index - 16) / 36 + g := ((index - 16) % 36) / 6 + b := (index - 16) % 6 + // Si algún componente es >= 3, usamos colores brillantes (8-15) + if r >= 3 || g >= 3 || b >= 3 { + // Aproximamos al color básico brillante basado en el componente dominante + if r >= g && r >= b { + return 9 // Rojo brillante + } else if g >= r && g >= b { + return 10 // Verde brillante + } else { + return 12 // Azul brillante + } + } + // Colores no brillantes (0-7) + if r >= g && r >= b { + return 1 // Rojo + } else if g >= r && g >= b { + return 2 // Verde + } else { + return 4 // Azul + } + } + // Índices 232-255 (escala de grises) se mapean a blanco o negro + if index >= 244 { + return 15 // Blanco para tonos claros + } + return 0 // Negro para tonos oscuros +} + + + +// RGBToBasic convierte un color RGB a un color básico (0-15). +func RGBToBasic(r, g, b uint8) uint8 { + // Convierte primero a índice y luego a básico + return IndexToBasic(RGBToIndex(r, g, b)) +} \ No newline at end of file diff --git a/codex/base/ansi2/style.go b/codex/base/ansi2/style.go index 79a966a..1eb98e8 100644 --- a/codex/base/ansi2/style.go +++ b/codex/base/ansi2/style.go @@ -169,9 +169,9 @@ func (s Style) GetFgColorType() ColorType { return ColorType((s & maskFgType) >> func (s Style) GetBgColorType() ColorType { return ColorType((s & maskBgType) >> shiftBgType) } -func (s Style) HasFgColor() bool { return s.GetFgColorType() != ColorTypeNone } +func (s Style) HasFgColor() bool { return s & maskFgType != 0 } -func (s Style) HasBgColor() bool { return s.GetBgColorType() != ColorTypeNone } +func (s Style) HasBgColor() bool { return s & maskBgType != 0 } // ========================================================= @@ -357,6 +357,9 @@ func (s Style) IndexBgColor(index uint8) Style { return s.Bg(MakeIndexColor(inde func (s Style) GrayFgColor(gray uint8) Style { return s.Fg(MakeGrayColor(gray)) } func (s Style) GrayBgColor(gray uint8) Style { return s.Bg(MakeGrayColor(gray)) } + +func (s Style) BasicFgColor (colro uint8) Style { return s.Fg(MakeBasicColor(colro)) } +func (s Style) BasicBgColor (colro uint8) Style { return s.Bg(MakeBasicColor(colro)) } func (s Style) Adapt (caps Capabilities) Style { @@ -364,14 +367,64 @@ func (s Style) Adapt (caps Capabilities) Style { return 0 } newStyle := Style(0) - // Adaptar formatos (bits 0–11) - newStyle |= (s & maskTextFormat) & Style(caps&maskTextFormat) + ct := s.GetFgColorType() + if !caps.CanColorType(ct) { + newStyle |= newStyle &^(maskFgType | maskFgData) | s.adaptColor(caps,ct,true) + } + ct = s.GetBgColorType() + if !caps.CanColorType(s.GetBgColorType()) { + newStyle |= newStyle &^(maskBgType | maskBgData) | s.adaptColor(caps,ct,false) + } + return newStyle | (s & maskTextFormat) & Style(caps&maskTextFormat) +} - return s + +func (s Style) adaptColor(caps Capabilities, ct ColorType, fg bool) Style { + var ( + color Color + maskType, maskData Style + ) + if fg { + color = s.GetFg() + maskType = maskFgType + maskData = maskFgData + } else { + color = s.GetBg() + maskType = maskBgType + maskData = maskBgData + } + switch { + case ct == ColorTypeBasic && caps.CanBasicColor(): + return s & (maskType | maskData) + case ct == ColorTypeIndex && caps.Can256Color(): + return s & (maskType | maskData) + case ct == ColorTypeIndex && caps.CanBasicColor() && !caps.Can256Color(): + //basicCl := IndexToBasic(color.indexColor()) + if fg { + return Style(0).BasicFgColor(IndexToBasic(color.indexColor())) + } + return Style(0).BasicBgColor(IndexToBasic(color.indexColor())) + case ct == ColorTypeTrue && caps.CanTrueColor(): + return s & (maskType | maskData) + case ct == ColorTypeTrue && caps.Can256Color(): + //indexCl := RGBToIndex(color.trueColor()) + if fg { + return Style(0).IndexFgColor(RGBToIndex(color.trueColor())) + } + return Style(0).IndexBgColor(RGBToIndex(color.trueColor())) + case ct == ColorTypeTrue && caps.CanBasicColor(): + //basicCl := RGBToBasic(color.trueColor()) + if fg { + return Style(0).BasicFgColor(RGBToBasic(color.trueColor())) + } + return Style(0).BasicBgColor(RGBToBasic(color.trueColor())) + } + return Style(0) } +/* func (s Style) Build () AnsiStyle { hsFg := s & (maskFgType) hsBg := s & (maskBgType) @@ -395,7 +448,6 @@ func (s Style) Build () AnsiStyle { cl = cl - 8 bf = append(bf, '9') } - bf = AppendItoa(bf, cl) case ColorTypeIndex: cl := s.GetFg().indexColor() bf = append(bf, '3', '8', ';', '5', ';') @@ -425,7 +477,6 @@ func (s Style) Build () AnsiStyle { } else { bf = append(bf, '1','0','0'+cl - 8) } - bf = AppendItoa(bf, cl) case ColorTypeIndex: cl := s.GetBg().indexColor() bf = append(bf, '4', '8', ';', '5', ';') @@ -456,11 +507,104 @@ func (s Style) Build () AnsiStyle { } } + if !hs { + return NoStyle + } - return AnsiStyle(bf) + return AnsiStyle(append(bf,ansiCodeEndStyle)) } +*/ + +// En ansi-all.gos + +func (s Style) Build() AnsiStyle { + hasFg := s & maskFgType + hasBg := s & maskBgType + hasFm := s & maskTextFormat + + if hasFg|hasBg|hasFm == 0 { + return NoStyle + } + + bf := make([]byte, 0, defaultStyleSize) + bf = append(bf, ansiFormatBegin...) // Agrega "\x1b[" + needsSep := false + + // --- Color de Primer Plano --- + if hasFg != 0 { + needsSep = true + switch s.GetFgColorType() { + case ColorTypeBasic: + c := s.GetFg().basicColor() + if c < 8 { // Códigos 30-37 + bf = append(bf, '3', '0'+c) + } else { // Códigos 90-97 + bf = append(bf, '9', '0'+(c-8)) + } + case ColorTypeIndex: + c := s.GetFg().indexColor() + bf = append(bf, '3', '8', ';', '5', ';') + bf = AppendItoa(bf, c) + case ColorTypeTrue: + r, g, b := s.GetFg().trueColor() + bf = append(bf, '3', '8', ';', '2', ';') + bf = AppendItoa(bf, r) + bf = append(bf, ';') + bf = AppendItoa(bf, g) + bf = append(bf, ';') + bf = AppendItoa(bf, b) + } + } + + // --- Color de Fondo --- + if hasBg != 0 { + if needsSep { + bf = append(bf, ';') + } + needsSep = true + switch s.GetBgColorType() { + case ColorTypeBasic: + c := s.GetBg().basicColor() + if c < 8 { // Códigos 40-47 + bf = append(bf, '4', '0'+c) + } else { // Códigos 100-107 + bf = append(bf, '1', '0', '0'+(c-8)) + } + case ColorTypeIndex: + c := s.GetBg().indexColor() + bf = append(bf, '4', '8', ';', '5', ';') + bf = AppendItoa(bf, c) + case ColorTypeTrue: + r, g, b := s.GetBg().trueColor() + bf = append(bf, '4', '8', ';', '2', ';') + bf = AppendItoa(bf, r) + bf = append(bf, ';') + bf = AppendItoa(bf, g) + bf = append(bf, ';') + bf = AppendItoa(bf, b) + } + } + + // --- Formato de Texto --- + if hasFm != 0 { + formatBits := s & maskTextFormat + for i := 0; i < numBitsFormat; i++ { + if formatBits&(1< 0 { + prefix = msg[0] + } + t.Errorf("%s: got %v, want %v", prefix, got, want) + } +} + +func TestMakeStyle(t *testing.T) { + t.Run("Create style with multiple options", func(t *testing.T) { + s := MakeStyle(Bold(), FgRed(), BgWhite()) + + assertEqual(t, s.IsBold(), true, "IsBold") + assertEqual(t, s.HasFgColor(), true, "HasFgColor") + assertEqual(t, s.HasBgColor(), true, "HasBgColor") + assertEqual(t, s.GetFg(), BasicRed(), "GetFg") + assertEqual(t, s.GetBg(), BasicWhite(), "GetBg") + }) + + t.Run("Create an empty style", func(t *testing.T) { + s := MakeStyle() + assertEqual(t, s.HasFormat(), false, "HasFormat") + assertEqual(t, s.HasColor(), false, "HasColor") + }) +} + +func TestStyle_Formatters(t *testing.T) { + s := Style(0) + + t.Run("Bold vs Faint", func(t *testing.T) { + s1 := s.Bold() + assertEqual(t, s1.IsBold(), true, "s1.IsBold") + assertEqual(t, s1.IsFaint(), false, "s1.IsFaint") + + s2 := s1.Faint() + assertEqual(t, s2.IsBold(), false, "s2.IsBold") + assertEqual(t, s2.IsFaint(), true, "s2.IsFaint") + }) + + t.Run("Blink", func(t *testing.T) { + s1 := s.SlowBlink() + assertEqual(t, s1.IsSlowBlink(), true, "s1.IsSlowBlink") + assertEqual(t, s1.IsRapidBlink(), false, "s1.IsRapidBlink") + + s2 := s1.RapidBlink() + assertEqual(t, s2.IsSlowBlink(), false, "s2.IsSlowBlink") + assertEqual(t, s2.IsRapidBlink(), true, "s2.IsRapidBlink") + }) + + t.Run("Remove format", func(t *testing.T) { + s1 := MakeStyle(Bold(), Italic()) + s2 := s1.NoBold() + assertEqual(t, s2.IsBold(), false, "s2.IsBold") + assertEqual(t, s2.IsItalic(), true, "s2.IsItalic") + }) +} + +func TestStyle_FgBg(t *testing.T) { + s := Style(0) + + t.Run("Set and Get TrueColor", func(t *testing.T) { + s1 := s.TrueFgColor(10, 20, 30).TrueBgColor(40, 50, 60) + fg := s1.GetFg() + bg := s1.GetBg() + + assertEqual(t, fg.ColorType(), ColorTypeTrue, "fg.ColorType") + assertEqual(t, bg.ColorType(), ColorTypeTrue, "bg.ColorType") + + r, g, b, ok := fg.GetTrueColor() + assertEqual(t, ok, true, "fg.GetTrueColor ok") + assertEqual(t, r, uint8(10), "fg r") + assertEqual(t, g, uint8(20), "fg g") + assertEqual(t, b, uint8(30), "fg b") + + r, g, b, ok = bg.GetTrueColor() + assertEqual(t, ok, true, "bg.GetTrueColor ok") + assertEqual(t, r, uint8(40), "bg r") + assertEqual(t, g, uint8(50), "bg g") + assertEqual(t, b, uint8(60), "bg b") + }) + + t.Run("Set and Get IndexColor", func(t *testing.T) { + s1 := s.IndexFgColor(123).IndexBgColor(234) + fg := s1.GetFg() + bg := s1.GetBg() + + assertEqual(t, fg.ColorType(), ColorTypeIndex, "fg.ColorType") + assertEqual(t, bg.ColorType(), ColorTypeIndex, "bg.ColorType") + + idx, ok := fg.GetIndexColor() + assertEqual(t, ok, true, "fg.GetIndexColor ok") + assertEqual(t, idx, uint8(123), "fg index") + + idx, ok = bg.GetIndexColor() + assertEqual(t, ok, true, "bg.GetIndexColor ok") + assertEqual(t, idx, uint8(234), "bg index") + }) +} + +func TestStyle_Adapt(t *testing.T) { + // Estilo original con alta capacidad (TrueColor y Bold) + originalStyle := MakeStyle(Bold(), FgTrueColor(100, 150, 200), BgTrueColor(20, 40, 60)) + + t.Run("Adapt TrueColor to 256Color", func(t *testing.T) { + // Capacidades: soporta 256 colores y negrita + caps := Can256Color | CanFormatBold + adaptedStyle := originalStyle.Adapt(caps) + + // El formato se debe mantener + assertEqual(t, adaptedStyle.IsBold(), true, "Adapted style should be bold") + + // El color de primer plano debe ser de tipo Index + fg := adaptedStyle.GetFg() + assertEqual(t, fg.ColorType(), ColorTypeIndex, "Adapted Fg should be IndexColor") + fgIndex, _ := fg.GetIndexColor() + expectedFgIndex := RGBToIndex(100, 150, 200) + assertEqual(t, fgIndex, expectedFgIndex, "Adapted Fg index mismatch") + + // El color de fondo debe ser de tipo Index + bg := adaptedStyle.GetBg() + assertEqual(t, bg.ColorType(), ColorTypeIndex, "Adapted Bg should be IndexColor") + bgIndex, _ := bg.GetIndexColor() + expectedBgIndex := RGBToIndex(20, 40, 60) + assertEqual(t, bgIndex, expectedBgIndex, "Adapted Bg index mismatch") + }) + + t.Run("Adapt TrueColor to BasicColor", func(t *testing.T) { + // Capacidades: solo soporta colores básicos + caps := CanBasicColor + adaptedStyle := originalStyle.Adapt(caps) + + // El formato se debe descartar + assertEqual(t, adaptedStyle.IsBold(), false, "Adapted style should not be bold") + + // El color de primer plano debe ser de tipo Basic + fg := adaptedStyle.GetFg() + assertEqual(t, fg.ColorType(), ColorTypeBasic, "Adapted Fg should be BasicColor") + fgBasic, _ := fg.GetBasicColor() + expectedFgBasic := RGBToBasic(100, 150, 200) + assertEqual(t, fgBasic, expectedFgBasic, "Adapted Fg basic color mismatch") + + // El color de fondo debe ser de tipo Basic + bg := adaptedStyle.GetBg() + assertEqual(t, bg.ColorType(), ColorTypeBasic, "Adapted Bg should be BasicColor") + bgBasic, _ := bg.GetBasicColor() + expectedBgBasic := RGBToBasic(20, 40, 60) + assertEqual(t, bgBasic, expectedBgBasic, "Adapted Bg basic color mismatch") + }) + + t.Run("Adapt 256Color to BasicColor", func(t *testing.T) { + // Estilo con color indexado (un tipo de azul) + indexedStyle := MakeStyle(Italic(), FgIndexColor(21)) // 21 es un azul + caps := CanBasicColor | CanFormatItalic + adaptedStyle := indexedStyle.Adapt(caps) + + assertEqual(t, adaptedStyle.IsItalic(), true, "Adapted style should be italic") + + fg := adaptedStyle.GetFg() + assertEqual(t, fg.ColorType(), ColorTypeBasic, "Adapted Fg should be BasicColor") + fgBasic, _ := fg.GetBasicColor() + expectedFgBasic := IndexToBasic(21) // Debería convertirse a azul básico + assertEqual(t, fgBasic, expectedFgBasic, fmt.Sprintf("Expected basic blue (%d)", expectedFgBasic)) + }) + + t.Run("Adapt to no color support", func(t *testing.T) { + caps := CanFormatBold // Solo soporta negrita, sin colores + adaptedStyle := originalStyle.Adapt(caps) + + // El formato se mantiene + assertEqual(t, adaptedStyle.IsBold(), true, "Adapted style should be bold") + // No debe tener color + assertEqual(t, adaptedStyle.HasColor(), false, "Adapted style should have no color") + }) + + t.Run("Adapt to no capabilities", func(t *testing.T) { + caps := CanNone + adaptedStyle := originalStyle.Adapt(caps) + // El estilo resultante debe ser 0 (sin formato ni color) + assertEqual(t, adaptedStyle, Style(0), "Adapted style should be empty") + }) +} + +// En z_ansi_all_test.go (o como se llame tu archivo de pruebas) + +func TestStyle_Build(t *testing.T) { + testCases := []struct { + name string + style Style + expected AnsiStyle + }{ + { + name: "NoStyle", + style: Style(0), + expected: NoStyle, + }, + { + name: "Bold only", + style: MakeStyle(Bold()), + expected: "\x1b[1m", + }, + { + name: "Fg Basic Red", + style: MakeStyle(FgRed()), + expected: "\x1b[31m", + }, + { + name: "Fg Bright Black", + style: MakeStyle(FgBrightBlack()), + expected: "\x1b[90m", + }, + { + name: "Bg Basic Green", + style: MakeStyle(BgGreen()), + expected: "\x1b[42m", + }, + { + name: "Bold and Fg Cyan", + style: MakeStyle(Bold(), FgCyan()), + expected: "\x1b[36;1m", + }, + { + name: "Italic, Fg Yellow, Bg Blue", + style: MakeStyle(Italic(), FgYellow(), BgBlue()), + expected: "\x1b[33;44;3m", + }, + { + name: "Fg Index Color", + style: MakeStyle(FgIndexColor(123)), + expected: "\x1b[38;5;123m", + }, + { + name: "Bg Index Color", + style: MakeStyle(BgIndexColor(201)), + expected: "\x1b[48;5;201m", + }, + { + name: "Fg TrueColor", + style: MakeStyle(FgTrueColor(10, 20, 30)), + expected: "\x1b[38;2;10;20;30m", + }, + { + name: "Bg TrueColor", + style: MakeStyle(BgTrueColor(110, 120, 130)), + expected: "\x1b[48;2;110;120;130m", + }, + { + name: "Complex Style", + style: MakeStyle(Underline(), CrossedOut(), FgTrueColor(50, 50, 50), BgIndexColor(250)), + expected: "\x1b[38;2;50;50;50;48;5;250;4;9m", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := tc.style.Build() + // Esta comparación ahora debería funcionar + assertEqual(t, got, tc.expected, "Build output mismatch") + }) + } +} + +func TestAnsiStyle_Stylize(t *testing.T) { + style := MakeStyle(FgRed()).Build() + text := "hello" + expected := "\x1b[31mhello\x1b[0m" + got := style.Stylize(text) + assertEqual(t, got, expected) + + noStyle := NoStyle + assertEqual(t, noStyle.Stylize("text"), "text") +} \ No newline at end of file diff --git a/codex/base/ansi2/z_build_benchmark_test.go b/codex/base/ansi2/z_build_benchmark_test.gos similarity index 100% rename from codex/base/ansi2/z_build_benchmark_test.go rename to codex/base/ansi2/z_build_benchmark_test.gos