diff --git a/functions/tui/apply_gradient.go b/functions/tui/apply_gradient.go new file mode 100644 index 00000000..f419f0b0 --- /dev/null +++ b/functions/tui/apply_gradient.go @@ -0,0 +1,48 @@ +package tui + +import ( + "strings" + + "github.com/charmbracelet/lipgloss" +) + +// DefaultGradientColors is the default color palette for ASCII art gradients. +// Purple → blue → cyan → red, applied line by line. +var DefaultGradientColors = []lipgloss.Color{ + lipgloss.Color("#9b59b6"), + lipgloss.Color("#8e44ad"), + lipgloss.Color("#3498db"), + lipgloss.Color("#2980b9"), + lipgloss.Color("#1abc9c"), + lipgloss.Color("#16a085"), + lipgloss.Color("#e74c3c"), + lipgloss.Color("#c0392b"), +} + +// ApplyGradient applies a color gradient to ASCII art lines. +// Each line gets a color from the palette, distributed proportionally. +// Pass nil for colors to use DefaultGradientColors. +func ApplyGradient(lines []string, colors []lipgloss.Color) string { + if len(lines) == 0 { + return "" + } + if len(colors) == 0 { + colors = DefaultGradientColors + } + + var result strings.Builder + totalLines := len(lines) + + for i, line := range lines { + colorIndex := (i * len(colors)) / totalLines + if colorIndex >= len(colors) { + colorIndex = len(colors) - 1 + } + styledLine := lipgloss.NewStyle().Foreground(colors[colorIndex]).Render(line) + result.WriteString(styledLine) + result.WriteString("\n") + } + + result.WriteString("\n") + return result.String() +} diff --git a/functions/tui/apply_gradient.md b/functions/tui/apply_gradient.md new file mode 100644 index 00000000..7d09589f --- /dev/null +++ b/functions/tui/apply_gradient.md @@ -0,0 +1,56 @@ +--- +name: apply_gradient +kind: function +lang: go +domain: tui +version: "1.0.0" +purity: pure +signature: "func ApplyGradient(lines []string, colors []lipgloss.Color) string" +description: "Aplica un degradado de colores linea por linea a texto ASCII art usando lipgloss. Distribuye los colores de la paleta proporcionalmente entre las lineas." +tags: [tui, ascii, gradient, color, lipgloss, art, header] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [strings, github.com/charmbracelet/lipgloss] +params: + - name: lines + desc: "lineas de ASCII art a colorear" + - name: colors + desc: "paleta de colores lipgloss; nil usa DefaultGradientColors (purple->blue->cyan->red)" +output: "string con las lineas coloreadas concatenadas con newlines" +tested: false +tests: [] +test_file_path: "" +file_path: "functions/tui/apply_gradient.go" +source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git" +source_license: "MIT" +source_file: "launcher/core/gradient.go" +--- + +## Ejemplo + +```go +lines := []string{ + " ██████╗ ███████╗", + " ██╔══██╗██╔════╝", + " ██║ ██║█████╗ ", +} +// Con paleta por defecto +fmt.Print(tui.ApplyGradient(lines, nil)) + +// Con paleta custom +palette := []lipgloss.Color{ + lipgloss.Color("#ff0000"), + lipgloss.Color("#00ff00"), + lipgloss.Color("#0000ff"), +} +fmt.Print(tui.ApplyGradient(lines, palette)) +``` + +## Notas + +Cada linea recibe un color de la paleta distribuido proporcionalmente: `colorIndex = (i * len(colors)) / totalLines`. Para ASCII art con pocas lineas, usar una paleta corta. Para muchas lineas, una paleta larga da transiciones mas suaves. + +`DefaultGradientColors` es una variable publica con 8 colores (purple->blue->cyan->red) que se puede reasignar si se desea cambiar el default global. diff --git a/functions/tui/box_chars.go b/functions/tui/box_chars.go new file mode 100644 index 00000000..980e1ff0 --- /dev/null +++ b/functions/tui/box_chars.go @@ -0,0 +1,14 @@ +package tui + +// Box drawing characters for TUI borders and separators. +const ( + BoxTL = "╔" + BoxTR = "╗" + BoxBL = "╚" + BoxBR = "╝" + BoxH = "═" + BoxV = "║" + BoxML = "╠" + BoxMR = "╣" + BoxSep = "─" +) diff --git a/functions/tui/draw_box.go b/functions/tui/draw_box.go new file mode 100644 index 00000000..dea598a1 --- /dev/null +++ b/functions/tui/draw_box.go @@ -0,0 +1,14 @@ +package tui + +import "github.com/charmbracelet/lipgloss" + +// DrawBox draws a Unicode box around content with the given width and style. +func DrawBox(content string, width int, style lipgloss.Style) string { + fill := lipgloss.NewStyle().Render( + lipgloss.PlaceHorizontal(width-2, lipgloss.Left, BoxH, lipgloss.WithWhitespaceChars(BoxH)), + ) + topLine := style.Render(BoxTL + fill + BoxTR) + bottomLine := style.Render(BoxBL + fill + BoxBR) + + return topLine + "\n" + content + "\n" + bottomLine +} diff --git a/functions/tui/draw_box.md b/functions/tui/draw_box.md new file mode 100644 index 00000000..36355203 --- /dev/null +++ b/functions/tui/draw_box.md @@ -0,0 +1,47 @@ +--- +name: draw_box +kind: function +lang: go +domain: tui +version: "1.0.0" +purity: pure +signature: "func DrawBox(content string, width int, style lipgloss.Style) string" +description: "Dibuja un box unicode (bordes dobles) alrededor de contenido con ancho y estilo lipgloss configurables." +tags: [tui, box, border, unicode, draw, lipgloss] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [github.com/charmbracelet/lipgloss] +params: + - name: content + desc: "texto interior del box" + - name: width + desc: "ancho total del box en caracteres" + - name: style + desc: "estilo lipgloss para los bordes" +output: "string con el box renderizado (top + content + bottom)" +tested: false +tests: [] +test_file_path: "" +file_path: "functions/tui/draw_box.go" +source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git" +source_license: "MIT" +source_file: "launcher/ui/styles.go" +--- + +## Ejemplo + +```go +style := lipgloss.NewStyle().Foreground(lipgloss.Color("#00d7ff")) +box := tui.DrawBox(" Hello World ", 40, style) +fmt.Println(box) +// ╔══════════════════════════════════════╗ +// Hello World +// ╚══════════════════════════════════════╝ +``` + +## Notas + +Usa caracteres de box drawing doble (BoxTL=╔, BoxH=═, etc.) definidos en box_chars.go. El contenido no se recorta ni centra automaticamente — el caller controla el formato interior. diff --git a/functions/tui/draw_separator.go b/functions/tui/draw_separator.go new file mode 100644 index 00000000..3f440048 --- /dev/null +++ b/functions/tui/draw_separator.go @@ -0,0 +1,13 @@ +package tui + +import "github.com/charmbracelet/lipgloss" + +// DrawSeparator draws a horizontal separator line of the given width. +func DrawSeparator(width int, style lipgloss.Style, sepChar string) string { + if sepChar == "" { + sepChar = BoxSep + } + return style.Render( + lipgloss.PlaceHorizontal(width, lipgloss.Left, sepChar, lipgloss.WithWhitespaceChars(sepChar)), + ) +} diff --git a/functions/tui/draw_separator.md b/functions/tui/draw_separator.md new file mode 100644 index 00000000..34523460 --- /dev/null +++ b/functions/tui/draw_separator.md @@ -0,0 +1,48 @@ +--- +name: draw_separator +kind: function +lang: go +domain: tui +version: "1.0.0" +purity: pure +signature: "func DrawSeparator(width int, style lipgloss.Style, sepChar string) string" +description: "Dibuja una linea separadora horizontal con ancho, estilo y caracter configurables. Por defecto usa el caracter de separacion simple (─)." +tags: [tui, separator, line, draw, lipgloss, divider] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [github.com/charmbracelet/lipgloss] +params: + - name: width + desc: "ancho de la linea en caracteres" + - name: style + desc: "estilo lipgloss para el separador" + - name: sepChar + desc: "caracter de separacion; vacio usa BoxSep (─)" +output: "string con la linea separadora renderizada" +tested: false +tests: [] +test_file_path: "" +file_path: "functions/tui/draw_separator.go" +source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git" +source_license: "MIT" +source_file: "launcher/ui/styles.go" +--- + +## Ejemplo + +```go +dim := lipgloss.NewStyle().Foreground(lipgloss.Color("#6c6c6c")) +sep := tui.DrawSeparator(60, dim, "") +fmt.Println(sep) +// ──────────────────────────────────────────────────────────── + +// Con caracter custom +sep2 := tui.DrawSeparator(40, dim, "═") +``` + +## Notas + +Si `sepChar` es vacio, usa `BoxSep` (─) de box_chars.go. Para lineas dobles usar "═", para puntos "·", etc. diff --git a/functions/tui/load_ascii_art.go b/functions/tui/load_ascii_art.go new file mode 100644 index 00000000..0ccc0c2f --- /dev/null +++ b/functions/tui/load_ascii_art.go @@ -0,0 +1,52 @@ +package tui + +import ( + "bufio" + "math/rand" + "os" + "path/filepath" + "strings" + "time" + + "github.com/charmbracelet/lipgloss" +) + +// LoadASCIIArt reads a random .txt file from staticPath and returns it +// rendered with a color gradient. Pass nil for colors to use DefaultGradientColors. +// Returns "" if no .txt files are found or on any error. +func LoadASCIIArt(staticPath string, colors []lipgloss.Color) string { + files, err := os.ReadDir(staticPath) + if err != nil { + return "" + } + + var txtFiles []string + for _, f := range files { + if !f.IsDir() && strings.HasSuffix(f.Name(), ".txt") { + txtFiles = append(txtFiles, f.Name()) + } + } + if len(txtFiles) == 0 { + return "" + } + + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + selected := txtFiles[rng.Intn(len(txtFiles))] + + file, err := os.Open(filepath.Join(staticPath, selected)) + if err != nil { + return "" + } + defer file.Close() + + var lines []string + sc := bufio.NewScanner(file) + for sc.Scan() { + lines = append(lines, sc.Text()) + } + + if len(lines) == 0 { + return "" + } + return ApplyGradient(lines, colors) +} diff --git a/functions/tui/load_ascii_art.md b/functions/tui/load_ascii_art.md new file mode 100644 index 00000000..81740630 --- /dev/null +++ b/functions/tui/load_ascii_art.md @@ -0,0 +1,54 @@ +--- +name: load_ascii_art +kind: function +lang: go +domain: tui +version: "1.0.0" +purity: impure +signature: "func LoadASCIIArt(staticPath string, colors []lipgloss.Color) string" +description: "Lee un archivo .txt aleatorio de un directorio y lo renderiza con degradado de colores. Util para headers de TUIs con ASCII art variado." +tags: [tui, ascii, art, header, gradient, random, file] +uses_functions: [apply_gradient_go_tui] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [bufio, math/rand, os, path/filepath, strings, time, github.com/charmbracelet/lipgloss] +params: + - name: staticPath + desc: "directorio con archivos .txt de ASCII art" + - name: colors + desc: "paleta de colores para el degradado; nil usa DefaultGradientColors" +output: "string con ASCII art coloreado, o vacio si no hay archivos o hay error" +tested: false +tests: [] +test_file_path: "" +file_path: "functions/tui/load_ascii_art.go" +source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git" +source_license: "MIT" +source_file: "launcher/middleware/assets.go" +--- + +## Ejemplo + +```go +// Cada app pone sus archivos .txt en un directorio static/ +header := tui.LoadASCIIArt("./static", nil) +if header != "" { + fmt.Print(header) +} + +// Con paleta custom +palette := []lipgloss.Color{ + lipgloss.Color("#ff6b6b"), + lipgloss.Color("#feca57"), + lipgloss.Color("#48dbfb"), +} +header = tui.LoadASCIIArt("./static", palette) +``` + +## Notas + +Patron de uso: cada app TUI mantiene un directorio `static/` con archivos `.txt` de ASCII art. Al iniciar, `LoadASCIIArt` elige uno al azar y le aplica el degradado de colores. Esto da variedad visual en cada ejecucion. + +Fallback silencioso: si el directorio no existe, no tiene .txt, o hay error de lectura, retorna "" sin error. El caller decide si mostrar un fallback. diff --git a/functions/tui/normalize_terminal_output.go b/functions/tui/normalize_terminal_output.go new file mode 100644 index 00000000..2a432073 --- /dev/null +++ b/functions/tui/normalize_terminal_output.go @@ -0,0 +1,28 @@ +package tui + +import "strings" + +// NormalizeTerminalOutput strips ANSI codes, normalizes line endings, +// and removes non-printable control characters from terminal output. +func NormalizeTerminalOutput(s string) string { + if s == "" { + return "" + } + + clean := StripANSI(s) + clean = strings.ReplaceAll(clean, "\r\n", "\n") + clean = strings.ReplaceAll(clean, "\r", "\n") + + var b strings.Builder + b.Grow(len(clean)) + for _, r := range clean { + switch { + case r == '\n' || r == '\t': + b.WriteRune(r) + case r >= 32 && r != 127: + b.WriteRune(r) + } + } + + return b.String() +} diff --git a/functions/tui/normalize_terminal_output.md b/functions/tui/normalize_terminal_output.md new file mode 100644 index 00000000..56b40afd --- /dev/null +++ b/functions/tui/normalize_terminal_output.md @@ -0,0 +1,40 @@ +--- +name: normalize_terminal_output +kind: function +lang: go +domain: tui +version: "1.0.0" +purity: pure +signature: "func NormalizeTerminalOutput(s string) string" +description: "Limpia output de terminal: remueve codigos ANSI, normaliza saltos de linea (CRLF/CR a LF) y elimina caracteres de control no imprimibles. Preserva tabs y newlines." +tags: [tui, terminal, normalize, clean, output, ansi] +uses_functions: [strip_ansi_go_tui] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [strings] +params: + - name: s + desc: "output de terminal con posibles codigos ANSI y caracteres de control" +output: "string limpio con solo caracteres imprimibles, tabs y newlines" +tested: false +tests: [] +test_file_path: "" +file_path: "functions/tui/normalize_terminal_output.go" +source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git" +source_license: "MIT" +source_file: "launcher/core/commands.go" +--- + +## Ejemplo + +```go +raw := "\033[32mOK\033[0m\r\nLinea 2\x00oculto" +clean := tui.NormalizeTerminalOutput(raw) +// clean == "OK\nLinea 2oculto" +``` + +## Notas + +Preserva `\n` y `\t` como caracteres validos. Filtra todo caracter con valor < 32 (excepto tab y newline) y DEL (127). Util para capturar output de subprocesos y mostrarlo en TUIs donde los codigos ANSI rompen el render. diff --git a/functions/tui/read_dir_autocomplete.go b/functions/tui/read_dir_autocomplete.go new file mode 100644 index 00000000..1da528ea --- /dev/null +++ b/functions/tui/read_dir_autocomplete.go @@ -0,0 +1,32 @@ +package tui + +import ( + "os" + "sort" + "strings" +) + +// AutocompleteCandidate represents a directory entry for path autocomplete. +type AutocompleteCandidate struct { + Name string + IsDir bool +} + +// ReadDirAutocomplete reads entries from searchDir whose name starts with prefix +// (case-insensitive). Returns candidates sorted by name. +func ReadDirAutocomplete(searchDir, prefix string) ([]AutocompleteCandidate, error) { + entries, err := os.ReadDir(searchDir) + if err != nil { + return nil, err + } + prefixLower := strings.ToLower(prefix) + var results []AutocompleteCandidate + for _, e := range entries { + name := e.Name() + if prefix == "" || strings.HasPrefix(strings.ToLower(name), prefixLower) { + results = append(results, AutocompleteCandidate{Name: name, IsDir: e.IsDir()}) + } + } + sort.Slice(results, func(i, j int) bool { return results[i].Name < results[j].Name }) + return results, nil +} diff --git a/functions/tui/read_dir_autocomplete.md b/functions/tui/read_dir_autocomplete.md new file mode 100644 index 00000000..8ab124d1 --- /dev/null +++ b/functions/tui/read_dir_autocomplete.md @@ -0,0 +1,44 @@ +--- +name: read_dir_autocomplete +kind: function +lang: go +domain: tui +version: "1.0.0" +purity: impure +signature: "func ReadDirAutocomplete(searchDir, prefix string) ([]AutocompleteCandidate, error)" +description: "Lee entradas de un directorio filtradas por prefijo (case-insensitive). Retorna candidatos ordenados por nombre, utiles para autocompletado de rutas en TUIs." +tags: [tui, autocomplete, directory, path, filter, completion] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [os, sort, strings] +params: + - name: searchDir + desc: "directorio donde buscar entradas" + - name: prefix + desc: "prefijo para filtrar nombres (case-insensitive); vacio retorna todas" +output: "slice de AutocompleteCandidate con Name y IsDir, ordenados alfabeticamente" +tested: false +tests: [] +test_file_path: "" +file_path: "functions/tui/read_dir_autocomplete.go" +source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git" +source_license: "MIT" +source_file: "launcher/middleware/command_fs.go" +--- + +## Ejemplo + +```go +candidates, err := tui.ReadDirAutocomplete("/home/user", "doc") +// candidates podria ser: [{Name:"Documents" IsDir:true}, {Name:"docker-compose.yml" IsDir:false}] + +// Sin prefijo retorna todas las entradas +all, _ := tui.ReadDirAutocomplete("/tmp", "") +``` + +## Notas + +El struct `AutocompleteCandidate` se define en el mismo archivo. El campo `IsDir` permite al caller diferenciar directorios de archivos para renderizar iconos o sufijos distintos. diff --git a/functions/tui/strip_ansi.go b/functions/tui/strip_ansi.go new file mode 100644 index 00000000..c70cfcf8 --- /dev/null +++ b/functions/tui/strip_ansi.go @@ -0,0 +1,11 @@ +package tui + +import "regexp" + +// ansiEscape matches ANSI terminal escape sequences. +var ansiEscape = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`) + +// StripANSI removes ANSI terminal escape sequences from s. +func StripANSI(s string) string { + return ansiEscape.ReplaceAllString(s, "") +} diff --git a/functions/tui/strip_ansi.md b/functions/tui/strip_ansi.md new file mode 100644 index 00000000..40e51969 --- /dev/null +++ b/functions/tui/strip_ansi.md @@ -0,0 +1,40 @@ +--- +name: strip_ansi +kind: function +lang: go +domain: tui +version: "1.0.0" +purity: pure +signature: "func StripANSI(s string) string" +description: "Remueve secuencias de escape ANSI de un string. Util para limpiar output de terminal antes de procesarlo o mostrarlo en contextos sin soporte ANSI." +tags: [tui, ansi, strip, terminal, escape, clean] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [regexp] +params: + - name: s + desc: "string con posibles secuencias de escape ANSI" +output: "string sin secuencias ANSI" +tested: false +tests: [] +test_file_path: "" +file_path: "functions/tui/strip_ansi.go" +source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git" +source_license: "MIT" +source_file: "launcher/core/commands.go" +--- + +## Ejemplo + +```go +raw := "\033[31mError:\033[0m archivo no encontrado" +clean := tui.StripANSI(raw) +// clean == "Error: archivo no encontrado" +``` + +## Notas + +Usa regex `\x1b\[[0-9;]*[a-zA-Z]` que cubre los codigos CSI estandar (colores, cursor, etc.). No cubre secuencias OSC ni DCS menos comunes.