feat: add Go TUI rendering and terminal helper functions
7 funciones Go del dominio tui: apply_gradient (gradiente de color en texto), draw_box y draw_separator (renderizado de cajas y separadores con box_chars), load_ascii_art (carga de arte ASCII desde archivos), normalize_terminal_output y strip_ansi (limpieza de salida de terminal), read_dir_autocomplete (autocompletado de rutas de directorio). Incluye box_chars.go como helper de caracteres Unicode para bordes.
This commit is contained in:
@@ -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()
|
||||
}
|
||||
@@ -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.
|
||||
@@ -0,0 +1,14 @@
|
||||
package tui
|
||||
|
||||
// Box drawing characters for TUI borders and separators.
|
||||
const (
|
||||
BoxTL = "╔"
|
||||
BoxTR = "╗"
|
||||
BoxBL = "╚"
|
||||
BoxBR = "╝"
|
||||
BoxH = "═"
|
||||
BoxV = "║"
|
||||
BoxML = "╠"
|
||||
BoxMR = "╣"
|
||||
BoxSep = "─"
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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)),
|
||||
)
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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, "")
|
||||
}
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user