From a78c42a161a65881927c4655e1e71c36b1f40ccb Mon Sep 17 00:00:00 2001 From: Celestino Amoroso Date: Sat, 24 Feb 2024 11:37:44 +0100 Subject: [PATCH] New module tty.go --- tty.go | 306 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 tty.go diff --git a/tty.go b/tty.go new file mode 100644 index 0000000..12bbc6c --- /dev/null +++ b/tty.go @@ -0,0 +1,306 @@ +// tty.go +package text + +import ( + "fmt" + "io" + "os" + "strconv" + "strings" +) + +type ctxFmt struct { + isTTY bool + ansiLen int +} + +func translateColor(name string) (c int) { + //fmt.Println("TranslateColor:", name) + if len(name) == 0 { + return -1 + } + + norm := strings.ToLower(name) + switch { + case strings.HasPrefix("black", norm): + c = 30 + case strings.HasPrefix("blue", norm): + c = 34 + case strings.HasPrefix("green", norm): + c = 32 + case strings.HasPrefix("red", norm): + c = 31 + case strings.HasPrefix("white", norm): + c = 37 + default: + c = -1 + } + if c > 0 && name[0] >= 'A' && name[0] <= 'Z' { + c += 10 + } + return +} + +func extractArgs(spec string) (result string) { + if len(spec) < 2 || spec[0] != '(' || spec[len(spec)-1] != ')' { + return + } + + result = spec[1 : len(spec)-1] + return +} + +func parseColor(fg bool, spec string) (result string) { + var intro string + if spec = extractArgs(spec); len(spec) == 0 { + return + } + + if spec[0] >= '0' && spec[0] <= '9' { + if fg { + intro = "38;" + } else { + intro = "48;" + } + parts := strings.SplitN(spec, ",", 3) + if len(parts) == 3 { + result = intro + "2;" + strings.Join(parts, ";") + } else if len(parts) == 1 { + result = intro + "5;" + parts[0] + } + return + } + + if c := translateColor(spec); c >= 0 { + result = strconv.Itoa(c) + } + return +} + +func advanceToTerms(s string, start int, terms string) (offset int) { + for offset = start; offset < len(s) && strings.IndexByte(terms, s[offset]) < 0; offset++ { + } + return +} + +func (ctx *ctxFmt) putAnsiString(sb *strings.Builder, code string) { + if ctx.isTTY { + a, _ := sb.WriteString("\x1b[") + b, _ := sb.WriteString(code) + sb.WriteByte('m') + ctx.ansiLen += a + b + 1 + } +} + +func (ctx *ctxFmt) putAnsiCode(sb *strings.Builder, code int) { + if ctx.isTTY { + a, _ := sb.WriteString("\x1b[") + b, _ := sb.WriteString(strconv.Itoa(code)) + sb.WriteByte('m') + ctx.ansiLen += a + b + 1 + } +} + +func (ctx *ctxFmt) putAnsiReset(sb *strings.Builder) { + if ctx.isTTY { + a, _ := sb.WriteString("\x1b[0m") + ctx.ansiLen += a + } +} + +type caseMode uint8 + +const ( + normal = caseMode(iota) + upper + lower + title + firstOnly +) + +type alignMode uint8 + +const ( + noAlign = alignMode(iota) + alignLeft + alignRight +) + +func (ctx *ctxFmt) putText(sb *strings.Builder, text string, start int, mode caseMode, align alignMode, fieldSize int, filler byte) (offset int) { + if t := text[start+1:]; len(t) > 0 { + if align == alignRight { + for k := 0; k < fieldSize-len(t); k++ { + sb.WriteByte(filler) + } + } + if mode == upper { + sb.WriteString(strings.ToUpper(t)) + } else if mode == lower { + sb.WriteString(strings.ToLower(t)) + } else if mode == title { + sb.WriteString(strings.Title(t)) + } else if mode == firstOnly { + if t[0] >= 'a' && t[0] <= 'z' { + sb.WriteByte(t[0] - 'a' + 'A') + } else { + sb.WriteByte(t[0]) + } + sb.WriteString(strings.ToLower(t[1:])) + } else { + sb.WriteString(t) + } + if align == alignLeft { + for k := 0; k < fieldSize-len(t); k++ { + sb.WriteByte(filler) + } + } + offset = start + len(t) + } + return +} + +func (ctx *ctxFmt) Handle(varSpec string, flags ScannerFlag) (value string, err error) { + var sb strings.Builder + var mode caseMode = normal + var align alignMode = noAlign + var fieldSize int + var filler byte = ' ' + + //fmt.Println("TTY:", varSpec) + if len(varSpec) == 0 { + ctx.putAnsiReset(&sb) + } else { + for i := 0; i < len(varSpec); i++ { + if j := advanceToTerms(varSpec, i, ";-"); j > i+1 { + if c := translateColor(varSpec[i:j]); c >= 0 { + ctx.putAnsiCode(&sb, c) + i = j + continue + } + } + + ch := varSpec[i] + switch ch { + case '.': + ctx.putAnsiReset(&sb) + case 'b': + ctx.putAnsiString(&sb, "1") + case 'B': + ctx.putAnsiString(&sb, "22") + case 'i': + ctx.putAnsiString(&sb, "3") + case 'I': + ctx.putAnsiString(&sb, "23") + case 'u': + ctx.putAnsiString(&sb, "4") + case 'U': + ctx.putAnsiString(&sb, "24") + case 'c', 'C': + j := advanceToTerms(varSpec, i, ")") + if c := parseColor(ch == 'c', varSpec[i+1:j+1]); len(c) > 0 { + ctx.putAnsiString(&sb, c) + //sb.WriteString("\x1b[" + c + "m") + i = j + } + case '\'': + mode = upper + case ',': + mode = lower + case '"': + mode = title + case '^': + mode = firstOnly + case '<', '>': + j := advanceToTerms(varSpec, i, ")") + if arg := extractArgs(varSpec[i+1 : j+1]); len(arg) > 0 { + var err1 error + if arg[0] <= '0' || arg[0] > '9' { + filler = arg[0] + arg = arg[1:] + } + if fieldSize, err1 = strconv.Atoi(arg); err1 == nil { + if ch == '<' { + align = alignLeft + } else { + align = alignRight + } + } + } + i = j + case '-': + i = ctx.putText(&sb, varSpec, i, mode, align, fieldSize, filler) + ctx.putAnsiReset(&sb) + } + } + } + return sb.String(), nil +} + +func IsTty(w io.Writer) (isTty bool) { + if fh, ok := w.(*os.File); ok { + if fi, err := fh.Stat(); err == nil { + isTty = (fi.Mode() & os.ModeCharDevice) != 0 + } + } + return +} + +func ToTty(source string) (result string, cleanLen int, err error) { + ctx := ctxFmt{true, 0} + if result, err = ScanExt('#', &ctx, source); err == nil { + cleanLen = len(result) - ctx.ansiLen + } + return +} + +func ToTtyStream(w io.Writer, source string) (result string, cleanLen int, err error) { + if IsTty(w) { + return ToTty(source) + } else { + return source, len(source), nil + } +} + +func Sprintf(templ string, args ...any) string { + templ, _, _ = ToTty(templ) + return fmt.Sprintf(templ, args...) +} + +func Printf(templ string, args ...any) (int, error) { + return Fprintf(os.Stdout, templ, args...) +} + +func Fprintf(w io.Writer, templ string, args ...any) (int, error) { + templ, _, _ = ToTtyStream(w, templ) + return fmt.Fprintf(w, templ, args...) + +} + +// func DebugSprintf() { +// list := []string{ +// "#{<(.20)-Param}: #{>(06)-123}", +// "ciao #{c(r)i<(12)-GIAN Carlo}. #{ub-Come} #{c(G)b-Stai}?", +// "ciao #{c(r)i>(12)-GIAN Carlo}. #{ub-Come} #{c(G)b-Stai}?", +// "ciao #{c(red);i}Mario#. #{u;b}Come# #{GREEN;b}Stai#{.}?", +// "ciao #{c(red)i-Mario}. #{ub-Come} #{GREEN;b-Stai}?", +// "ciao #{c(r)i-Mario}. #{ub-Come} #{Gr;b-Stai}?", +// "ciao #{c(r)i-Mario}. #{ub-Come} #{c(G)b-Stai}?", +// "ciao #{c(r)i'-gian carlo}. #{ub-Come} #{c(G)b-Stai}?", +// "ciao #{c(r)i\"-gian carlo}. #{ub-Come} #{c(G)b-Stai}?", +// "ciao #{c(r)i,-GIAN Carlo}. #{ub-Come} #{c(G)b-Stai}?", +// "ciao #{c(r)i^-GIAN Carlo}. #{ub-Come} #{c(G)b-Stai}?", +// } + +// fmt.Println("--- Sprintf() ---") +// for i, s := range list { +// x := Sprintf(s) +// fmt.Printf("--- Test nr %d: %q\n", i+1, s) +// fmt.Println(x) +// } +// fmt.Println("\n--- Sprintf() ---") +// for i, s := range list { +// fmt.Printf("--- Test nr %d: %q\n", i+1, s) +// Printf(s) +// fmt.Println() +// } +// }