From bf1a9a786bc2195be077706d0d38a5f66090ddc9 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Wed, 27 Nov 2024 15:10:09 +0300 Subject: [PATCH 1/9] [fmtutil/table] Improve support of data with ANSI escape sequences --- CHANGELOG.md | 4 ++++ fmtutil/table/table.go | 35 ++++++++++++++++++----------------- version.go | 2 +- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46b06824..7a364bb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Changelog +### [13.13.2](https://kaos.sh/ek/13.13.2) + +- `[fmtutil/table]` Improved support of data with ANSI escape sequences + ### [13.13.1](https://kaos.sh/ek/13.13.1) - `[spellcheck]` Distance calculation method now public diff --git a/fmtutil/table/table.go b/fmtutil/table/table.go index 18f2db19..f9473444 100644 --- a/fmtutil/table/table.go +++ b/fmtutil/table/table.go @@ -12,6 +12,7 @@ import ( "fmt" "strings" + "github.com/essentialkaos/ek/v13/ansi" "github.com/essentialkaos/ek/v13/fmtc" "github.com/essentialkaos/ek/v13/mathutil" "github.com/essentialkaos/ek/v13/strutil" @@ -373,7 +374,9 @@ func renderRowData(t *Table, data []string, totalColumns int) { break } - if strutil.LenVisual(fmtc.Clean(columnData)) > t.columnSizes[columnIndex] { + dataLen := getDataLen(columnData) + + if dataLen > t.columnSizes[columnIndex] { fmtc.Print(" " + strutil.Ellipsis(columnData, t.columnSizes[columnIndex]) + " ") } else { if columnIndex+1 == totalColumns && getAlignment(t, columnIndex) == ALIGN_LEFT { @@ -443,10 +446,10 @@ func calculateColumnSizes(t *Table) { continue } - itemSizes := strutil.LenVisual(fmtc.Clean(item)) + itemLen := getDataLen(item) - if itemSizes > t.columnSizes[index] { - t.columnSizes[index] = itemSizes + if itemLen > t.columnSizes[index] { + t.columnSizes[index] = itemLen } } } @@ -491,7 +494,6 @@ func setColumnsSizes(t *Table, columns int) { if index+1 == columns { if totalSize+(columns*3) < tableWidth { - fmt.Println(1001) t.columnSizes[index]++ } @@ -527,29 +529,23 @@ func getColumnsNum(t *Table) int { // formatText align text with color tags func formatText(data string, size int, align uint8) string { - var dataSize int - - if strings.Contains(data, "{") { - dataSize = strutil.LenVisual(fmtc.Clean(data)) - } else { - dataSize = strutil.LenVisual(data) - } + dataLen := getDataLen(data) - if dataSize >= size { + if dataLen >= size { return data } switch align { case ALIGN_RIGHT: - return strings.Repeat(" ", size-dataSize) + data + return strings.Repeat(" ", size-dataLen) + data case ALIGN_CENTER: - prefixSize := (size - dataSize) / 2 - suffixSize := size - (prefixSize + dataSize) + prefixSize := (size - dataLen) / 2 + suffixSize := size - (prefixSize + dataLen) return strings.Repeat(" ", prefixSize) + data + strings.Repeat(" ", suffixSize) } - return data + strings.Repeat(" ", size-dataSize) + return data + strings.Repeat(" ", size-dataLen) } // getAlignment return align for given column @@ -592,3 +588,8 @@ func getTableWidth(t *Table) int { return 0 } + +// getDataLen returns len of data excludinf ANSI escape codes and fmtc tags +func getDataLen(data string) int { + return strutil.LenVisual(ansi.RemoveCodes(fmtc.Clean(data))) +} diff --git a/version.go b/version.go index 242ee83f..7971c874 100644 --- a/version.go +++ b/version.go @@ -8,4 +8,4 @@ package ek // ////////////////////////////////////////////////////////////////////////////////// // // VERSION is current ek package version -const VERSION = "13.13.1" +const VERSION = "13.13.2" From 0308f2daf9d27280354e75f6cab181b90ae70d20 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Fri, 29 Nov 2024 12:15:14 +0300 Subject: [PATCH 2/9] [fmtc] Added methods 'Printfn', 'Fprintfn', 'Sprintfn', and 'LPrintfn' --- CHANGELOG.md | 3 ++- fmtc/cond_wrapper.go | 41 +++++++++++++++++++++++++++++++++++++ fmtc/fmtc.go | 26 ++++++++++++++++++++++++ fmtc/fmtc_test.go | 48 ++++++++++++++++++++++++++------------------ 4 files changed, 97 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a364bb8..b7cf03e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ ## Changelog -### [13.13.2](https://kaos.sh/ek/13.13.2) +### [13.14.0](https://kaos.sh/ek/13.14.0) +- `[fmtc]` Added methods `Printfn`, `Fprintfn`, `Sprintfn`, and `LPrintfn` - `[fmtutil/table]` Improved support of data with ANSI escape sequences ### [13.13.1](https://kaos.sh/ek/13.13.1) diff --git a/fmtc/cond_wrapper.go b/fmtc/cond_wrapper.go index cd167888..1f9bd83e 100644 --- a/fmtc/cond_wrapper.go +++ b/fmtc/cond_wrapper.go @@ -53,6 +53,17 @@ func (cw CondWrapper) Printf(f string, a ...any) (int, error) { return Printf(f, a...) } +// Printfn formats according to a format specifier and writes to standard output with +// the new line at the end. It returns the number of bytes written and any write +// error encountered. +func (cw CondWrapper) Printfn(f string, a ...any) (int, error) { + if !cw { + return 0, nil + } + + return Printfn(f, a...) +} + // Fprint formats using the default formats for its operands and writes to w. // Spaces are added between operands when neither is a string. It returns the // number of bytes written and any write error encountered. @@ -85,6 +96,16 @@ func (cw CondWrapper) Fprintf(w io.Writer, f string, a ...any) (int, error) { return Fprintf(w, f, a...) } +// Fprintfn formats according to a format specifier and writes to w with the newline +// at the end. It returns the number of bytes written and any write error encountered. +func (cw CondWrapper) Fprintfn(w io.Writer, f string, a ...any) (int, error) { + if !cw { + return 0, nil + } + + return Fprintfn(w, f, a...) +} + // Sprint formats using the default formats for its operands and returns the // resulting string. Spaces are added between operands when neither is a string. func (cw CondWrapper) Sprint(a ...any) string { @@ -105,6 +126,16 @@ func (cw CondWrapper) Sprintf(f string, a ...any) string { return Sprintf(f, a...) } +// Sprintfn formats according to a format specifier and returns the resulting +// string with the newline at the end. +func (cw CondWrapper) Sprintfn(f string, a ...any) string { + if !cw { + return "" + } + + return Sprintfn(f, a...) +} + // Sprintln formats using the default formats for its operands and returns the // resulting string. Spaces are always added between operands and a newline is // appended. @@ -164,6 +195,16 @@ func (cw CondWrapper) LPrintf(maxSize int, f string, a ...any) (int, error) { return LPrintf(maxSize, f, a...) } +// LPrintfn formats according to a format specifier and writes to standard output +// limited by the text size and with the newline at the end +func (cw CondWrapper) LPrintfn(maxSize int, f string, a ...any) (int, error) { + if !cw { + return 0, nil + } + + return LPrintfn(maxSize, f, a...) +} + // LPrintln formats using the default formats for its operands and writes to standard // output limited by the text size func (cw CondWrapper) LPrintln(maxSize int, a ...any) (int, error) { diff --git a/fmtc/fmtc.go b/fmtc/fmtc.go index 5eee0474..4929b167 100644 --- a/fmtc/fmtc.go +++ b/fmtc/fmtc.go @@ -192,6 +192,13 @@ func Printf(f string, a ...any) (int, error) { return fmt.Printf(searchColors(f, -1, DisableColors, true), a...) } +// Printfn formats according to a format specifier and writes to standard output with +// the new line at the end. It returns the number of bytes written and any write +// error encountered. +func Printfn(f string, a ...any) (int, error) { + return fmt.Printf(searchColors(f, -1, DisableColors, true)+"\n", a...) +} + // Fprint formats using the default formats for its operands and writes to w. // Spaces are added between operands when neither is a string. It returns the // number of bytes written and any write error encountered. @@ -214,6 +221,12 @@ func Fprintf(w io.Writer, f string, a ...any) (int, error) { return fmt.Fprintf(w, searchColors(f, -1, DisableColors, true), a...) } +// Fprintfn formats according to a format specifier and writes to w with the newline +// at the end. It returns the number of bytes written and any write error encountered. +func Fprintfn(w io.Writer, f string, a ...any) (int, error) { + return fmt.Fprintf(w, searchColors(f, -1, DisableColors, true)+"\n", a...) +} + // Sprint formats using the default formats for its operands and returns the // resulting string. Spaces are added between operands when neither is a string. func Sprint(a ...any) string { @@ -227,6 +240,12 @@ func Sprintf(f string, a ...any) string { return fmt.Sprintf(searchColors(f, -1, DisableColors, true), a...) } +// Sprintfn formats according to a format specifier and returns the resulting +// string with the newline at the end. +func Sprintfn(f string, a ...any) string { + return fmt.Sprintf(searchColors(f, -1, DisableColors, true)+"\n", a...) +} + // Sprintln formats using the default formats for its operands and returns the // resulting string. Spaces are always added between operands and a newline is // appended. @@ -274,6 +293,13 @@ func LPrintf(maxSize int, f string, a ...any) (int, error) { return fmt.Print(searchColors(s, maxSize, DisableColors, true)) } +// LPrintfn formats according to a format specifier and writes to standard output +// limited by the text size and with the newline at the end +func LPrintfn(maxSize int, f string, a ...any) (int, error) { + s := fmt.Sprintf(f, a...) + return fmt.Print(searchColors(s, maxSize, DisableColors, true) + "\n") +} + // LPrintln formats using the default formats for its operands and writes to standard // output limited by the text size func LPrintln(maxSize int, a ...any) (int, error) { diff --git a/fmtc/fmtc_test.go b/fmtc/fmtc_test.go index 3659b09b..d5f302f8 100644 --- a/fmtc/fmtc_test.go +++ b/fmtc/fmtc_test.go @@ -343,42 +343,50 @@ func (s *FormatSuite) TestAux(c *C) { func (s *FormatSuite) TestIfHelper(c *C) { w := bytes.NewBufferString("") - If(false).Print("Print: OK\n") - If(false).Println("Println: OK") - If(false).Printf("Printf: %s\n", "OK") - If(false).Fprint(w, "Fprint\n") - If(false).Fprintln(w, "Fprintln") - If(false).Fprintf(w, "Fprintf: %s\n", "OK") - If(false).Sprint("Sprint: OK\n") - If(false).Sprintln("Sprintln: OK") - If(false).Sprintf("Sprintf: %s\n", "OK") - If(false).TPrint("TPrint: OK\n") - If(false).TPrintln("TPrintln: OK") - If(false).TPrintf("TPrintf: %s\n", "OK") - If(false).LPrint(100, "LPrint: OK\n") - If(false).LPrintln(100, "LPrintln: OK") - If(false).LPrintf(100, "LPrintf: %s\n", "OK") - If(false).TLPrint(100, "TLPrint: OK\n") - If(false).TLPrintln(100, "TLPrintln: OK") - If(false).TLPrintf(100, "TLPrintf: %s\n", "OK") + If(false).Print("Print: NOT OK\n") + If(false).Println("Println: NOT OK") + If(false).Printf("Printf: %s\n", "NOT OK") + If(false).Printfn("Printfn: %s\n", "NOT OK") + If(false).Fprint(w, "Fprint: NOT OK\n") + If(false).Fprintln(w, "Fprintln: NOT OK") + If(false).Fprintf(w, "Fprintf: %s\n", "NOT OK") + If(false).Fprintfn(w, "Fprintfn: %s", "NOT OK") + If(false).Sprint("Sprint: NOT OK\n") + If(false).Sprintln("Sprintln: NOT OK") + If(false).Sprintf("Sprintf: %s\n", "NOT OK") + If(false).Sprintfn("Sprintfn: %s", "NOT OK") + If(false).TPrint("TPrint: NOT OK\n") + If(false).TPrintln("TPrintln: NOT OK") + If(false).TPrintf("TPrintf: %s\n", "NOT OK") + If(false).LPrint(100, "LPrint: NOT OK\n") + If(false).LPrintln(100, "LPrintln: NOT OK") + If(false).LPrintf(100, "LPrintf: %s\n", "NOT OK") + If(false).LPrintfn(100, "LPrintfn: %s", "NOT OK") + If(false).TLPrint(100, "TLPrint: NOT OK\n") + If(false).TLPrintln(100, "TLPrintln: NOT OK") + If(false).TLPrintf(100, "TLPrintf: %s\n", "NOT OK") If(false).NewLine() If(false).Bell() If(true).Print("Print: OK\n") If(true).Println("Println: OK") If(true).Printf("Printf: %s\n", "OK") - If(true).Fprint(w, "Fprint\n") - If(true).Fprintln(w, "Fprintln") + If(true).Printfn("Printfn: %s\n", "OK") + If(true).Fprint(w, "Fprint: OK\n") + If(true).Fprintln(w, "Fprintln: OK") If(true).Fprintf(w, "Fprintf: %s\n", "OK") + If(true).Fprintfn(w, "Fprintfn: %s", "OK") If(true).Sprint("Sprint: OK\n") If(true).Sprintln("Sprintln: OK") If(true).Sprintf("Sprintf: %s\n", "OK") + If(true).Sprintfn("Sprintf: %s", "OK") If(true).TPrint("TPrint: OK\n") If(true).TPrintln("TPrintln: OK") If(true).TPrintf("TPrintf: %s\n", "OK") If(true).LPrint(100, "LPrint: OK\n") If(true).LPrintln(100, "LPrintln: OK") If(true).LPrintf(100, "LPrintf: %s\n", "OK") + If(true).LPrintfn(100, "LPrintfn: %s", "OK") If(true).TLPrint(100, "TLPrint: OK\n") If(true).TLPrintln(100, "TLPrintln: OK") If(true).TLPrintf(100, "TLPrintf: %s\n", "OK") From 45e386061bf1106507438980f54d39bb235bade8 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Fri, 29 Nov 2024 12:21:27 +0300 Subject: [PATCH 3/9] [fmtc] Add more usage examples --- fmtc/examples_test.go | 81 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/fmtc/examples_test.go b/fmtc/examples_test.go index 294df54d..2c5613ca 100644 --- a/fmtc/examples_test.go +++ b/fmtc/examples_test.go @@ -162,6 +162,70 @@ func ExamplePrintf() { Printf("{%6a5acd}%s{!}\n", "slateblue background") } +func ExamplePrintfn() { + // print colored text + // {!} is tag for style reset + Printfn("{d}%s{!}", "black") + Printfn("{r}%s{!}", "red") + Printfn("{y}%s{!}", "yellow") + Printfn("{b}%s{!}", "blue") + Printfn("{c}%s{!}", "cyan") + Printfn("{m}%s{!}", "magenta") + Printfn("{g}%s{!}", "green") + Printfn("{s}%s{!}", "light grey") + + // use text modificators + + // light colors + Printfn("{r-}%s{!}", "light red") + Printfn("{r-}%s{!}", "dark grey") + + // bold + color + Printfn("{r*}%s{!}", "red") + Printfn("{g*}%s{!}", "green") + + // dim + Printfn("{r^}%s{!}", "red") + Printfn("{g^}%s{!}", "green") + + // underline + Printfn("{r_}%s{!}", "red") + Printfn("{g_}%s{!}", "green") + + // blink + Printfn("{r~}%s{!}", "red") + Printfn("{g~}%s{!}", "green") + + // reverse + Printfn("{r@}%s{!}", "red") + Printfn("{g@}%s{!}", "green") + + // background color + Printfn("{D}%s{!}", "black") + Printfn("{R}%s{!}", "red") + Printfn("{Y}%s{!}", "yellow") + Printfn("{B}%s{!}", "blue") + Printfn("{C}%s{!}", "cyan") + Printfn("{M}%s{!}", "magenta") + Printfn("{G}%s{!}", "green") + Printfn("{S}%s{!}", "light grey") + + // many tags at once + // underline, cyan text with the red background + Printfn("{cR_}%s{!}", "text") + + // many tags in once + Printfn("{r}{*}%s{!}", "red and bold") + + // 256 colors (# for foreground, % for background) + Printfn("{#201}%s{!}", "pink text") + Printfn("{%201}%s{!}", "pink background") + + // 24-bit colors (# for foreground, % for background) + Printfn("{#7cfc00}%s{!}", "lawngreen text") + Printfn("{%6a5acd}%s{!}", "slateblue background") +} + func ExamplePrintln() { // print colored text // {!} is tag for style reset @@ -239,6 +303,11 @@ func ExampleFprintf() { Fprintf(os.Stdout, "{g}%s{!}\n", "This is normal message") } +func ExampleFprintfn() { + Fprintfn(os.Stderr, "{r}%s{!}", "This is error message") + Fprintfn(os.Stdout, "{g}%s{!}", "This is normal message") +} + func ExampleFprintln() { Fprintln(os.Stderr, "{r}This is error message{!}") Fprintln(os.Stdout, "{g}This is normal message{!}") @@ -254,6 +323,11 @@ func ExampleSprintf() { fmt.Print(msg) } +func ExampleSprintfn() { + msg := Sprintfn("{r}%s{!}", "This is error message") + fmt.Print(msg) +} + func ExampleSprintln() { msg := Sprintln("{r}This is error message{!}") fmt.Print(msg) @@ -289,7 +363,12 @@ func ExampleLPrint() { func ExampleLPrintf() { // Only "This is text" will be shown - LPrintf(12, "{r}This is %s {g} with colors{!}", "text") + LPrintf(12, "{r}This is %s {g} with colors{!}\n", "text") +} + +func ExampleLPrintfn() { + // Only "This is text" will be shown + LPrintfn(12, "{r}This is %s {g} with colors{!}", "text") } func ExampleLPrintln() { From a66658e61fd97bca9286553348ff7c7cafde3402 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Fri, 29 Nov 2024 12:25:29 +0300 Subject: [PATCH 4/9] Version bump --- version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.go b/version.go index 7971c874..e469bc5b 100644 --- a/version.go +++ b/version.go @@ -8,4 +8,4 @@ package ek // ////////////////////////////////////////////////////////////////////////////////// // // VERSION is current ek package version -const VERSION = "13.13.2" +const VERSION = "13.14.0" From bd82ba7abc769017e9abd03507fc27a9b6b2f981 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Fri, 29 Nov 2024 12:40:07 +0300 Subject: [PATCH 5/9] Code refactoring --- fmtutil/panel/panel.go | 2 +- spinner/spinner.go | 2 +- support/support.go | 10 +++++----- usage/usage.go | 20 ++++++++++---------- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/fmtutil/panel/panel.go b/fmtutil/panel/panel.go index 7b2d7208..fc886948 100644 --- a/fmtutil/panel/panel.go +++ b/fmtutil/panel/panel.go @@ -157,7 +157,7 @@ func renderPanel(label, colorTag, title, message string, options Options) { } if lineSize > 0 { - fmtc.Printf(colorTag+" %s{!}\n", strings.Repeat("─", lineSize)) + fmtc.Printfn(colorTag+" %s{!}", strings.Repeat("─", lineSize)) } else { fmtc.NewLine() } diff --git a/spinner/spinner.go b/spinner/spinner.go index 86faff33..2dc41bd6 100644 --- a/spinner/spinner.go +++ b/spinner/spinner.go @@ -195,7 +195,7 @@ func stopSpinner(action uint8) { } fmtc.Print(desc + " ") - fmtc.Printf(timeColorTag+"(%s){!}\n", timeutil.ShortDuration(time.Since(start), true)) + fmtc.Printfn(timeColorTag+"(%s){!}", timeutil.ShortDuration(time.Since(start), true)) mu.RUnlock() diff --git a/support/support.go b/support/support.go index 72a7b1dc..611e4768 100644 --- a/support/support.go +++ b/support/support.go @@ -737,11 +737,11 @@ func (i *Info) printChecksInfo() { switch c.Status { case CHECK_OK, CHECK_SKIP: - fmtc.Printf(" {s}— {&}%s{!}\n", c.Message) + fmtc.Printfn(" {s}— {&}%s{!}", c.Message) case CHECK_WARN: - fmtc.Printf(" {s}— {y}{&}%s{!}\n", c.Message) + fmtc.Printfn(" {s}— {y}{&}%s{!}", c.Message) case CHECK_ERROR: - fmtc.Printf(" {s}— {r}{&}%s{!}\n", c.Message) + fmtc.Printfn(" {s}— {r}{&}%s{!}", c.Message) } } } @@ -803,9 +803,9 @@ func (i *Info) printDependencies() { for _, dep := range i.Deps { switch dep.Extra { case "": - fmtc.Printf(" {s}%8s{!} %s\n", dep.Version, dep.Path) + fmtc.Printfn(" {s}%8s{!} %s", dep.Version, dep.Path) default: - fmtc.Printf(" {s}%8s{!} %s {s-}(%s){!}\n", dep.Version, dep.Path, dep.Extra) + fmtc.Printfn(" {s}%8s{!} %s {s-}(%s){!}", dep.Version, dep.Path, dep.Extra) } } } diff --git a/usage/usage.go b/usage/usage.go index 642900cf..3ce81111 100644 --- a/usage/usage.go +++ b/usage/usage.go @@ -483,9 +483,9 @@ func (e *Example) Print() { } if e.Raw { - fmtc.Printf(" %s\n", e.Cmd) + fmtc.Printfn(" %s", e.Cmd) } else { - fmtc.Printf(" %s %s\n", appName, e.Cmd) + fmtc.Printfn(" %s %s", appName, e.Cmd) } if e.Desc != "" { @@ -495,7 +495,7 @@ func (e *Example) Print() { e.info.ExampleDescColorTag, "", ), DEFAULT_EXAMPLE_DESC_COLOR_TAG, ) - fmtc.Printf(" "+descColor+"%s{!}\n", wrapText(e.Desc, 2, wrapLen)) + fmtc.Printfn(" "+descColor+"%s\n", wrapText(e.Desc, 2, wrapLen)) } } @@ -544,16 +544,16 @@ func (a *About) Print(infoType ...string) { fmtc.If(a.Release != "").Printf(releaseColor+relSeparator+"%s{!}", a.Release) fmtc.If(a.Build != "").Printf(buildColor+" (%s){!}", a.Build) - fmtc.Printf(" "+descSeparator+" %s\n", a.Desc) + fmtc.Printfn(" "+descSeparator+" %s", a.Desc) if len(a.Environment) > 0 { - fmtc.Printf("{s-}│{!}\n") + fmtc.Printfn("{s-}│{!}") for i, env := range a.Environment { if len(a.Environment) != i+1 { - fmtc.Printf("{s-}├ %s: %s{!}\n", env.Name, strutil.Q(env.Version, "—")) + fmtc.Printfn("{s-}├ %s: %s{!}", env.Name, strutil.Q(env.Version, "—")) } else { - fmtc.Printf("{s-}└ %s: %s{!}\n", env.Name, strutil.Q(env.Version, "—")) + fmtc.Printfn("{s-}└ %s: %s{!}", env.Name, strutil.Q(env.Version, "—")) } } } @@ -572,7 +572,7 @@ func (a *About) Print(infoType ...string) { ) } - fmtc.If(a.License != "").Printf("{s-}%s{!}\n", a.License) + fmtc.If(a.License != "").Printfn("{s-}%s{!}", a.License) if a.UpdateChecker.CheckFunc != nil && a.UpdateChecker.Payload != "" { newVersion, releaseDate, hasUpdate := a.UpdateChecker.CheckFunc( @@ -771,7 +771,7 @@ func getOptionSize(opt *Option) int { // printGroupHeader print category header func printGroupHeader(name string) { - fmtc.Printf("\n{*}%s{!}\n\n", name) + fmtc.Printfn("\n{*}%s{!}\n", name) } // isNewerVersion return true if latest version is greater than current @@ -825,7 +825,7 @@ func printNewVersionInfo(curVersion, newVersion string, releaseDate time.Time) { case 1: fmtc.Println("{s-}(released 1 day ago){!}") default: - fmtc.Printf("{s-}(released %d days ago){!}\n", days) + fmtc.Printfn("{s-}(released %d days ago){!}", days) } } From eae5b6976fdf47f712c8d850dd05817d6f813839 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Fri, 29 Nov 2024 12:46:59 +0300 Subject: [PATCH 6/9] [fmtc] Mark 'NameColor' as deprecated (use method 'AddColor' instead) --- CHANGELOG.md | 1 + fmtc/fmtc.go | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7cf03e9..e4bdb136 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - `[fmtc]` Added methods `Printfn`, `Fprintfn`, `Sprintfn`, and `LPrintfn` - `[fmtutil/table]` Improved support of data with ANSI escape sequences +- `[fmtc]` `NameColor` marked as deprecated (_use method `AddColor` instead_) ### [13.13.1](https://kaos.sh/ek/13.13.1) diff --git a/fmtc/fmtc.go b/fmtc/fmtc.go index 4929b167..5722f15e 100644 --- a/fmtc/fmtc.go +++ b/fmtc/fmtc.go @@ -93,7 +93,14 @@ var colorTermEnvVar = env.Var("COLORTERM") // ////////////////////////////////////////////////////////////////////////////////// // // NameColor defines or redifines named color +// +// Deprecated: Use method AddColor instead func NameColor(name, tag string) error { + return AddColor(name, tag) +} + +// AddColor defines or redifines named color +func AddColor(name, tag string) error { if colorsMap == nil { colorsMap = &sync.Map{} } From 570c92d7817aa239e5b6f1d5d0472497e9422447 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Mon, 2 Dec 2024 16:10:18 +0300 Subject: [PATCH 7/9] [fmtutil/filetree] Add experimental package for printing file tree --- CHANGELOG.md | 1 + fmtutil/filetree/filetree.go | 240 +++++++++++++++++++++++++++++++++++ 2 files changed, 241 insertions(+) create mode 100644 fmtutil/filetree/filetree.go diff --git a/CHANGELOG.md b/CHANGELOG.md index e4bdb136..ae5c4557 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### [13.14.0](https://kaos.sh/ek/13.14.0) +- `[fmtutil/filetree]` Added experimental package for printing file tree - `[fmtc]` Added methods `Printfn`, `Fprintfn`, `Sprintfn`, and `LPrintfn` - `[fmtutil/table]` Improved support of data with ANSI escape sequences - `[fmtc]` `NameColor` marked as deprecated (_use method `AddColor` instead_) diff --git a/fmtutil/filetree/filetree.go b/fmtutil/filetree/filetree.go new file mode 100644 index 00000000..e5134021 --- /dev/null +++ b/fmtutil/filetree/filetree.go @@ -0,0 +1,240 @@ +// Package filetree provides methods for printing file tree +package filetree + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2024 ESSENTIAL KAOS // +// Apache License, Version 2.0 // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + "path" + "strings" + + "github.com/essentialkaos/ek/v13/fmtc" + "github.com/essentialkaos/ek/v13/lscolors" + "github.com/essentialkaos/ek/v13/mathutil" + "github.com/essentialkaos/ek/v13/sortutil" + "github.com/essentialkaos/ek/v13/strutil" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +// Tree is file tree +type Tree struct { + Path string + MaxDepth int + Root *Dir +} + +// Dir contains info about directory +type Dir struct { + Files []string + Dirs DirIndex + level int +} + +// DirIndex is map "name" → dir +type DirIndex map[string]*Dir + +// ////////////////////////////////////////////////////////////////////////////////// // + +var ( + // TreeColorTag is color tag of tree lines + TreeColorTag = "{s-}" + + // DirColorTag is color tag for directory names + DirColorTag = "{*}" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +// FromPaths builds tree from paths slice +func FromPaths(paths []string) *Tree { + if len(paths) == 0 { + return &Tree{} + } + + sortutil.StringsNatural(paths) + + tree := &Tree{ + Path: getRootDir(paths), + Root: &Dir{Dirs: DirIndex{}, level: 0}, + } + + var prevPath string + + for _, p := range paths { + p = strings.TrimPrefix(p, tree.Path) + + if prevPath == "" || strings.HasPrefix(p, prevPath) { + prevPath = p + continue + } + + tree.addFile(prevPath) + + prevPath = p + } + + tree.addFile(prevPath) + + return tree +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// IsEmpty returns true if tree is empty +func (t *Tree) IsEmpty() bool { + return t == nil || t.Root.IsEmpty() +} + +// Render renders tree data +func (t *Tree) Render() { + if t.IsEmpty() { + return + } + + t.Root.render(strutil.Q(t.Path, "/"), true, true) +} + +// IsEmpty returns true if dir is empty +func (d *Dir) IsEmpty() bool { + return d == nil || (len(d.Files) == 0 && len(d.Dirs) == 0) +} + +// IsEmpty returns sorted slice of directory names +func (d DirIndex) Names() []string { + var result []string + + for n, _ := range d { + result = append(result, n) + } + + sortutil.StringsNatural(result) + + return result +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// addFile adds file to the tree +func (t *Tree) addFile(filePath string) { + filePath = strings.TrimLeft(filePath, "/") + + if strings.ReplaceAll(filePath, " ", "") == "" { + return + } + + fileName := path.Base(filePath) + + if !strings.Contains(filePath, "/") { + t.Root.Files = append(t.Root.Files, fileName) + return + } + + dirPath := strings.Split(path.Dir(filePath), "/") + dir := t.Root.createDir(dirPath) + dir.Files = append(dir.Files, fileName) + + t.MaxDepth = mathutil.Max(t.MaxDepth, len(dirPath)) +} + +// createDir creates new directory structure or returns existing one +func (d *Dir) createDir(path []string) *Dir { + cwd := d + + for _, p := range path { + dd, ok := cwd.Dirs[p] + + if !ok { + cwd.Dirs[p] = &Dir{Dirs: DirIndex{}, level: cwd.level + 1} + cwd = cwd.Dirs[p] + } else { + cwd = dd + } + } + + return cwd +} + +// Render renders dir data +func (d *Dir) render(name string, isFirst, isLast bool) { + if d.IsEmpty() { + return + } + + if isFirst { + fmtc.Printfn(TreeColorTag+"┌{!} "+DirColorTag+"%s{!}", name) + } else { + if isLast && len(d.Files) == 0 { + fmtc.Printfn( + TreeColorTag+strings.Repeat("│ ", d.level-1)+"└─{!} "+DirColorTag+"%s{!}", + name, + ) + } else { + fmtc.Printfn( + TreeColorTag+strings.Repeat("│ ", d.level-1)+"├─{!} "+DirColorTag+"%s{!}", + name, + ) + } + } + + for i, n := range d.Dirs.Names() { + if i+1 == len(d.Dirs) { + d.Dirs[n].render(n, false, true) + } else { + d.Dirs[n].render(n, false, false) + } + } + + for i, f := range d.Files { + if i+1 == len(d.Files) { + fmtc.Printfn( + TreeColorTag+strings.Repeat("│ ", d.level)+"└─{!} %s", + lscolors.Colorize(f), + ) + } else { + fmtc.Printfn( + TreeColorTag+strings.Repeat("│ ", d.level)+"├─{!} %s", + lscolors.Colorize(f), + ) + } + } +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// getRootDir returns root dir of all paths in given slice +func getRootDir(paths []string) string { + rootDir := getLongestPath(paths) + + for _, p := range paths { + for i := 0; i < len(rootDir); i++ { + if i == len(p) || rootDir[i] != p[i] { + rootDir = rootDir[:i] + break + } + } + } + + return strings.TrimRight(rootDir, "/") +} + +// getLongestPath returns the most longest path from given slice +func getLongestPath(paths []string) string { + var maxDepth int + var longestPath string + + for _, p := range paths { + depth := strings.Count(p, "/") + + if maxDepth < depth { + maxDepth = depth + longestPath = path.Dir(p) + } + } + + return longestPath +} From f02f8b536924e24203cec0e2d81b6483c7f39d60 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Mon, 2 Dec 2024 16:52:55 +0300 Subject: [PATCH 8/9] [req] Add custom encoder for arrays in 'Query' --- CHANGELOG.md | 1 + req/example_test.go | 6 ++++++ req/query.go | 40 ++++++++++++++++++++++++++++++++++++---- req/req_test.go | 12 ++++++++++++ 4 files changed, 55 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae5c4557..0d11539c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - `[fmtutil/filetree]` Added experimental package for printing file tree - `[fmtc]` Added methods `Printfn`, `Fprintfn`, `Sprintfn`, and `LPrintfn` +- `[req]` Added custom encoder for arrays in `Query` - `[fmtutil/table]` Improved support of data with ANSI escape sequences - `[fmtc]` `NameColor` marked as deprecated (_use method `AddColor` instead_) diff --git a/req/example_test.go b/req/example_test.go index f3e0e963..289c500e 100644 --- a/req/example_test.go +++ b/req/example_test.go @@ -31,9 +31,15 @@ func ExampleRequest_Do() { Method: GET, URL: "https://my.domain.com", Query: Query{ + // will be encoded as element without value + "custom": nil, "name": "Bob", "id": 120, "progress": 12.34, + // will be encoded as "groups=admins,regular" + "groups": []string{"admins", "regular"}, + // will be encoded as "zones[]=a&zones[]=b" + "zones[]": []string{"a", "b"}, }, Headers: Headers{ "My-Suppa-Header": "Test", diff --git a/req/query.go b/req/query.go index aeafe7ea..b06c115b 100644 --- a/req/query.go +++ b/req/query.go @@ -78,14 +78,24 @@ func (q Query) Encode() string { case []string: if len(u) > 0 { - buf.WriteRune('=') - queryFormatStringSlice(&buf, u) + if strings.HasSuffix(k, "[]") { + buf.WriteRune('=') + queryFormatStringSliceSplit(&buf, k, u) + } else { + buf.WriteRune('=') + queryFormatStringSlice(&buf, u) + } } case []fmt.Stringer: if len(u) > 0 { - buf.WriteRune('=') - queryFormatStringerSlice(&buf, u) + if strings.HasSuffix(k, "[]") { + buf.WriteRune('=') + queryFormatStringerSliceSplit(&buf, k, u) + } else { + buf.WriteRune('=') + queryFormatStringerSlice(&buf, u) + } } case []int: @@ -206,6 +216,17 @@ func queryFormatStringSlice(buf *bytes.Buffer, v []string) { } } +func queryFormatStringSliceSplit(buf *bytes.Buffer, k string, v []string) { + buf.WriteString(v[0]) + + for _, vv := range v[1:] { + buf.WriteRune('&') + buf.WriteString(queryFormatString(k)) + buf.WriteRune('=') + buf.WriteString(queryFormatString(vv)) + } +} + func queryFormatStringerSlice(buf *bytes.Buffer, v []fmt.Stringer) { l := buf.Len() @@ -223,6 +244,17 @@ func queryFormatStringerSlice(buf *bytes.Buffer, v []fmt.Stringer) { } } +func queryFormatStringerSliceSplit(buf *bytes.Buffer, k string, v []fmt.Stringer) { + buf.WriteString(v[0].String()) + + for _, vv := range v[1:] { + buf.WriteRune('&') + buf.WriteString(queryFormatString(k)) + buf.WriteRune('=') + buf.WriteString(queryFormatString(vv.String())) + } +} + func queryFormatNumSlice[T mathutil.Integer](buf *bytes.Buffer, v []T) { for _, vv := range v { buf.WriteString(queryFormatNumber(vv)) diff --git a/req/req_test.go b/req/req_test.go index 1dbd433d..d02a4d9f 100644 --- a/req/req_test.go +++ b/req/req_test.go @@ -760,6 +760,18 @@ func (s *ReqSuite) TestQueryParsing(c *C) { c.Assert(nq.Get("test31"), Equals, "0,1,2") c.Assert(nq.Get("test32"), Equals, "0.01,1,2.231213") c.Assert(nq.Get("test33"), Equals, "0.01,1,2.231213") + + q = Query{ + "test[]": []string{"abc", "def", "123"}, + } + + c.Assert(q.Encode(), Equals, `test%5B%5D=abc&test%5B%5D=def&test%5B%5D=123`) + + q = Query{ + "test[]": []fmt.Stringer{ts, ts}, + } + + c.Assert(q.Encode(), Equals, `test%5B%5D=test&test%5B%5D=test`) } // ////////////////////////////////////////////////////////////////////////////////// // From 8f4e20dd5ea0c8c286255c0f4b17e4a122986b1a Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Mon, 2 Dec 2024 17:12:30 +0300 Subject: [PATCH 9/9] [req] Add custom encoder for arrays in 'Query' --- req/query.go | 130 +++++++++++++++++++++++++++++++++++++++--------- req/req_test.go | 74 ++++++++++++++++++++++++--- 2 files changed, 172 insertions(+), 32 deletions(-) diff --git a/req/query.go b/req/query.go index b06c115b..eae465bb 100644 --- a/req/query.go +++ b/req/query.go @@ -100,74 +100,134 @@ func (q Query) Encode() string { case []int: if len(u) > 0 { - buf.WriteRune('=') - queryFormatNumSlice(&buf, u) + if strings.HasSuffix(k, "[]") { + buf.WriteRune('=') + queryFormatNumSliceSplit(&buf, k, u) + } else { + buf.WriteRune('=') + queryFormatNumSlice(&buf, u) + } } case []int8: if len(u) > 0 { - buf.WriteRune('=') - queryFormatNumSlice(&buf, u) + if strings.HasSuffix(k, "[]") { + buf.WriteRune('=') + queryFormatNumSliceSplit(&buf, k, u) + } else { + buf.WriteRune('=') + queryFormatNumSlice(&buf, u) + } } case []int16: if len(u) > 0 { - buf.WriteRune('=') - queryFormatNumSlice(&buf, u) + if strings.HasSuffix(k, "[]") { + buf.WriteRune('=') + queryFormatNumSliceSplit(&buf, k, u) + } else { + buf.WriteRune('=') + queryFormatNumSlice(&buf, u) + } } case []int32: if len(u) > 0 { - buf.WriteRune('=') - queryFormatNumSlice(&buf, u) + if strings.HasSuffix(k, "[]") { + buf.WriteRune('=') + queryFormatNumSliceSplit(&buf, k, u) + } else { + buf.WriteRune('=') + queryFormatNumSlice(&buf, u) + } } case []int64: if len(u) > 0 { - buf.WriteRune('=') - queryFormatNumSlice(&buf, u) + if strings.HasSuffix(k, "[]") { + buf.WriteRune('=') + queryFormatNumSliceSplit(&buf, k, u) + } else { + buf.WriteRune('=') + queryFormatNumSlice(&buf, u) + } } case []uint: if len(u) > 0 { - buf.WriteRune('=') - queryFormatNumSlice(&buf, u) + if strings.HasSuffix(k, "[]") { + buf.WriteRune('=') + queryFormatNumSliceSplit(&buf, k, u) + } else { + buf.WriteRune('=') + queryFormatNumSlice(&buf, u) + } } case []uint8: if len(u) > 0 { - buf.WriteRune('=') - queryFormatNumSlice(&buf, u) + if strings.HasSuffix(k, "[]") { + buf.WriteRune('=') + queryFormatNumSliceSplit(&buf, k, u) + } else { + buf.WriteRune('=') + queryFormatNumSlice(&buf, u) + } } case []uint16: if len(u) > 0 { - buf.WriteRune('=') - queryFormatNumSlice(&buf, u) + if strings.HasSuffix(k, "[]") { + buf.WriteRune('=') + queryFormatNumSliceSplit(&buf, k, u) + } else { + buf.WriteRune('=') + queryFormatNumSlice(&buf, u) + } } case []uint32: if len(u) > 0 { - buf.WriteRune('=') - queryFormatNumSlice(&buf, u) + if strings.HasSuffix(k, "[]") { + buf.WriteRune('=') + queryFormatNumSliceSplit(&buf, k, u) + } else { + buf.WriteRune('=') + queryFormatNumSlice(&buf, u) + } } case []uint64: if len(u) > 0 { - buf.WriteRune('=') - queryFormatNumSlice(&buf, u) + if strings.HasSuffix(k, "[]") { + buf.WriteRune('=') + queryFormatNumSliceSplit(&buf, k, u) + } else { + buf.WriteRune('=') + queryFormatNumSlice(&buf, u) + } } case []float32: if len(u) > 0 { - buf.WriteRune('=') - queryFormatFloatSlice(&buf, u) + if strings.HasSuffix(k, "[]") { + buf.WriteRune('=') + queryFormatFloatSliceSplit(&buf, k, u) + } else { + buf.WriteRune('=') + queryFormatFloatSlice(&buf, u) + } } case []float64: if len(u) > 0 { - buf.WriteRune('=') - queryFormatFloatSlice(&buf, u) + if strings.HasSuffix(k, "[]") { + buf.WriteRune('=') + queryFormatFloatSliceSplit(&buf, k, u) + } else { + buf.WriteRune('=') + queryFormatFloatSlice(&buf, u) + } } default: @@ -264,6 +324,17 @@ func queryFormatNumSlice[T mathutil.Integer](buf *bytes.Buffer, v []T) { buf.Truncate(buf.Len() - 1) } +func queryFormatNumSliceSplit[T mathutil.Integer](buf *bytes.Buffer, k string, v []T) { + buf.WriteString(queryFormatNumber(v[0])) + + for _, vv := range v[1:] { + buf.WriteRune('&') + buf.WriteString(queryFormatString(k)) + buf.WriteRune('=') + buf.WriteString(queryFormatNumber(vv)) + } +} + func queryFormatFloatSlice[T mathutil.Float](buf *bytes.Buffer, v []T) { for _, vv := range v { buf.WriteString(queryFormatFloat(vv)) @@ -272,3 +343,14 @@ func queryFormatFloatSlice[T mathutil.Float](buf *bytes.Buffer, v []T) { buf.Truncate(buf.Len() - 1) } + +func queryFormatFloatSliceSplit[T mathutil.Float](buf *bytes.Buffer, k string, v []T) { + buf.WriteString(queryFormatFloat(v[0])) + + for _, vv := range v[1:] { + buf.WriteRune('&') + buf.WriteString(queryFormatString(k)) + buf.WriteRune('=') + buf.WriteString(queryFormatFloat(vv)) + } +} diff --git a/req/req_test.go b/req/req_test.go index d02a4d9f..ae57855d 100644 --- a/req/req_test.go +++ b/req/req_test.go @@ -761,17 +761,75 @@ func (s *ReqSuite) TestQueryParsing(c *C) { c.Assert(nq.Get("test32"), Equals, "0.01,1,2.231213") c.Assert(nq.Get("test33"), Equals, "0.01,1,2.231213") - q = Query{ - "test[]": []string{"abc", "def", "123"}, - } + c.Assert( + Query{"test[]": []string{"abc", "def", "123"}}.Encode(), + Equals, `test%5B%5D=abc&test%5B%5D=def&test%5B%5D=123`, + ) - c.Assert(q.Encode(), Equals, `test%5B%5D=abc&test%5B%5D=def&test%5B%5D=123`) + c.Assert( + Query{"test[]": []fmt.Stringer{ts, ts}}.Encode(), + Equals, `test%5B%5D=test&test%5B%5D=test`, + ) - q = Query{ - "test[]": []fmt.Stringer{ts, ts}, - } + c.Assert( + Query{"test[]": []int{1, 2, 3}}.Encode(), + Equals, `test%5B%5D=1&test%5B%5D=2&test%5B%5D=3`, + ) + + c.Assert( + Query{"test[]": []int8{1, 2, 3}}.Encode(), + Equals, `test%5B%5D=1&test%5B%5D=2&test%5B%5D=3`, + ) + + c.Assert( + Query{"test[]": []int16{1, 2, 3}}.Encode(), + Equals, `test%5B%5D=1&test%5B%5D=2&test%5B%5D=3`, + ) + + c.Assert( + Query{"test[]": []int32{1, 2, 3}}.Encode(), + Equals, `test%5B%5D=1&test%5B%5D=2&test%5B%5D=3`, + ) + + c.Assert( + Query{"test[]": []int64{1, 2, 3}}.Encode(), + Equals, `test%5B%5D=1&test%5B%5D=2&test%5B%5D=3`, + ) + + c.Assert( + Query{"test[]": []uint{1, 2, 3}}.Encode(), + Equals, `test%5B%5D=1&test%5B%5D=2&test%5B%5D=3`, + ) + + c.Assert( + Query{"test[]": []uint8{1, 2, 3}}.Encode(), + Equals, `test%5B%5D=1&test%5B%5D=2&test%5B%5D=3`, + ) + + c.Assert( + Query{"test[]": []uint16{1, 2, 3}}.Encode(), + Equals, `test%5B%5D=1&test%5B%5D=2&test%5B%5D=3`, + ) - c.Assert(q.Encode(), Equals, `test%5B%5D=test&test%5B%5D=test`) + c.Assert( + Query{"test[]": []uint32{1, 2, 3}}.Encode(), + Equals, `test%5B%5D=1&test%5B%5D=2&test%5B%5D=3`, + ) + + c.Assert( + Query{"test[]": []uint64{1, 2, 3}}.Encode(), + Equals, `test%5B%5D=1&test%5B%5D=2&test%5B%5D=3`, + ) + + c.Assert( + Query{"test[]": []float32{1.2, 2.5, 3.75}}.Encode(), + Equals, `test%5B%5D=1.2&test%5B%5D=2.5&test%5B%5D=3.75`, + ) + + c.Assert( + Query{"test[]": []float64{1.2, 2.5, 3.75}}.Encode(), + Equals, `test%5B%5D=1.2&test%5B%5D=2.5&test%5B%5D=3.75`, + ) } // ////////////////////////////////////////////////////////////////////////////////// //